diff --git a/package-lock.json b/package-lock.json index 9e566eb9..7e8a720e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -770,9 +770,9 @@ } }, "@karrotmarket/mini": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@karrotmarket/mini/-/mini-0.11.3.tgz", - "integrity": "sha512-3wG4vXZBYKb+8Edow3RQCBDm6woWdTx+XpSRHirqokAwNrOFhH034LFvCTrdYdhILDWkQGiJhe6KSGJ6FMvALA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@karrotmarket/mini/-/mini-0.12.0.tgz", + "integrity": "sha512-lE+RR7Hk3VrJ5tjhtBcP6/C7+EdnLfk8r60tFLelF0lbNt6IKqze7VPk71wH0InJmubIOii5/a+nRT1DI0LriA==", "requires": { "@babel/runtime": "^7.12.5", "@oclif/command": "^1.8.0", @@ -807,23 +807,178 @@ "fastq": "^1.6.0" } }, - "@oclif/command": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.0.tgz", - "integrity": "sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw==", + "@oclif/cmd": { + "version": "npm:@oclif/command@1.8.12", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.12.tgz", + "integrity": "sha512-Qv+5kUdydIUM00HN0m/xuEB+SxI+5lI4bap1P5I4d8ZLqtwVi7Q6wUZpDM5QqVvRkay7p4TiYXRXw1rfXYwEjw==", "requires": { - "@oclif/config": "^1.15.1", - "@oclif/errors": "^1.3.3", - "@oclif/parser": "^3.8.3", - "@oclif/plugin-help": "^3", + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "@oclif/parser": "^3.8.6", + "@oclif/plugin-help": "3.2.16", "debug": "^4.1.1", "semver": "^7.3.2" }, "dependencies": { + "@oclif/command": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.11.tgz", + "integrity": "sha512-2fGLMvi6J5+oNxTaZfdWPMWY8oW15rYj0V8yLzmZBAEjfzjLqLIzJE9IlNccN1zwRqRHc1bcISSRDdxJ56IS/Q==", + "requires": { + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "@oclif/parser": "^3.8.6", + "@oclif/plugin-help": "3.2.14", + "debug": "^4.1.1", + "semver": "^7.3.2" + }, + "dependencies": { + "@oclif/plugin-help": { + "version": "3.2.14", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-3.2.14.tgz", + "integrity": "sha512-NP5qmE2YfcW3MmXjcrxiqKe9Hf3G0uK/qNc0zAMYKU4crFyIsWj7dBfQVFZSb28YXGioOOpjMzG1I7VMxKF38Q==", + "requires": { + "@oclif/command": "^1.8.9", + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "chalk": "^4.1.2", + "indent-string": "^4.0.0", + "lodash": "^4.17.21", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0" + } + } + } + }, + "@oclif/plugin-help": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-3.2.16.tgz", + "integrity": "sha512-O78iV+NhBQtviIhVEVuI21vZ9nRr9B5pR+P60oB5XFvvPKkSkV5Culih42mYU30VuWiaiWlg7+OdA4pmSPEpwg==", + "requires": { + "@oclif/command": "1.8.11", + "@oclif/config": "1.18.2", + "@oclif/errors": "1.3.5", + "chalk": "^4.1.2", + "indent-string": "^4.0.0", + "lodash": "^4.17.21", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "@oclif/command": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.12.tgz", + "integrity": "sha512-Qv+5kUdydIUM00HN0m/xuEB+SxI+5lI4bap1P5I4d8ZLqtwVi7Q6wUZpDM5QqVvRkay7p4TiYXRXw1rfXYwEjw==", + "requires": { + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "@oclif/parser": "^3.8.6", + "@oclif/plugin-help": "3.2.16", + "debug": "^4.1.1", + "semver": "^7.3.2" + }, + "dependencies": { + "@oclif/command": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.11.tgz", + "integrity": "sha512-2fGLMvi6J5+oNxTaZfdWPMWY8oW15rYj0V8yLzmZBAEjfzjLqLIzJE9IlNccN1zwRqRHc1bcISSRDdxJ56IS/Q==", + "dev": true, + "requires": { + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "@oclif/parser": "^3.8.6", + "@oclif/plugin-help": "3.2.14", + "debug": "^4.1.1", + "semver": "^7.3.2" + }, + "dependencies": { + "@oclif/plugin-help": { + "version": "3.2.14", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-3.2.14.tgz", + "integrity": "sha512-NP5qmE2YfcW3MmXjcrxiqKe9Hf3G0uK/qNc0zAMYKU4crFyIsWj7dBfQVFZSb28YXGioOOpjMzG1I7VMxKF38Q==", + "dev": true, + "requires": { + "@oclif/command": "^1.8.9", + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "chalk": "^4.1.2", + "indent-string": "^4.0.0", + "lodash": "^4.17.21", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0" + } + } + } + }, + "@oclif/plugin-help": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-3.2.16.tgz", + "integrity": "sha512-O78iV+NhBQtviIhVEVuI21vZ9nRr9B5pR+P60oB5XFvvPKkSkV5Culih42mYU30VuWiaiWlg7+OdA4pmSPEpwg==", + "requires": { + "@oclif/config": "1.18.2", + "@oclif/errors": "1.3.5", + "chalk": "^4.1.2", + "indent-string": "^4.0.0", + "lodash": "^4.17.21", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -832,13 +987,31 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } } } }, "@oclif/config": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.17.0.tgz", - "integrity": "sha512-Lmfuf6ubjQ4ifC/9bz1fSCHc6F6E653oyaRXxg+lgT4+bYf9bk+nqrUpAbrXyABkCqgIBiFr3J4zR/kiFdE1PA==", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.18.2.tgz", + "integrity": "sha512-cE3qfHWv8hGRCP31j7fIS7BfCflm/BNZ2HNqHexH+fDrdF2f1D5S8VmXWLC77ffv3oDvWyvE9AZeR0RfmHCCaA==", "requires": { "@oclif/errors": "^1.3.3", "@oclif/parser": "^3.8.0", @@ -849,9 +1022,9 @@ }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -909,82 +1082,31 @@ "integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw==" }, "@oclif/parser": { - "version": "3.8.5", - "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.5.tgz", - "integrity": "sha512-yojzeEfmSxjjkAvMRj0KzspXlMjCfBzNRPkWw8ZwOSoNWoJn+OCS/m/S+yfV6BvAM4u2lTzX9Y5rCbrFIgkJLg==", + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.6.tgz", + "integrity": "sha512-tXb0NKgSgNxmf6baN6naK+CCwOueaFk93FG9u202U7mTBHUKsioOUlw1SG/iPi9aJM3WE4pHLXmty59pci0OEw==", "requires": { "@oclif/errors": "^1.2.2", "@oclif/linewrap": "^1.0.0", - "chalk": "^2.4.2", - "tslib": "^1.9.3" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } + "chalk": "^4.1.0", + "tslib": "^2.0.0" } }, "@oclif/plugin-help": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-3.2.3.tgz", - "integrity": "sha512-l2Pd0lbOMq4u/7xsl9hqISFqyR9gWEz/8+05xmrXFr67jXyS6EUCQB+mFBa0wepltrmJu0sAFg9AvA2mLaMMqQ==", - "requires": { - "@oclif/command": "^1.5.20", - "@oclif/config": "^1.15.1", - "@oclif/errors": "^1.2.2", - "chalk": "^4.1.0", + "version": "3.2.17", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-3.2.17.tgz", + "integrity": "sha512-dutwtACVnQ0tDqu9Fq3nhYzBAW5jwhslC6tYlyMQv4WBbQXowJ1ML5CnPmaSRhm5rHtIAcR8wrK3xCV3CUcQCQ==", + "requires": { + "@oclif/cmd": "npm:@oclif/command@1.8.12", + "@oclif/config": "1.18.2", + "@oclif/errors": "1.3.5", + "chalk": "^4.1.2", "indent-string": "^4.0.0", - "lodash.template": "^4.4.0", + "lodash": "^4.17.21", "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "widest-line": "^3.1.0", - "wrap-ansi": "^4.0.0" + "wrap-ansi": "^6.2.0" }, "dependencies": { "ansi-regex": { @@ -992,32 +1114,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1027,37 +1123,13 @@ } }, "wrap-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", - "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } } } @@ -1315,6 +1387,15 @@ "@types/react-router": "*" } }, + "@types/react-slick": { + "version": "0.23.7", + "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.7.tgz", + "integrity": "sha512-v5/puo5Ix+ZeWNo2wqzfEP5CaTtEMU3qByESGt3brp98mIyKhssNaB2hgGrshu+lHAXkV1ku78Tea6FtztyXug==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/retry": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", @@ -3178,6 +3259,11 @@ } } }, + "enquire.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", + "integrity": "sha1-PoeAybi4NQhMP2DhZtvDwqPImBQ=" + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -4955,6 +5041,14 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=", + "requires": { + "string-convert": "^0.2.0" + } + }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", @@ -5119,13 +5213,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.camelcase": { "version": "4.3.0", @@ -5138,6 +5226,11 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -5168,23 +5261,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -6269,6 +6345,11 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "react-hook-form": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.21.0.tgz", + "integrity": "sha512-aekCf+dedYFIg+7nCK2acMvZ+s6Ohw2I7UNQ+zNIadBl1SoXow2Tl6c3F49xF8GFCdn5jeK43JHH26rmtdRyLQ==" + }, "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", @@ -6370,6 +6451,23 @@ "tiny-warning": "^1.0.0" } }, + "react-slick": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.28.1.tgz", + "integrity": "sha512-JwRQXoWGJRbUTE7eZI1rGIHaXX/4YuwX6gn7ulfvUZ4vFDVQAA25HcsHSYaUiRCduTr6rskyIuyPMpuG6bbluw==", + "requires": { + "classnames": "^2.2.5", + "enquire.js": "^2.1.6", + "json2mq": "^0.2.0", + "lodash.debounce": "^4.0.8", + "resize-observer-polyfill": "^1.5.0" + } + }, + "react-toast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-toast/-/react-toast-1.0.3.tgz", + "integrity": "sha512-gL3+O5hlLaoBmd36oXWKrjFeUyLCMQ04AIh48LrnUvdeg2vhJQ0E803TgVemgJvYUXKlutMVn9+/QS2DDnk26Q==" + }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -6570,6 +6668,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -6885,6 +6988,11 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "slick-carousel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", + "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==" + }, "sockjs": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", @@ -7039,6 +7147,11 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c=" + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", diff --git a/package.json b/package.json index 3cbd1900..e5099a9a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@emotion/styled": "^11.3.0", "@firebase/analytics": "^0.7.2", "@karrotframe/navigator": "^0.17.3", - "@karrotmarket/mini": "^0.11.3", + "@karrotmarket/mini": "^0.12.0", "agora-rtc-react": "^1.1.0", "agora-rtc-sdk": "^3.6.6", "axios": "^0.22.0", @@ -42,11 +42,15 @@ "react-bubble-ui": "^1.1.1", "react-cookie": "^4.1.1", "react-dom": "^17.0.2", + "react-hook-form": "^7.21.0", "react-icons": "^4.3.1", "react-loading-skeleton": "^3.0.1", "react-rewards": "^1.1.2", "react-router-dom": "^5.3.0", + "react-slick": "^0.28.1", + "react-toast": "^1.0.3", "recoil": "^0.4.1", + "slick-carousel": "^1.8.1", "swiper": "^7.0.9", "ts-node": "^10.2.1" }, @@ -55,6 +59,7 @@ "@types/dotenv-webpack": "^7.0.3", "@types/react": "^17.0.27", "@types/react-dom": "^17.0.9", + "@types/react-slick": "^0.23.7", "@types/webpack": "^5.28.0", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", diff --git a/public/index.html b/public/index.html index 5d2012e6..58f2b1a4 100644 --- a/public/index.html +++ b/public/index.html @@ -2,12 +2,13 @@ - + Document
+
diff --git a/src/App.tsx b/src/App.tsx index 4c6da96c..cecb121e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,18 +3,21 @@ import React, { useEffect } from 'react'; import { css } from '@emotion/css'; import { Navigator, Screen } from '@karrotframe/navigator'; import { getAnalytics, logEvent } from 'firebase/analytics'; +import { ToastContainer } from 'react-toast'; +import CreateMeetingForm from './components/CreateMeetingPage/CreateMeetingForm'; +import CreateGuidePage from './components/FullImgPage/CreateGuidePage'; +import GuidePage from './components/FullImgPage/GuidePage'; import LandingPage from './components/LandingPage'; import MeetingDetailPage from './components/MeetingDetailPage'; const AgoraPage = React.lazy(() => import('./components/MeetingPage')); import MeetingSuggestionPage from './components/MeetingSuggestionPage'; +import MyPage from './components/MyPage'; import NotFoundPage from './components/NotFountPage'; import NotServiceRegionPage from './components/NotServiceRegionPage'; -import OnBoardPage from './components/OnBoardPage'; import ReservationPage from './components/ReservationPage'; -import AuthWithoutMini from './hoc/AuthWithoutMini'; +import useMini from './hook/useMini'; import { app } from './util/firebase'; -import mini from './util/mini'; import { checkMobileType } from './util/utils'; const NavigatorStyle = css` @@ -24,22 +27,26 @@ const NavigatorStyle = css` export const analytics = getAnalytics(app); const App: React.FC = () => { + const { ejectApp, loginWithoutMini } = useMini(); + useEffect(() => { logEvent(analytics, 'launch_app'); - }, []); + loginWithoutMini(); + }, [loginWithoutMini]); return ( mini.close()} + onClose={ejectApp} className={NavigatorStyle} > - - - + + + + + + + diff --git a/src/api/agora.ts b/src/api/agora.ts index ba12d5f4..dcddd68e 100644 --- a/src/api/agora.ts +++ b/src/api/agora.ts @@ -31,6 +31,13 @@ export type InfoType = { recommend_user: { text: string }[]; recommend_topic: { text: string }[]; }; + host: { + id: number; + nickname: string; + profile_image_url: string; + manner_temperature: number; + region_name: string; + }; }; user: { id: number; diff --git a/src/api/image.ts b/src/api/image.ts new file mode 100644 index 00000000..1ddb00f5 --- /dev/null +++ b/src/api/image.ts @@ -0,0 +1,46 @@ +import axios from 'axios'; + +import customAxios from '../util/request'; + +export const uploadImage = async (file: File): Promise => { + const preSignedUrl = await getPreSignedUrl(file.name); + const imageUrl = await uploadToBucket(preSignedUrl, file); + return imageUrl + ? `${preSignedUrl.data.url}${preSignedUrl.data.fields.key}` + : ''; +}; + +const getPreSignedUrl = async (fileName: string) => { + const result: presignedUrlRes = await customAxios().get( + `/meetings/presigned-url?file_name=${fileName}`, + ); + return result; +}; + +const uploadToBucket = async (preSignedUrl: presignedUrlRes, file: File) => { + try { + const formData = new FormData(); + for (const [key, value] of Object.entries(preSignedUrl.data.fields)) { + formData.append(key, value); + } + formData.append('file', file); + await axios.post(preSignedUrl.data.url, formData); + return true; + } catch (e) { + return false; + } +}; + +type presignedUrlRes = { + data: { + url: string; + fields: { + key: string; + 'x-amz-algorithm': string; + 'x-amz-credential': string; + 'x-amz-date': string; + policy: string; + 'x-amz-signature': string; + }; + }; +}; diff --git a/src/api/meeting.ts b/src/api/meeting.ts index 3e7e6cf7..954b6fb8 100644 --- a/src/api/meeting.ts +++ b/src/api/meeting.ts @@ -13,6 +13,17 @@ export const getMeetings = async (region_id: string) => { } }; +export const getMyMeetings = async () => { + try { + const result: getMeetingsRes = await customAxios().get( + `/users/me/meetings`, + ); + return { success: true, data: result.data }; + } catch (e) { + return { success: false }; + } +}; + export const getMeetingDetail = async ( id: string, ): Promise => { @@ -35,6 +46,40 @@ export const increaseMeetingEnterUserCount = async ( } }; +export const createMeeting = async ( + createData: createFormType, +): Promise => { + try { + const res: { data: { id: number } } = await customAxios().post( + `/meetings/`, + createData, + ); + return { success: true, data: res.data }; + } catch (e) { + return { success: false }; + } +}; + +export const deleteMeeting = async (id: string): Promise => { + try { + await customAxios().delete(`/meetings/${id}/`); + return { success: true }; + } catch (e) { + return { success: false }; + } +}; + +export const shareMeeting = async (id: string): Promise => { + try { + const res: { data: { short_url: string } } = await customAxios().get( + `/share/short-url/meeting?meeting=${id}`, + ); + return { success: true, data: res.data }; + } catch (e) { + return { success: false }; + } +}; + interface getMeetingsRes { success: boolean; data?: MeetingList[]; @@ -48,3 +93,29 @@ interface getMeetingDetailRes { interface increaseMeetingEnterUserCountRes { success: boolean; } + +interface createFormType { + title: string; + date: string; + start_time: string; + end_time: string; + is_video: boolean; + image_url: string | undefined; + description: { + text: string; + }; +} + +interface createMeetingRes { + success: boolean; + data?: { id: number }; +} + +interface deleteMeetingRes { + success: boolean; +} + +interface shareMeetingRes { + success: boolean; + data?: { short_url: string }; +} diff --git a/src/api/user.ts b/src/api/user.ts index f2a664c4..bed4d1c1 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -28,7 +28,12 @@ interface loginReq { interface loginRes { success: boolean; - data?: { token: string; nickname: string; region: string }; + data?: { + token: string; + nickname: string; + region: string; + profile_img_url: string; + }; } interface usersRes { diff --git a/src/assets/icon/Notifications_none.tsx b/src/assets/icon/Notifications_none.tsx deleted file mode 100644 index 5331ccae..00000000 --- a/src/assets/icon/Notifications_none.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { ReactElement } from 'react'; - -import { svgProps } from 'customProps'; - -interface NotiType { - className?: string; -} - -function NotificationsNone({ - width = '24', - height = '24', - fill = 'none', - className, -}: svgProps.svg & NotiType): ReactElement { - return ( - - - - ); -} - -export default NotificationsNone; diff --git a/src/assets/icon/agora/host_icon.svg b/src/assets/icon/agora/host_icon.svg new file mode 100644 index 00000000..6827527c --- /dev/null +++ b/src/assets/icon/agora/host_icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icon/arrow_iOS_large.svg b/src/assets/icon/arrow_iOS_large.svg deleted file mode 100644 index 5e7eae24..00000000 --- a/src/assets/icon/arrow_iOS_large.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/arrow_iOS_small.svg b/src/assets/icon/arrow_iOS_small.svg deleted file mode 100644 index 10bcc845..00000000 --- a/src/assets/icon/arrow_iOS_small.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/bulb.svg b/src/assets/icon/bulb_suggestion.svg similarity index 100% rename from src/assets/icon/bulb.svg rename to src/assets/icon/bulb_suggestion.svg diff --git a/src/assets/icon/nav_back.svg b/src/assets/icon/common/nav_back.svg similarity index 100% rename from src/assets/icon/nav_back.svg rename to src/assets/icon/common/nav_back.svg diff --git a/src/assets/icon/nav_close.svg b/src/assets/icon/common/nav_close.svg similarity index 100% rename from src/assets/icon/nav_close.svg rename to src/assets/icon/common/nav_close.svg diff --git a/src/assets/icon/common/nav_my_page.svg b/src/assets/icon/common/nav_my_page.svg new file mode 100644 index 00000000..a6cc43d8 --- /dev/null +++ b/src/assets/icon/common/nav_my_page.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icon/person.svg b/src/assets/icon/common/person.svg similarity index 100% rename from src/assets/icon/person.svg rename to src/assets/icon/common/person.svg diff --git a/src/assets/icon/common/person_fill__grey.svg b/src/assets/icon/common/person_fill__grey.svg new file mode 100644 index 00000000..b7cb4410 --- /dev/null +++ b/src/assets/icon/common/person_fill__grey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icon/common/spinner.svg b/src/assets/icon/common/spinner.svg new file mode 100644 index 00000000..cdc0eea9 --- /dev/null +++ b/src/assets/icon/common/spinner.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icon/createMeeting/audio_active.svg b/src/assets/icon/createMeeting/audio_active.svg new file mode 100644 index 00000000..4d1b0aef --- /dev/null +++ b/src/assets/icon/createMeeting/audio_active.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icon/createMeeting/audio_disabled.svg b/src/assets/icon/createMeeting/audio_disabled.svg new file mode 100644 index 00000000..98a142d3 --- /dev/null +++ b/src/assets/icon/createMeeting/audio_disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icon/createMeeting/delete_icon.svg b/src/assets/icon/createMeeting/delete_icon.svg new file mode 100644 index 00000000..e9b240b4 --- /dev/null +++ b/src/assets/icon/createMeeting/delete_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icon/createMeeting/upload_img.svg b/src/assets/icon/createMeeting/upload_img.svg new file mode 100644 index 00000000..8a99e76d --- /dev/null +++ b/src/assets/icon/createMeeting/upload_img.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icon/createMeeting/video_active.svg b/src/assets/icon/createMeeting/video_active.svg new file mode 100644 index 00000000..6eca2769 --- /dev/null +++ b/src/assets/icon/createMeeting/video_active.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icon/createMeeting/video_disabled.svg b/src/assets/icon/createMeeting/video_disabled.svg new file mode 100644 index 00000000..1bee2c3e --- /dev/null +++ b/src/assets/icon/createMeeting/video_disabled.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icon/arrow_iOS_xsmall_green.svg b/src/assets/icon/detailPage/arrow_iOS_xsmall_green.svg similarity index 100% rename from src/assets/icon/arrow_iOS_xsmall_green.svg rename to src/assets/icon/detailPage/arrow_iOS_xsmall_green.svg diff --git a/src/assets/icon/cam.svg b/src/assets/icon/detailPage/cam.svg similarity index 100% rename from src/assets/icon/cam.svg rename to src/assets/icon/detailPage/cam.svg diff --git a/src/assets/icon/detailPage/info_i.svg b/src/assets/icon/detailPage/info_i.svg new file mode 100644 index 00000000..e446cc3c --- /dev/null +++ b/src/assets/icon/detailPage/info_i.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icon/mic.svg b/src/assets/icon/detailPage/mic.svg similarity index 100% rename from src/assets/icon/mic.svg rename to src/assets/icon/detailPage/mic.svg diff --git a/src/assets/icon/detailPage/neighbor_person.svg b/src/assets/icon/detailPage/neighbor_person.svg new file mode 100644 index 00000000..aa94f0d2 --- /dev/null +++ b/src/assets/icon/detailPage/neighbor_person.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icon/detailPage/share_meeting.svg b/src/assets/icon/detailPage/share_meeting.svg new file mode 100644 index 00000000..fb3df392 --- /dev/null +++ b/src/assets/icon/detailPage/share_meeting.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icon/detailPage/trailing_icon.svg b/src/assets/icon/detailPage/trailing_icon.svg new file mode 100644 index 00000000..f4b9453e --- /dev/null +++ b/src/assets/icon/detailPage/trailing_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/zoom_logo__white.svg b/src/assets/icon/detailPage/zoom_logo__white.svg similarity index 100% rename from src/assets/icon/zoom_logo__white.svg rename to src/assets/icon/detailPage/zoom_logo__white.svg diff --git a/src/assets/icon/index.ts b/src/assets/icon/index.ts index ba25548d..d3d8198e 100644 --- a/src/assets/icon/index.ts +++ b/src/assets/icon/index.ts @@ -1,6 +1,5 @@ -import ArrowBackAnd from './ArrowBackAnd'; -import ArrowBackIos from './ArrowBackIos'; import Dot from './Dot'; -import NotificationsNone from './Notifications_none'; +import ArrowBackAnd from './reservationPage/ArrowBackAnd'; +import ArrowBackIos from './reservationPage/ArrowBackIos'; -export { ArrowBackAnd, ArrowBackIos, Dot, NotificationsNone }; +export { ArrowBackAnd, ArrowBackIos, Dot }; diff --git a/src/assets/icon/landingPage/big_plus__white.svg b/src/assets/icon/landingPage/big_plus__white.svg new file mode 100644 index 00000000..1c0cf261 --- /dev/null +++ b/src/assets/icon/landingPage/big_plus__white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icon/home/camera_meeting_tag__gray.svg b/src/assets/icon/landingPage/camera_meeting_tag__gray.svg similarity index 100% rename from src/assets/icon/home/camera_meeting_tag__gray.svg rename to src/assets/icon/landingPage/camera_meeting_tag__gray.svg diff --git a/src/assets/icon/card_noti_off.svg b/src/assets/icon/landingPage/card_noti_off.svg similarity index 100% rename from src/assets/icon/card_noti_off.svg rename to src/assets/icon/landingPage/card_noti_off.svg diff --git a/src/assets/icon/card_noti_on.svg b/src/assets/icon/landingPage/card_noti_on.svg similarity index 100% rename from src/assets/icon/card_noti_on.svg rename to src/assets/icon/landingPage/card_noti_on.svg diff --git a/src/assets/icon/landingPage/tooltip_close__white.svg b/src/assets/icon/landingPage/tooltip_close__white.svg new file mode 100644 index 00000000..5cc54cd5 --- /dev/null +++ b/src/assets/icon/landingPage/tooltip_close__white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/landingPage/upcoming_noti_off__green.svg b/src/assets/icon/landingPage/upcoming_noti_off__green.svg new file mode 100644 index 00000000..ac8f33c3 --- /dev/null +++ b/src/assets/icon/landingPage/upcoming_noti_off__green.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/landingPage/upcoming_noti_on__green.svg b/src/assets/icon/landingPage/upcoming_noti_on__green.svg new file mode 100644 index 00000000..b8d75301 --- /dev/null +++ b/src/assets/icon/landingPage/upcoming_noti_on__green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icon/landingPage/video_upcoming_tag__green.svg b/src/assets/icon/landingPage/video_upcoming_tag__green.svg new file mode 100644 index 00000000..88e7cdc7 --- /dev/null +++ b/src/assets/icon/landingPage/video_upcoming_tag__green.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icon/landingPage/video_upcoming_tag__grey.svg b/src/assets/icon/landingPage/video_upcoming_tag__grey.svg new file mode 100644 index 00000000..26255e36 --- /dev/null +++ b/src/assets/icon/landingPage/video_upcoming_tag__grey.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/icon/home/voice_meeting_tag__gray.svg b/src/assets/icon/landingPage/voice_meeting_tag__gray.svg similarity index 100% rename from src/assets/icon/home/voice_meeting_tag__gray.svg rename to src/assets/icon/landingPage/voice_meeting_tag__gray.svg diff --git a/src/assets/icon/landingPage/voice_upcoming_tag__green.svg b/src/assets/icon/landingPage/voice_upcoming_tag__green.svg new file mode 100644 index 00000000..fbbf71ea --- /dev/null +++ b/src/assets/icon/landingPage/voice_upcoming_tag__green.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icon/landingPage/voice_upcoming_tag__grey.svg b/src/assets/icon/landingPage/voice_upcoming_tag__grey.svg new file mode 100644 index 00000000..28c4ca53 --- /dev/null +++ b/src/assets/icon/landingPage/voice_upcoming_tag__grey.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icon/myPage/notification_fill__grey.svg b/src/assets/icon/myPage/notification_fill__grey.svg new file mode 100644 index 00000000..e6306e8c --- /dev/null +++ b/src/assets/icon/myPage/notification_fill__grey.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icon/myPage/notification_off__grey.svg b/src/assets/icon/myPage/notification_off__grey.svg new file mode 100644 index 00000000..c4301de1 --- /dev/null +++ b/src/assets/icon/myPage/notification_off__grey.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icon/myPage/plus__white.svg b/src/assets/icon/myPage/plus__white.svg new file mode 100644 index 00000000..1397f250 --- /dev/null +++ b/src/assets/icon/myPage/plus__white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icon/btn404.svg b/src/assets/icon/notFound/btn404.svg similarity index 100% rename from src/assets/icon/btn404.svg rename to src/assets/icon/notFound/btn404.svg diff --git a/src/assets/icon/notification_empty.svg b/src/assets/icon/notification_empty.svg deleted file mode 100644 index 80f8394b..00000000 --- a/src/assets/icon/notification_empty.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/icon/notifications_none_reservation.svg b/src/assets/icon/notifications_none_reservation.svg new file mode 100644 index 00000000..1bb1407a --- /dev/null +++ b/src/assets/icon/notifications_none_reservation.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/redirect_house.svg b/src/assets/icon/redirect_house.svg deleted file mode 100644 index 9224aaa6..00000000 --- a/src/assets/icon/redirect_house.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/icon/ArrowBackAnd.tsx b/src/assets/icon/reservationPage/ArrowBackAnd.tsx similarity index 100% rename from src/assets/icon/ArrowBackAnd.tsx rename to src/assets/icon/reservationPage/ArrowBackAnd.tsx diff --git a/src/assets/icon/ArrowBackIos.tsx b/src/assets/icon/reservationPage/ArrowBackIos.tsx similarity index 100% rename from src/assets/icon/ArrowBackIos.tsx rename to src/assets/icon/reservationPage/ArrowBackIos.tsx diff --git a/src/assets/icon/tooltip_close.svg b/src/assets/icon/tooltip_close.svg deleted file mode 100644 index 6c36dc7e..00000000 --- a/src/assets/icon/tooltip_close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/image/create_meeting_guide.png b/src/assets/image/create_meeting_guide.png new file mode 100644 index 00000000..59a67eb8 Binary files /dev/null and b/src/assets/image/create_meeting_guide.png differ diff --git a/src/assets/image/home_banner.png b/src/assets/image/home_banner.png deleted file mode 100644 index b81ad93b..00000000 Binary files a/src/assets/image/home_banner.png and /dev/null differ diff --git a/src/assets/image/home_banner_01.png b/src/assets/image/home_banner_01.png new file mode 100644 index 00000000..3b717368 Binary files /dev/null and b/src/assets/image/home_banner_01.png differ diff --git a/src/assets/image/home_banner_02.png b/src/assets/image/home_banner_02.png new file mode 100644 index 00000000..6763304a Binary files /dev/null and b/src/assets/image/home_banner_02.png differ diff --git a/src/assets/image/service_guide.png b/src/assets/image/service_guide.png index 1e370026..c2ab5448 100644 Binary files a/src/assets/image/service_guide.png and b/src/assets/image/service_guide.png differ diff --git a/src/components/CreateMeetingPage/CreateMeetingForm.tsx b/src/components/CreateMeetingPage/CreateMeetingForm.tsx new file mode 100644 index 00000000..3584bb45 --- /dev/null +++ b/src/components/CreateMeetingPage/CreateMeetingForm.tsx @@ -0,0 +1,522 @@ +import React, { + ChangeEvent, + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import styled from '@emotion/styled'; +import { logEvent } from '@firebase/analytics'; +import { useNavigator } from '@karrotframe/navigator'; +import { CreateMeeting } from 'meeting'; + +import { uploadImage } from '../../api/image'; +import { createMeeting } from '../../api/meeting'; +import { analytics } from '../../App'; +import audio_disabled from '../../assets/icon/createMeeting/audio_disabled.svg'; +import video_disabled from '../../assets/icon/createMeeting/video_disabled.svg'; +import { COLOR } from '../../constant/color'; +import { CREATE_MEETING } from '../../constant/message'; +import useMini from '../../hook/useMini'; +import CustomScreenHelmet from '../common/CustomScreenHelmet'; +import Divider from '../common/Divider'; +import SpinnerModal from '../common/SpinnerModal'; +import DatePicker from './components/DatePicker'; +import EditableTextarea from './components/EditableTextarea'; +import ImageUploaderBox from './components/ImageUploaderBox'; +import TimePicker from './components/TimePicker'; +import WordCounter from './components/WordCounter'; + +function CreateMeetingForm(): ReactElement { + const [form, setForm] = useState({ + title: '', + description: '', + type: undefined, + date: '', + time: { start_time: '', end_time: '' }, + image: null, + }); + const [submitState, setSubmitState] = useState<{ + state: 'loading' | 'wait' | 'submit'; + }>({ state: 'wait' }); + const { loginWithMini } = useMini(); + const { replace } = useNavigator(); + const previewRef = useRef(null); + + const refParams = useMemo(() => { + const urlHashParams = new URLSearchParams( + window.location.hash.substr(window.location.hash.indexOf('?')), + ); + return urlHashParams.get('ref'); + }, []); + + const isValid = useMemo( + () => + form.title.length !== 0 && + form.title.length <= 40 && + form.description.length !== 0 && + form.description.length <= 140 && + form.type !== undefined && + form.date.length != 0 && + form.time.start_time.length !== 0 && + form.time.end_time.length !== 0, + [form.date, form.description, form.time, form.title, form.type], + ); + + const onSetImageHandler = useCallback( + async (e?: ChangeEvent) => { + if (e && e.target.files) { + const file = e.target.files[0]; + setForm(prevForm => ({ ...prevForm, image: file })); + } else setForm(prevForm => ({ ...prevForm, image: null })); + }, + [], + ); + + const onSubmitBtnHandler = useCallback(async () => { + setSubmitState({ state: 'loading' }); + if (!isValid) { + setSubmitState({ state: 'submit' }); + return; + } + + const uploadImageResult = form.image + ? await uploadImage(form.image) + : undefined; + + const result = await createMeeting({ + title: form.title, + date: form.date, + start_time: form.time.start_time.split(' ')[1], + end_time: form.time.end_time.split(' ')[1], + image_url: uploadImageResult, + is_video: form.type === 'video' ? true : false, + description: { text: form.description }, + }); + if (!result.success) return; + if (refParams === 'banner') + replace(`/meetings/${result.data?.id}?created=banner`); + else replace(`/meetings/${result.data?.id}?created=others`); + }, [ + form.date, + form.description, + form.image, + form.time.end_time, + form.time.start_time, + form.title, + form.type, + isValid, + refParams, + replace, + ]); + + useEffect(() => { + logEvent(analytics, 'create_meeting__show', { + from: refParams || '', + }); + }, [refParams]); + + return ( + + {CREATE_MEETING.NAVIGATOR_TITLE}} + /> + + + + <TitleText>모임 제목</TitleText> + <TitleInput + className="body3" + placeholder="모임 제목을 입력해주세요. (예. 같이 책 읽고 대화 나눠요.)" + height="8.6rem" + validation={ + (submitState.state !== 'submit' && form.title.length < 40) || + (submitState.state === 'submit' && + form.title.length !== 0 && + form.title.length < 40) + } + formHandler={(value: string) => + setForm(prevState => ({ ...prevState, title: value })) + } + /> + <ValidationInfoWarpper> + <ValidationInfo> + {form.title.length > 40 && ( + <div>모임 제목은 최대 40자까지 입력할 수 있어요.</div> + )} + {submitState.state === 'submit' && form.title.length === 0 && ( + <div>모임 제목을 입력해주세요.</div> + )} + </ValidationInfo> + <WordCounter maxWords={40} words={form.title} /> + </ValidationInfoWarpper> + + + 모임 내용 + + setForm(prevState => ({ ...prevState, description: value })) + } + /> + + +
+ {form.description.length > 140 && + '모임 내용은 최대 140자까지 입력할 수 있어요.'} +
+
+ {submitState.state === 'submit' && + form.description.length === 0 && + '모임 내용을 입력해주세요.'} +
+
+ +
+
+ + + 모임 진행 방식 + {submitState.state === 'submit' && form.type === undefined && ( + + +
모임 진행 방식을 선택해주세요.
+
+
+ )} + + + setForm(prevState => { + return { ...prevState, type: 'audio' }; + }) + } + > + + + + + 음성모임 + + + 음성모임은 목소리로만 진행되는 모임이에요. 모임 링크는 자동으로 + 생성돼요. + + + + + setForm(prevState => { + return { ...prevState, type: 'video' }; + }) + } + > + + + + + 화상모임 + + + 화상모임은 줌(zoom) 링크가 자동으로 생성돼요. 줌 어플을 + 다운로드한 후 이용할 수 있어요. + + + + +
+ + + 모임 날짜 + + 모임 날짜는 일주일 이내로 선택할 수 있어요. + + + setForm(prevState => ({ ...prevState, date: value })) + } + /> + + +
+ {submitState.state === 'submit' && + (form.date === undefined || form.date.length === 0) && + '모임 날짜를 선택해주세요.'} +
+
+
+
+ + + + + + {submitState.state === 'submit' && + !isValid && + '모든 항목을 올바르게 입력해주세요'} + + + + + submitState.state !== 'loading' && loginWithMini(onSubmitBtnHandler) + } + > + 모임 만들기 + + +
+ ); +} + +const CreateMeeting = styled.div` + width: 100%; + height: auto; + box-sizing: border-box; +`; + +const PageTitle = styled.div` + font-weight: 600; + font-size: 1.6rem; + line-height: 2.4rem; + letter-spacing: -0.03em; + box-sizing: border-box; +`; + +const ValidationInfoWarpper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 0.6rem; +`; + +const ValidationInfo = styled.div` + display: flex; + flex-direction: column; + font-size: 1.3rem; + line-height: 1.6rem; + letter-spacing: -0.03rem; + color: #ff5638; +`; + +const SubmitValidation = styled.div` + margin-bottom: 0.6rem; +`; + +const GreenInfoText = styled.div` + font-size: 1.3rem; + line-height: 1.8rem; + letter-spacing: -0.03rem; + color: ${COLOR.LIGHT_GREEN}; + margin-top: 0.8rem; +`; + +const TitleText = styled.div` + font-weight: 700; + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + + margin-bottom: 0.8rem; + color: ${COLOR.TEXT_BLACK}; +`; + +const Title = styled.div` + width: auto; + box-sizing: border-box; + margin: 0 1.6rem 3.2rem 1.6rem; + + display: flex; + flex-direction: column; + + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.GREY_500}; +`; + +const TitleInput = styled(EditableTextarea)<{ validation: boolean }>` + /* border: 1px solid + ${({ validation }) => (validation ? COLOR.GREY_400 : '#ff5638')}; + border-radius: 0.6rem; + padding: 1.6rem 1.6rem 2.4rem 1.6rem; + + &::placeholder { + color: ${COLOR.PLACEHOLDER_GREY}; + font-weight: 400; + } + + &:focus { + outline: none !important; + border: 1px solid + ${({ validation }) => (validation ? COLOR.LIGHT_GREEN : '#ff5638')}; + } */ +`; + +const Description = styled.div` + margin: 0 1.6rem 3.2rem 1.6rem; + + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.GREY_500}; +`; + +const DescriptionInput = styled(EditableTextarea)``; + +const MeetingTypeWrapper = styled.div` + margin: 0 1.6rem 4rem 1.6rem; + + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.GREY_500}; +`; + +const TypeBtnWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + margin: 1.6rem 0; +`; + +const TypeIcon = styled.img` + margin-right: 0.4rem; +`; + +const TypeBtn = styled.label` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + width: 100%; + + box-sizing: border-box; + margin-bottom: 2.4rem; +`; + +const RadioInput = styled.input` + min-width: 2rem; + min-height: 2rem; + margin-right: 0.6rem; + position: relative; + -webkit-appearance: none; + -moz-appearance: none; + box-sizing: border-box; + + border: 2px solid ${COLOR.GREY_400}; + border-radius: 100%; + + &:before { + position: absolute; + display: block; + content: ''; + border: 2px solid white; + height: 100%; + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + border-radius: 100%; + } + + &:checked { + background-color: ${COLOR.LIGHT_GREEN}; + border: 2px solid ${COLOR.LIGHT_GREEN}; + -webkit-appearance: none; + -moz-appearance: none; + border-radius: 100%; + } +`; + +const TypeContentWrapper = styled.div` + display: flex; + flex-direction: column; +`; + +const TypeHeader = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 0.8rem; + color: ${COLOR.TEXT_BLACK}; +`; + +const TypeName = styled.div` + font-size: 1.5rem; + line-height: 2.3rem; +`; + +const TypeInfo = styled.div` + font-size: 1.3rem; + line-height: 2rem; + letter-spacing: -0.03rem; + color: ${COLOR.TEXT_GREY}; +`; + +const Date = styled.div` + margin: 1.96rem 1.6rem 0 1.6rem; +`; +const Time = styled.div` + margin: 3.2rem 1.6rem 4rem 1.6rem; +`; + +const SubmitArea = styled.div` + margin: 6rem 1.6rem 4rem 1.6rem; +`; +const SubmitBtn = styled.div` + width: 100%; + background: ${COLOR.LIGHT_GREEN}; + border-radius: 0.6rem; + padding: 1.3rem 0; + + font-weight: 600; + font-size: 1.6rem; + line-height: 2.4rem; + + text-align: center; + letter-spacing: -0.03rem; + + color: ${COLOR.TEXT_WHITE}; +`; + +export default CreateMeetingForm; diff --git a/src/components/CreateMeetingPage/components/DatePicker.tsx b/src/components/CreateMeetingPage/components/DatePicker.tsx new file mode 100644 index 00000000..ebe3a5c9 --- /dev/null +++ b/src/components/CreateMeetingPage/components/DatePicker.tsx @@ -0,0 +1,102 @@ +import React, { ReactElement, useEffect } from 'react'; + +import styled from '@emotion/styled'; +import dayjs from 'dayjs'; + +import 'dayjs/locale/ko'; + +import { COLOR } from '../../../constant/color'; +dayjs.locale('ko'); + +interface Props { + submitState: { state: 'loading' | 'wait' | 'submit' }; + onChange: (value: string) => void; +} + +function DatePicker({ submitState, onChange }: Props): ReactElement { + const [dayList, setDayList] = React.useState([]); + const [dateState, setDateState] = React.useState( + undefined, + ); + + useEffect(() => { + for (let i = 0; i < 7; i++) { + const day = dayjs().add(i, 'day'); + setDayList(prevState => { + return [...prevState, day]; + }); + } + }, []); + + return ( + + { + setDateState(e.target.value); + onChange(e.target.value); + }} + selected={dateState ? true : false} + trySubmit={submitState.state === 'submit'} + > + + {dayList.map(day => { + return ( + + {day.format('MM월 DD일 dddd')} + + ); + })} + + + ); +} + +const DatePickerWrapper = styled.div` + margin-top: 1.6rem; +`; + +const SelectorStyle = styled.select<{ selected: boolean; trySubmit: boolean }>` + width: 100%; + height: 5.5rem; + color: ${({ selected }) => (selected ? COLOR.TEXT_BLACK : COLOR.GREY_500)}; + + background-color: white; + box-sizing: border-box; + border-radius: 0.6rem; + padding: 0 20px; + background: url('http://cdn1.iconfinder.com/data/icons/cc_mono_icon_set/blacks/16x16/br_down.png') + no-repeat right #ffffff; + -webkit-appearance: none; + background-position-x: calc(100% - 20px); + font-size: 1.5rem; + line-height: 2.3rem; + + border: 1px solid + ${({ trySubmit, selected }) => + !selected && trySubmit ? '#ff5638' : '#cbcccd'}; + + &:focus { + outline: none; + border: 2px solid ${COLOR.LIGHT_GREEN}; + border-radius: 0.6rem; + } +`; + +const DefaultOption = styled.option` + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.GREY_500}; +`; + +const SelectOption = styled.option` + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.TEXT_BLACK}; +`; + +export default DatePicker; diff --git a/src/components/CreateMeetingPage/components/EditableTextarea.tsx b/src/components/CreateMeetingPage/components/EditableTextarea.tsx new file mode 100644 index 00000000..a69f392b --- /dev/null +++ b/src/components/CreateMeetingPage/components/EditableTextarea.tsx @@ -0,0 +1,59 @@ +import React, { ReactElement } from 'react'; + +import styled from '@emotion/styled'; +import classnames from 'classnames'; + +import { COLOR } from '../../../constant/color'; + +interface Props { + placeholder?: string; + formHandler?: (value: string) => void; + className?: string; + height?: string; + validation?: boolean; +} + +function EditableTextarea({ + placeholder, + formHandler, + className, + height, + validation, +}: Props): ReactElement { + return ( + ) => { + formHandler && formHandler(e.target.innerText); + }} + /> + ); +} + +const EditableArea = styled.div<{ height?: string; validation?: boolean }>` + box-sizing: border-box; + width: auto; + height: ${props => props.height || 'auto'}; + padding: 1.6rem 1.6rem 2.4rem 1.6rem; + border: 1px solid + ${({ validation }) => (validation ? COLOR.GREY_400 : '#ff5638')}; + border-radius: 0.6rem; + color: ${COLOR.TEXT_BLACK}; + caret-color: ${COLOR.LIGHT_GREEN}; + overflow-y: auto; + &:focus { + outline: none !important; + border: 1px solid + ${({ validation }) => (validation ? COLOR.LIGHT_GREEN : '#ff5638')}; + } + &[placeholder]:empty::before { + content: attr(placeholder); + color: ${COLOR.PLACEHOLDER_GREY}; + } +`; + +export default EditableTextarea; diff --git a/src/components/CreateMeetingPage/components/ImageUploaderBox.tsx b/src/components/CreateMeetingPage/components/ImageUploaderBox.tsx new file mode 100644 index 00000000..a530c818 --- /dev/null +++ b/src/components/CreateMeetingPage/components/ImageUploaderBox.tsx @@ -0,0 +1,150 @@ +import React, { + ChangeEvent, + ReactElement, + useCallback, + useEffect, + useState, +} from 'react'; + +import styled from '@emotion/styled'; + +import delete_icon from '../../../assets/icon/createMeeting/delete_icon.svg'; +import upload_img from '../../../assets/icon/createMeeting/upload_img.svg'; +import { COLOR } from '../../../constant/color'; +interface Props { + previewRef: React.MutableRefObject; + onSetImageHandler: (e?: ChangeEvent) => void; + image: File | null; +} + +function ImageUploaderBox({ + previewRef, + onSetImageHandler, + image, +}: Props): ReactElement { + const [imageUrl, setImageUrl] = useState(upload_img); + + const ImageSrc = useCallback(() => { + if (image && typeof image === 'object') { + const reader = new FileReader(); + reader.onload = () => { + setImageUrl(reader.result); + }; + reader.readAsDataURL(image); + } else setImageUrl(upload_img); + }, [image]); + + useEffect(() => { + ImageSrc(); + }, [ImageSrc, image]); + + return ( + + + 사진 추가(선택) + + 모임 사진을 선택하지 않으면 기본 사진이 들어가요. + + + + + + {image && ( + onSetImageHandler()} /> + )} + + + ); +} + +const ImageUploaderBoxWrapper = styled.div` + margin: 1.96rem 1.6rem 4rem 1.6rem; +`; + +const TitleText = styled.div` + font-weight: 700; + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + + margin-bottom: 0.8rem; + color: ${COLOR.TEXT_BLACK}; +`; + +const SubTitle = styled.div` + display: inline; + margin-left: 0.4rem; + font-weight: 400; + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.FONT_BODY_GREY}; +`; + +const Notice = styled.div` + font-size: 1.3rem; + line-height: 1.9rem; + letter-spacing: -0.03rem; + + color: ${COLOR.LIGHT_GREEN}; + margin-bottom: 1.6rem; +`; + +const FileWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: flex-end; +`; + +const FileUploadBox = styled.div` + position: relative; + overflow: hidden; + + width: 23.2rem; + height: 9rem; + border: 1px solid #cbcccd; + box-sizing: border-box; + border-radius: 0.4rem; + background-size: 7rem; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +`; + +const FileUploadInput = styled.input` + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + margin: 0; + padding: 0; + font-size: 20px; + cursor: pointer; + opacity: 0; + filter: alpha(opacity=0); +`; + +const ImageInputPreview = styled.img<{ hasImage: boolean }>` + max-width: 100%; + height: auto; +`; + +const RemoveImg = styled.img` + margin-left: 0.2rem; +`; + +export default ImageUploaderBox; diff --git a/src/components/CreateMeetingPage/components/InputList.tsx b/src/components/CreateMeetingPage/components/InputList.tsx new file mode 100644 index 00000000..d709d1f9 --- /dev/null +++ b/src/components/CreateMeetingPage/components/InputList.tsx @@ -0,0 +1,52 @@ +import React, { ReactElement, useState } from 'react'; + +import styled from '@emotion/styled'; + +import { COLOR } from '../../../constant/color'; + +function InputList(): ReactElement { + const [inputElement, setInputElement] = useState([ + , + ]); + + const addInput = () => { + setInputElement(prevState => [ + ...prevState, + , + ]); + }; + return ( + + {inputElement.map(input => input)} + + + ); +} + +const InputListWrapper = styled.div` + width: 100%; + + display: flex; + flex-direction: column; + + &::placeholder { + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.GREY_500}; + } +`; + +const InputStyle = styled.input` + padding: 1.6rem; + width: 100%; + height: 5.5rem; + border: 1px solid #cbcccd; + box-sizing: border-box; + border-radius: 0.6rem; +`; + +export default InputList; diff --git a/src/components/CreateMeetingPage/components/TimePicker.tsx b/src/components/CreateMeetingPage/components/TimePicker.tsx new file mode 100644 index 00000000..ab169132 --- /dev/null +++ b/src/components/CreateMeetingPage/components/TimePicker.tsx @@ -0,0 +1,190 @@ +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; + +import styled from '@emotion/styled'; +import dayjs from 'dayjs'; +import { CreateMeeting, TimeType } from 'meeting'; + +import nav_close from '../../../assets/icon/common/nav_close.svg'; +import { COLOR } from '../../../constant/color'; + +interface Props { + date: string; + time: TimeType; + setForm: React.Dispatch>; + trySubmit: boolean; +} + +function TimePicker({ date, time, setForm, trySubmit }: Props): ReactElement { + const [startList, setStartList] = useState([]); + const [endList, setEndList] = useState([]); + + const startListHandler = useCallback(() => { + const isToday = dayjs().isSame(date, 'day'); + const day = isToday ? dayjs() : dayjs(date); + const nextDay = day.add(1, 'day').format('YYYY-MM-DD'); + const remainMin = dayjs(nextDay).diff(day, 'minute'); + for (let i = 0; i < Math.floor(remainMin / 30); i++) { + const min = day.minute(); + const time = day.add( + i * 30 + (min % 30 === 0 ? 0 : 30 - (min % 30)), + 'minute', + ); + setStartList(prevState => { + return [...prevState, time]; + }); + } + }, [date]); + + const endListHandler = useCallback(() => { + if (!time.start_time) return; + const startTime = dayjs(time.start_time); + for (let i = 1; i <= 6; i++) { + const time = startTime.add(i * 30, 'minute'); + setEndList(prevState => { + return [...prevState, time]; + }); + } + }, [time.start_time]); + + useEffect(() => { + setStartList([]); + setEndList([]); + setForm((prevState: CreateMeeting) => ({ + ...prevState, + time: { start_time: '', end_time: '' }, + })); + date && startListHandler(); + }, [date, setForm, startListHandler]); + + useEffect(() => { + setEndList([]); + setForm(prevState => { + return { ...prevState, time: { ...prevState.time, end_time: '' } }; + }); + time.start_time !== '' && endListHandler(); + }, [endListHandler, setForm, time.start_time]); + return ( + + { + setForm((prevState: CreateMeeting) => ({ + ...prevState, + time: { start_time: e.target.value, end_time: '' }, + })); + }} + selected={time.start_time !== '' ? true : false} + trySubmit={trySubmit} + > + + + {startList.map((day, idx) => { + return ( + + {day.format('a hh:mm')} + + ); + })} + + + { + setForm((prevState: CreateMeeting) => ({ + ...prevState, + time: { ...prevState.time, end_time: e.target.value }, + })); + }} + selected={time.end_time ? true : false} + trySubmit={trySubmit} + > + + {endList.map((day, idx) => { + return ( + + {day.format('a hh:mm')} + + ); + })} + + + ); +} + +const TimePickerWrapper = styled.div` + display: flex; + flex-direction: row; + + align-items: center; +`; + +const SelectorStyle = styled.select<{ selected: boolean; trySubmit: boolean }>` + width: 100%; + height: 4.7rem; + color: ${({ selected }) => (selected ? COLOR.TEXT_BLACK : COLOR.GREY_500)}; + padding: 0 1.6rem; + background-color: white; + box-sizing: border-box; + border-radius: 0.6rem; + font-size: 1.5rem; + line-height: 2.3rem; + + background: url('http://cdn1.iconfinder.com/data/icons/cc_mono_icon_set/blacks/16x16/br_down.png') + no-repeat right #ffffff; + -webkit-appearance: none; + background-position-x: calc(100% - 20px); + + border: 1px solid + ${({ trySubmit, selected }) => + !selected && trySubmit ? '#ff5638' : '#cbcccd'}; + + &:focus { + outline: none; + border: 2px solid ${COLOR.LIGHT_GREEN}; + border-radius: 0.6rem; + } +`; + +const DefaultOption = styled.option` + width: 100%; + display: flex; + flex-direction: row; + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.GREY_500}; + + &:after { + content: ${nav_close}; + background-color: red; + background-size: 28px 28px; + height: 28px; + width: 28px; + } +`; + +const Tilde = styled.div` + margin: 0.8rem; + width: 0.8rem; + height: 0.1rem; + background: ${COLOR.TEXT_BLACK}; +`; + +const SelectOption = styled.option` + font-size: 1.5rem; + line-height: 2.3rem; + letter-spacing: -0.03rem; + color: ${COLOR.TEXT_BLACK}; +`; + +export default TimePicker; diff --git a/src/components/CreateMeetingPage/components/WordCounter.tsx b/src/components/CreateMeetingPage/components/WordCounter.tsx new file mode 100644 index 00000000..2bcf6ff8 --- /dev/null +++ b/src/components/CreateMeetingPage/components/WordCounter.tsx @@ -0,0 +1,32 @@ +import React, { ReactElement } from 'react'; + +import styled from '@emotion/styled'; + +import { COLOR } from '../../../constant/color'; + +interface Props { + maxWords: number; + words: string; +} + +function WordCounter({ maxWords, words }: Props): ReactElement { + return ( + + {words.length}/{maxWords}자 + + ); +} + +const WordCounterWrapper = styled.div` + margin-right: 1.6rem; + font-size: 1.3rem; + line-height: 1.6rem; + /* identical to box height */ + + text-align: right; + letter-spacing: -0.03rem; + + color: ${COLOR.GREY_500}; +`; + +export default React.memo(WordCounter); diff --git a/src/components/FullImgPage/CreateGuidePage.tsx b/src/components/FullImgPage/CreateGuidePage.tsx new file mode 100644 index 00000000..ddca59dc --- /dev/null +++ b/src/components/FullImgPage/CreateGuidePage.tsx @@ -0,0 +1,19 @@ +import React, { ReactElement, useEffect } from 'react'; + +import { logEvent } from '@firebase/analytics'; + +import { analytics } from '../../App'; +import create_meeting_guide from '../../assets/image/create_meeting_guide.png'; +import CreateFooter from './components/CreateFooter'; +import FullImgPage from './FullImgPage'; + +function CreateGuidePage(): ReactElement { + useEffect(() => { + logEvent(analytics, 'home_banner_create_meeting__show'); + }, []); + return ( + } /> + ); +} + +export default CreateGuidePage; diff --git a/src/components/FullImgPage/FullImgPage.tsx b/src/components/FullImgPage/FullImgPage.tsx new file mode 100644 index 00000000..f6503871 --- /dev/null +++ b/src/components/FullImgPage/FullImgPage.tsx @@ -0,0 +1,45 @@ +import React, { ReactElement } from 'react'; + +import styled from '@emotion/styled'; + +import CustomScreenHelmet from '../common/CustomScreenHelmet'; + +interface Props { + imgSource: any; + footer?: any; +} +function FullImgPage({ imgSource, footer }: Props): ReactElement { + return ( + + + + + + {footer && footer} + + ); +} + +const PageWrapper = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + white-space: pre-line; + box-sizing: border-box; +`; + +const ContentsWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; + overflow-y: scroll; +`; + +const Image = styled.img` + width: 100%; + height: auto; +`; + +export default FullImgPage; diff --git a/src/components/FullImgPage/GuidePage.tsx b/src/components/FullImgPage/GuidePage.tsx new file mode 100644 index 00000000..9eba7a52 --- /dev/null +++ b/src/components/FullImgPage/GuidePage.tsx @@ -0,0 +1,16 @@ +import React, { ReactElement, useEffect } from 'react'; + +import { logEvent } from '@firebase/analytics'; + +import { analytics } from '../../App'; +import service_guide from '../../assets/image/service_guide.png'; +import FullImgPage from './FullImgPage'; + +function GuidePage(): ReactElement { + useEffect(() => { + logEvent(analytics, 'home_banner_service__show'); + }, []); + return ; +} + +export default GuidePage; diff --git a/src/components/FullImgPage/components/CreateFooter.tsx b/src/components/FullImgPage/components/CreateFooter.tsx new file mode 100644 index 00000000..11eda689 --- /dev/null +++ b/src/components/FullImgPage/components/CreateFooter.tsx @@ -0,0 +1,51 @@ +import React, { ReactElement } from 'react'; + +import styled from '@emotion/styled'; +import { logEvent } from '@firebase/analytics'; +import { useNavigator } from '@karrotframe/navigator'; + +import { analytics } from '../../../App'; +import { COLOR } from '../../../constant/color'; + +function CreateFooter(): ReactElement { + const { push } = useNavigator(); + + const onCreateBtnClickHandler = async () => { + logEvent(analytics, 'create_guide_btn__click'); + push('/create?ref=banner'); + }; + + return ( + + 모임 만들기 + + ); +} + +const FooterWrapper = styled.div` + max-height: 7rem; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 1rem 1.6rem; + border-top: 1px solid ${COLOR.NAVBAR_TOP_BORDER}; +`; + +const Btn = styled.div` + width: 100%; + height: 4.4rem; + + display: flex; + justify-content: center; + align-items: center; + border-radius: 0.6rem; + background: ${COLOR.LIGHT_GREEN}; + + font-weight: 600; + font-size: 1.6rem; + line-height: 2.3rem; + color: ${COLOR.TEXT_WHITE}; +`; + +export default CreateFooter; diff --git a/src/components/LandingPage/components/CarouselBanner.tsx b/src/components/LandingPage/components/CarouselBanner.tsx new file mode 100644 index 00000000..7f48b5d9 --- /dev/null +++ b/src/components/LandingPage/components/CarouselBanner.tsx @@ -0,0 +1,95 @@ +import React, { ReactElement } from 'react'; + +import styled from '@emotion/styled'; +import { useNavigator } from '@karrotframe/navigator'; +import Slider from 'react-slick'; + +import home_banner_01 from '../../../assets/image/home_banner_01.png'; +import home_banner_02 from '../../../assets/image/home_banner_02.png'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; +import { COLOR } from '../../../constant/color'; + +const settings = { + dots: true, + infinite: true, + autoplay: true, + speed: 700, + autoplaySpeed: 3000, + slidesToShow: 1, + slidesToScroll: 1, + adaptiveHeight: true, + customPaging: () => , +}; + +function CarouselBanner(): ReactElement { + const { push } = useNavigator(); + + return ( + + + push('/create-guide')} + /> + push('/guide')} + /> + + + ); +} + +const BannerWrapper = styled.div` + box-sizing: border-box; + overflow: hidden; + width: 100%; + height: auto; + + .slick-list, + .slick-track { + height: calc(100vw * 0.333) !important; + } + .slick-dots { + bottom: 1.2rem; + transform: translateZ(10px); + } + + .slick-dots > li { + width: auto; + height: auto; + } + + .slick-active .dot { + background-color: ${COLOR.LIGHT_GREEN}; + } + + .dot:last-child { + margin-right: 0; + } + + .slick-arrow { + display: none; + } +`; + +const CustomDot = styled.div` + width: 0.6rem; + height: 0.6rem; + background-color: #c4c4c4; + border-radius: 50%; + margin-right: 0.6rem; +`; + +const BannerImg = styled.img` + box-sizing: border-box; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +`; +export default CarouselBanner; diff --git a/src/components/LandingPage/components/MeetingCard/CurrMeetingCard.tsx b/src/components/LandingPage/components/MeetingCard/CurrMeetingCard.tsx index a79ece2d..e31cde92 100644 --- a/src/components/LandingPage/components/MeetingCard/CurrMeetingCard.tsx +++ b/src/components/LandingPage/components/MeetingCard/CurrMeetingCard.tsx @@ -2,14 +2,14 @@ import React, { ReactElement, useCallback } from 'react'; import styled from '@emotion/styled'; import { useNavigator } from '@karrotframe/navigator'; -import { MeetingList } from 'meeting'; +import { LiveStatus, MeetingList } from 'meeting'; import camera_meeting_tag__gray from '../../../../assets/icon/detailPage/camera_meeting_tag__gray.svg'; import voice_meeting_tag__gray from '../../../../assets/icon/detailPage/voice_meeting_tag__gray.svg'; import { COLOR } from '../../../../constant/color'; import Gradient from '../../../common/Gradient'; -import CurrMeetingTimer from '../CurrMeetingTimer'; import ParticipantNum from '../ParticipantNum'; +import UserProfile from '../UserProfile'; interface Props { data: MeetingList; @@ -18,7 +18,7 @@ interface Props { interface WrapperProps { idx: number; - live_status: 'live' | 'upcoming' | 'tomorrow' | 'finish'; + live_status: LiveStatus; } function CurrMeetingCard({ idx, data }: Props): ReactElement { @@ -37,7 +37,6 @@ function CurrMeetingCard({ idx, data }: Props): ReactElement { > - 진행중 - - {data.title} + + <Tag color={COLOR.ORANGE}>진행중</Tag> + {data.title} + + + + {data.user_enter_cnt !== 0 && ( )} - ); } const MeetingCardWrapper = styled.div` - margin-bottom: 1.6rem; + margin-bottom: 2.4rem; width: 100%; height: auto; border: 1px solid ${COLOR.TEXTAREA_LIGHT_GREY}; @@ -84,8 +87,7 @@ const MeetingCardWrapper = styled.div` `; const ImageWrapper = styled.div` - width: 100%; - height: 15.1rem; + height: 12rem; display: flex; justify-content: center; align-items: center; @@ -115,45 +117,25 @@ const Thumbnail = styled.img` align-items: center; `; -const LiveTag = styled.div` - position: relative; - padding: 0.5rem 0.8rem; - display: flex; - justify-content: center; - align-items: center; - - font-weight: 600; - font-size: 1.2rem; - line-height: 1.4rem; - letter-spacing: -0.03rem; - color: ${COLOR.TEXT_WHITE}; - background-color: ${COLOR.ORANGE}; - border-radius: 0.4rem; -`; - -const MeetingTypeTag = styled.img` - margin-left: 0.6rem; -`; +const MeetingTypeTag = styled.img``; const ContentsWrapper = styled.div` flex: 1; - padding: 1.4rem 1.5rem; + padding: 1.6rem; display: flex; flex-direction: column; `; const InfoWrapper = styled.div` flex: 1; - padding: 0 0.4rem; `; const Title = styled.div` font-weight: 600; max-height: 5.2rem; - font-size: 1.7rem; - line-height: 2.5rem; - letter-spacing: -0.04rem; + font-size: 1.6rem; + line-height: 2.4rem; + letter-spacing: -0.03rem; color: ${COLOR.TEXT_BLACK}; - margin-bottom: 1.4rem; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 2; @@ -164,19 +146,14 @@ const Title = styled.div` display: box; `; -const Button = styled.div` - width: 100%; - height: 4rem; - background: ${COLOR.LIGHT_GREEN}; - border-radius: 0.6rem; - font-weight: 600; - font-size: 1.4rem; - line-height: 1.7rem; - letter-spacing: -0.03rem; - color: ${COLOR.TEXT_WHITE}; - display: flex; - justify-content: center; - align-items: center; +const UserProfileWrapper = styled.div` + margin-top: 0.8rem; +`; + +const Tag = styled.div<{ color: string }>` + display: inline; + color: ${({ color }) => (color ? color : COLOR.ORANGE)}; + margin-right: 0.6rem; `; export default CurrMeetingCard; diff --git a/src/components/LandingPage/components/MeetingCard/MeetingCard.tsx b/src/components/LandingPage/components/MeetingCard/MeetingCard.tsx index 6fae0c9d..7b4298f3 100644 --- a/src/components/LandingPage/components/MeetingCard/MeetingCard.tsx +++ b/src/components/LandingPage/components/MeetingCard/MeetingCard.tsx @@ -1,23 +1,26 @@ import React, { ReactElement, useCallback, useState } from 'react'; +// import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { logEvent } from '@firebase/analytics'; import { useNavigator } from '@karrotframe/navigator'; -import { MeetingList } from 'meeting'; -import { useRecoilState, useSetRecoilState } from 'recoil'; +import { LiveStatus, MeetingList } from 'meeting'; +import { useRecoilValue } from 'recoil'; import { deleteAlarm, newAlarm } from '../../../../api/alarm'; import { analytics } from '../../../../App'; -import card_noti_off from '../../../../assets/icon/card_noti_off.svg'; -import card_noti_on from '../../../../assets/icon/card_noti_on.svg'; -import camera_meeting_tag__gray from '../../../../assets/icon/home/camera_meeting_tag__gray.svg'; -import voice_meeting_tag__gray from '../../../../assets/icon/home/voice_meeting_tag__gray.svg'; +import upcoming_noti_off__green from '../../../../assets/icon/landingPage/upcoming_noti_off__green.svg'; +import upcoming_noti_on__green from '../../../../assets/icon/landingPage/upcoming_noti_on__green.svg'; +import video_upcoming_tag__green from '../../../../assets/icon/landingPage/video_upcoming_tag__green.svg'; +import voice_upcoming_tag__green from '../../../../assets/icon/landingPage/voice_upcoming_tag__green.svg'; import { COLOR } from '../../../../constant/color'; -import { codeAtom, userInfoAtom, UserInfoType } from '../../../../store/user'; -import { getTimeForm } from '../../../../util/utils'; -import { authHandler } from '../../../../util/withMini'; +import useMini from '../../../../hook/useMini'; +import { userInfoAtom } from '../../../../store/user'; +import { getStartTimeForm } from '../../../../util/utils'; +// import ImageRenderer from '../../../common/LazyLoading/ImageRenderer'; import DeleteAlarmModal from '../../../common/Modal/DeleteAlarmModal'; import NewAlarmModal from '../../../common/Modal/NewAlarmModal'; +import UserProfile from '../UserProfile'; interface Props { data: MeetingList; @@ -27,19 +30,19 @@ interface Props { interface WrapperProps { idx: number; - live_status: 'live' | 'upcoming' | 'tomorrow' | 'finish'; + live_status: LiveStatus; } function MeetingCard({ idx, data, setMeetings }: Props): ReactElement { const [openNewAlarmModal, setOpenNewAlarmModal] = useState(false); const [openDeleteAlarmModal, setOpenDeleteAlarmModal] = useState(false); - const [userInfo, setUserInfo] = useRecoilState(userInfoAtom); - const setCode = useSetRecoilState(codeAtom); + const userInfo = useRecoilValue(userInfoAtom); + const { loginWithMini } = useMini(); const { push } = useNavigator(); const deleteAlarmHandler = useCallback(async () => { - if (data?.alarm_id && userInfo) { + if (data && data?.alarm_id && userInfo) { logEvent(analytics, 'delete_alarm__click', { location: 'meeting_card', meeting_id: data.id, @@ -66,6 +69,37 @@ function MeetingCard({ idx, data, setMeetings }: Props): ReactElement { } } return false; + }, [data, setMeetings, userInfo]); + + const alarmHandler = useCallback(async () => { + if (data?.alarm_id) { + setOpenDeleteAlarmModal(true); + } else if (data.id && userInfo) { + logEvent(analytics, 'add_alarm__click', { + location: 'meeting_card', + meeting_id: data.id, + meeting_name: data.title, + is_current: data.live_status, + userNickname: userInfo.nickname, + userRegion: userInfo.region, + }); + const result = await newAlarm(data.id.toString()); + if (result.success && result.data?.id) { + setMeetings(el => + el.map(prevState => { + if (prevState.id === data.id && result.data?.id) { + return { + ...prevState, + alarm_num: prevState.alarm_num + 1, + alarm_id: result.data.id, + }; + } + return prevState; + }), + ); + setOpenNewAlarmModal(true); + } + } }, [ data.alarm_id, data.id, @@ -75,41 +109,6 @@ function MeetingCard({ idx, data, setMeetings }: Props): ReactElement { userInfo, ]); - const alarmHandler = useCallback( - (userInfo: UserInfoType) => async (e?: React.MouseEvent) => { - e?.stopPropagation(); - if (data?.alarm_id) { - setOpenDeleteAlarmModal(true); - } else if (data.id && userInfo) { - logEvent(analytics, 'add_alarm__click', { - location: 'meeting_card', - meeting_id: data.id, - meeting_name: data.title, - is_current: data.live_status, - userNickname: userInfo.nickname, - userRegion: userInfo.region, - }); - const result = await newAlarm(data.id.toString()); - if (result.success && result.data?.id) { - setMeetings(el => - el.map(prevState => { - if (prevState.id === data.id && result.data?.id) { - return { - ...prevState, - alarm_num: prevState.alarm_num + 1, - alarm_id: result.data.id, - }; - } - return prevState; - }), - ); - setOpenNewAlarmModal(true); - } - } - }, - [data.alarm_id, data.id, data.live_status, data.title, setMeetings], - ); - const onClickCardHandler = useCallback(() => { push(`/meetings/${data.id}`); }, [data.id, push]); @@ -136,84 +135,134 @@ function MeetingCard({ idx, data, setMeetings }: Props): ReactElement { deleteAlarmHandler={deleteAlarmHandler} /> )} - - + + - - - {data.alarm_num} - - + + + + {/* */} + + - {getTimeForm( - data.start_time, - data.end_time, - data.live_status, - true, - )} + {getStartTimeForm(data.start_time, data.live_status, true)} - + {data.title} + + + + { + e.stopPropagation(); + loginWithMini(alarmHandler); + }} + > + + {data.alarm_num} + + - {data.live_status === 'upcoming' && ( - - {data.description_text} - - )} ); } const MeetingCardWrapper = styled.div` box-sizing: border-box; - margin: 0 0 1.6rem 0; + margin: 0 1.6rem; height: auto; - padding: 1.1rem 1.5rem 1.7rem 1.5rem; display: flex; - flex-direction: column; + flex-direction: row; + align-items: center; word-break: keep-all; background-color: ${COLOR.TEXT_WHITE}; - border-radius: 0.6rem; - border: 1px solid ${COLOR.GREY_200}; + box-sizing: border-box; - margin-top: ${props => (props.idx === 0 ? '1.8rem' : 0)}; + margin-top: ${props => props.idx === 0 && '0.8rem'}; `; const ContentsWrapper = styled.div` + width: calc(100% - 8rem); + padding-left: 1.6rem; display: flex; - flex-direction: column; + flex-direction: row; justify-content: space-between; `; +const CardImageWrapper = styled.div` + width: 8rem; + height: 8rem; + overflow: hidden; + position: relative; + display: flex; + justify-content: center; + align-items: center; + border-radius: 0.6rem; +`; + +const TagWrapper = styled.div` + position: absolute; + width: 100%; + left: 0; + top: 0; + + display: flex; + flex-direction: row; + z-index: 1; +`; + +const ImageThumbnail = styled.img` + min-width: 8rem; + height: 8rem; + object-fit: cover; + border-radius: 0.6rem; + overflow: hidden; +`; + +// const LazyImageItemStyle = styled(ImageRenderer)` +// width: 100%; +// height: 8rem; +// object-fit: cover; +// border-radius: 0.6rem; +// overflow: hidden; +// `; + +// const ImageStyle = css` +// //TODO: 가로 세로 비율 변경 +// width: auto; +// height: 8rem; +// object-fit: cover; +// `; + const InfoWrapper = styled.div` + /* width: calc(100% - 3rem); */ display: flex; flex-direction: column; + box-sizing: border-box; `; -const MeetingTypeTag = styled.img` - width: 6.8rem; - height: 2.4rem; - margin-bottom: 0.6rem; -`; +const MeetingTypeTag = styled.img``; const AlarmBtn = styled.div<{ hasAlarm: boolean }>` display: flex; @@ -221,65 +270,70 @@ const AlarmBtn = styled.div<{ hasAlarm: boolean }>` align-items: center; justify-content: center; box-sizing: border-box; - min-width: 6rem; + min-width: 5.5rem; - padding: 0.4rem 0.9rem 0.4rem 0.8rem; - border: ${({ hasAlarm }) => - hasAlarm ? '1px solid #41AC70' : `1px solid #85878A`}; - background: ${({ hasAlarm }) => (hasAlarm ? '#E0F3E9' : 'none')}; + padding: 0.5rem 0.9rem 0.5rem 0.8rem; + border: ${({ hasAlarm }) => (hasAlarm ? 'noen' : `1px solid #41AC70`)}; + background: ${({ hasAlarm }) => + hasAlarm ? '#E0F3E9' : COLOR.BACKGROUND_WHITE}; box-sizing: border-box; border-radius: 1.8rem; - font-weight: 700; - font-size: 1.5rem; - line-height: 1.8rem; + font-weight: 400; + font-size: 1.3rem; + line-height: 2rem; letter-spacing: -0.03rem; - color: ${({ hasAlarm }) => (hasAlarm ? '#41AC70' : COLOR.GREY_600)}; ; + color: #41ac70; `; const AlarmIcon = styled.img` width: 2.2rem; height: 2.2rem; - margin-right: 0.2rem; + margin-right: 0.473rem; `; -const CardHeader = styled.div` - width: 100%; +const AlarmWrapper = styled.div` display: flex; flex-direction: row; - align-items: center; - justify-content: space-between; + align-items: flex-start; + justify-content: center; `; const MeetingTime = styled.div` - color: ${COLOR.LIGHT_GREEN}; + font-weight: 700; + font-size: 1.4rem; + line-height: 2.1rem; + margin-bottom: 0.2rem; `; -interface MeetingTitleType { - live_status: 'live' | 'tomorrow' | 'upcoming' | 'finish'; -} - const MeetingTitle = styled.div` + width: 100%; + + font-weight: 400; + font-size: 1.5rem; + line-height: 2.3rem; color: ${COLOR.TEXT_BLACK}; - margin-bottom: ${({ live_status }: MeetingTitleType) => - live_status === 'upcoming' ? '0.8rem' : '0'}; -`; -const CardFooter = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; -`; + margin-bottom: 1.2rem; + padding-right: 0.8rem; -const FooterText = styled.div` - font-size: 1.4rem; - line-height: 1.7rem; - letter-spacing: -0.02rem; - color: ${COLOR.FONT_BODY_GREY}; - white-space: nowrap; + box-sizing: border-box; + + display: -webkit-box; + display: -ms-flexbox; + display: box; + max-height: 5.2rem; overflow: hidden; + vertical-align: top; text-overflow: ellipsis; + word-break: break-all; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +`; + +const UserProfileStyle = styled(UserProfile)` + font-size: 11px; + line-height: 100%; `; export default MeetingCard; diff --git a/src/components/LandingPage/components/MeetingList/CurrMeetingList.tsx b/src/components/LandingPage/components/MeetingList/CurrMeetingList.tsx index e2771ab1..5820b017 100644 --- a/src/components/LandingPage/components/MeetingList/CurrMeetingList.tsx +++ b/src/components/LandingPage/components/MeetingList/CurrMeetingList.tsx @@ -11,15 +11,16 @@ import CurrMeetingCard from '../MeetingCard/CurrMeetingCard'; interface Props { className?: string; meetings: MeetingList[]; + title?: string; } -function CurrMeetingList({ className, meetings }: Props): ReactElement { +function CurrMeetingList({ className, meetings, title }: Props): ReactElement { return ( - {LANDING.CURRENT_MEETING} + {title ? title : LANDING.CURRENT_MEETING} {meetings.map((el, idx) => { @@ -33,7 +34,7 @@ function CurrMeetingList({ className, meetings }: Props): ReactElement { const CurrMeetingListWrapper = styled.div` width: 100%; box-sizing: border-box; - padding: 3.2rem 1.6rem 5rem 1.6rem; + padding: 4rem 1.6rem; .swiper { width: 100%; diff --git a/src/components/LandingPage/components/MeetingList/MeetingList.tsx b/src/components/LandingPage/components/MeetingList/MeetingList.tsx index 631399e7..bf29de55 100644 --- a/src/components/LandingPage/components/MeetingList/MeetingList.tsx +++ b/src/components/LandingPage/components/MeetingList/MeetingList.tsx @@ -1,16 +1,16 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useEffect, useState } from 'react'; import styled from '@emotion/styled'; import classnames from 'classnames'; +import dayjs from 'dayjs'; import { MeetingList } from 'meeting'; import { COLOR } from '../../../../constant/color'; -import { LANDING } from '../../../../constant/message'; +import Divider from '../../../common/Divider'; import MeetingCard from '../MeetingCard/MeetingCard'; import SkeletonCard from '../MeetingCard/SkeletonCard'; interface Props { - title: string; className?: string; meetings: MeetingList[]; hasMeetings: boolean; @@ -18,47 +18,55 @@ interface Props { } function MeetingList({ - title, className, meetings, hasMeetings, setMeetings, }: Props): ReactElement { + const [dateList, setDateList] = useState([]); + + useEffect(() => { + const filteredDate = meetings.map(el => el.date); + const result = filteredDate.reduce((unique: string[], item) => { + return unique.includes(item) ? unique : [...unique, item]; + }, []); + setDateList(result); + }, [meetings]); + return ( - {title === LANDING.UPCOMING_MEETING ? ( - - {LANDING.UPCOMING_MEETING_01} - <MeetingCounter className="title2 meeting-list__counter"> - {meetings && meetings.length.toString()} - </MeetingCounter> - {LANDING.UPCOMING_MEETING_02} - - ) : ( - - {title} - <MeetingCounter className="title2 meeting-list__counter"> - {meetings && meetings.length.toString()} - </MeetingCounter> - - )} + + 다가오는 모임 + <MeetingCounter className="title2 meeting-list__counter"> + {meetings && meetings.length.toString()} + </MeetingCounter> + + {dateList.map((date, dateListIdx) => { + const filteredMeetings = meetings.filter(el => el.date === date); + return ( + + {dayjs(date).format('MM월 DD일')} + {filteredMeetings.map((meeting, meetingIdx) => { + return ( +
+ + {filteredMeetings.length - 1 !== meetingIdx && ( + + )} +
+ ); + })} +
+ ); + })} - {meetings.length !== 0 ? ( - meetings.map((el, idx) => { - return ( - - ); - }) - ) : ( -
- )} {!hasMeetings && }
); @@ -66,10 +74,7 @@ function MeetingList({ const MeetingListWrapper = styled.div` box-sizing: border-box; - padding: 5rem 1.6rem 5rem 1.6rem; - .meeting-card:last-child { - margin-bottom: 0; - } + padding: 4rem 0; `; const Title = styled.div` @@ -85,12 +90,43 @@ const MeetingCounter = styled.div` `; const ListTitle = styled.div` + margin: 0 1.6rem 1.4rem 1.6rem; font-weight: 700; font-size: 2rem; line-height: 2.8rem; letter-spacing: -0.05rem; color: ${COLOR.TEXT_BLACK}; - padding-left: 0.4rem; +`; + +const DateWrapper = styled.div` + position: static; + width: 100%; + height: auto; + box-sizing: border-box; + + .meeting-card:last-child { + padding-bottom: 3.4rem; + } +`; + +const DateLabel = styled.div` + box-sizing: border-box; + position: -webkit-sticky; + position: sticky; + width: 100%; + top: 0; + background: ${COLOR.BACKGROUND_WHITE}; + z-index: 10; + padding: 1rem 1.6rem; + + font-weight: 700; + font-size: 1.5rem; + line-height: 2.3rem; + color: ${COLOR.TEXT_BLACK}; +`; + +const DividerStyle = styled(Divider)` + margin: 2rem 0; `; export default MeetingList; diff --git a/src/components/LandingPage/components/ParticipantNum.tsx b/src/components/LandingPage/components/ParticipantNum.tsx index 60720da8..054e4e97 100644 --- a/src/components/LandingPage/components/ParticipantNum.tsx +++ b/src/components/LandingPage/components/ParticipantNum.tsx @@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'; import styled from '@emotion/styled'; -import person from '../../../assets/icon/person.svg'; +import person_fill__grey from '../../../assets/icon/common/person_fill__grey.svg'; import { COLOR } from '../../../constant/color'; interface Props { @@ -12,10 +12,8 @@ interface Props { function ParticipantNum({ userMeetingNum }: Props): ReactElement { return ( - - - 누적 참여자 {userMeetingNum}명 - + + 참여 이웃 {userMeetingNum}명 ); } @@ -24,15 +22,17 @@ const ParticipantNumWrapper = styled.div` display: flex; flex-direction: row; align-items: center; - margin-bottom: 1rem; + margin-top: 1.6rem; `; const ParticipantIcon = styled.img` - margin-right: 0.4rem; + margin-right: 0.6rem; `; const Participant = styled.div` - color: ${COLOR.TEXT_GREY}; + font-size: 1.3rem; + line-height: 2rem; + color: ${COLOR.FONT_BODY_GREY}; `; export default ParticipantNum; diff --git a/src/components/LandingPage/components/UserProfile.tsx b/src/components/LandingPage/components/UserProfile.tsx new file mode 100644 index 00000000..9be9184b --- /dev/null +++ b/src/components/LandingPage/components/UserProfile.tsx @@ -0,0 +1,53 @@ +import React, { ReactElement } from 'react'; + +import styled from '@emotion/styled'; +import classnames from 'classnames'; + +import { COLOR } from '../../../constant/color'; + +interface Props { + profileUrl?: string; + nickname: string; + region: string; + className?: string; +} + +function UserProfile({ + profileUrl, + nickname, + region, + className, +}: Props): ReactElement { + return ( + + {profileUrl && } + {nickname} + · + {region} + + ); +} + +const UserProfileWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + font-size: 1.3rem; + line-height: 2rem; +`; + +const ProfileImg = styled.img` + width: 2rem; + height: 2rem; + border-radius: 50%; + margin-right: 0.8rem; +`; +const Text = styled.div` + color: ${COLOR.TEXT_GREY}; +`; +const DotDivider = styled.div` + margin: 0 0.4rem; +`; +export default UserProfile; diff --git a/src/components/LandingPage/index.tsx b/src/components/LandingPage/index.tsx index 2ee3683e..5af36a98 100644 --- a/src/components/LandingPage/index.tsx +++ b/src/components/LandingPage/index.tsx @@ -1,7 +1,7 @@ /** @jsx jsx */ import React, { useCallback, useEffect, useState } from 'react'; -import { jsx } from '@emotion/react'; +import { jsx, keyframes } from '@emotion/react'; import styled from '@emotion/styled'; import { logEvent } from '@firebase/analytics'; import { useNavigator } from '@karrotframe/navigator'; @@ -10,31 +10,54 @@ import { useRecoilValue } from 'recoil'; import { getMeetings } from '../../api/meeting'; import { analytics } from '../../App'; -import home_banner from '../../assets/image/home_banner.png'; +import nav_my_page from '../../assets/icon/common/nav_my_page.svg'; +import big_plus__white from '../../assets/icon/landingPage/big_plus__white.svg'; +import tooltip_close__white from '../../assets/icon/landingPage/tooltip_close__white.svg'; import nav_logo from '../../assets/image/nav_logo.png'; -import suggestion_img from '../../assets/image/suggestion_img.png'; -import { LANDING } from '../../constant/message'; +import { COLOR } from '../../constant/color'; +import useMini from '../../hook/useMini'; import { userInfoAtom } from '../../store/user'; import { getRegionId } from '../../util/utils'; import CustomScreenHelmet from '../common/CustomScreenHelmet'; import Divider from '../common/Divider'; +import CarouselBanner from './components/CarouselBanner'; +import SkeletonCard from './components/MeetingCard/SkeletonCard'; import CurrMeetingList from './components/MeetingList/CurrMeetingList'; import MeetingList from './components/MeetingList/MeetingList'; import { useRedirect } from './useRedirect'; const LandingPage: React.FC = () => { const { push, replace } = useNavigator(); - + const [showTooltip, setShowTooltip] = useState(false); const [meetings, setMeetings] = useState([]); const userInfo = useRecoilValue(userInfoAtom); const redirectUrl = useRedirect(); + const { loginWithMini } = useMini(); const meetingListHandler = useCallback(async () => { const region_id = getRegionId(window.location.search); const result = await getMeetings(region_id); - if (result.success && result.data) setMeetings(result.data); + if (result.success && result.data) + setMeetings( + result.data.sort((a, b) => { + if (a.date === b.date) return a.start_time < b.start_time ? -1 : 1; + return a.date < b.date ? -1 : 1; + }), + ); }, [setMeetings]); + const tooltipCloseHandler = () => { + const tooltip = window.localStorage.getItem('create_btn_tooltip'); + if (!tooltip) { + window.localStorage.setItem('create_btn_tooltip', 'true'); + setShowTooltip(false); + } + }; + + const myPageHandler = () => { + push('/me'); + }; + useEffect(() => { if (redirectUrl) replace(redirectUrl); }, [redirectUrl, replace]); @@ -44,20 +67,49 @@ const LandingPage: React.FC = () => { }, [meetingListHandler, push, userInfo]); useEffect(() => { - if (userInfo) { - logEvent(analytics, 'landing_page__show'); - } - }, [userInfo]); + logEvent(analytics, 'landing_page__show'); + const tooltip = window.localStorage.getItem('create_btn_tooltip'); + if (!tooltip) setShowTooltip(true); + }, []); return ( - } /> - push('/guide')} + } + appendRight={ + loginWithMini(myPageHandler)} + /> + } /> - {meetings.filter(el => el.live_status === 'live').length !== 0 && ( + + + + + + {showTooltip && ( + + + 버튼을 눌러 모임을 만들어 보세요{' '} + tooltipCloseHandler()} + /> + + + )} + { + tooltipCloseHandler(); + push('/create'); + }} + > + + + + {meetings.length === 0 && } + {meetings.filter(el => el.live_status === 'live') && (
{
)} - {meetings.filter(el => el.live_status === 'upcoming').length !== 0 && ( + {
el.live_status === 'upcoming')} - hasMeetings={meetings.length !== 0 ? true : false} + meetings={meetings.filter( + el => el.live_status !== 'live' && el.live_status !== 'finish', + )} + hasMeetings={meetings.length !== 0} setMeetings={setMeetings} />
- )} - el.live_status === 'tomorrow')} - hasMeetings={meetings.length !== 0 ? true : false} - setMeetings={setMeetings} - /> - - - push('/suggestion/meeting')} - /> - + }
); }; +const tooltipAni = keyframes` + 0% { + transform: translate3d(0,0,0); + } + 50% { + transform: translate3d(0, -5px, 0); + } + +`; + const PageWrapper = styled.div` width: 100%; - height: 100%; + min-height: 100%; display: flex; flex-direction: column; box-sizing: border-box; `; const PageTitle = styled.img` - margin-left: 3.2rem; - height: 33%; + height: 1.43rem; width: auto; `; -const BannerImg = styled.img` +const UserIcon = styled.img` + width: 2.4rem; + height: 2.4rem; + + margin-right: 1.6rem; +`; + +const CreateBtnWrapper = styled.div` + position: -webkit-sticky; /* 사파리 브라우저 지원 */ + position: sticky; + top: calc(100% - 9rem); + left: calc(100% - 7.6rem); + width: 0; + height: 0; + z-index: 1000; +`; + +const CreateBtn = styled.div` + position: 0; box-sizing: border-box; - width: 100%; + width: 5.6rem; + height: 5.6rem; + border-radius: 50%; + background: ${COLOR.LIGHT_GREEN}; + color: ${COLOR.TEXT_WHITE}; display: flex; justify-content: center; align-items: center; + font-size: 3rem; + line-height: 5.6rem; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.3), 0px 4px 8px rgba(0, 0, 0, 0.15); `; -const SuggestionBannerWrapper = styled.div` - padding: 2.4rem 1.6rem 5rem 1.6rem; +const ToolTipOutside = styled.div` + position: relative; + width: calc(100vw - 2rem); + right: calc(100vw - 7.6rem); `; -const SuggestionImg = styled.img` - width: 100%; - height: 100%; +const ToolTip = styled.div` + animation: ${tooltipAni} 1s ease infinite; + position: absolute; + max-width: calc(100vw - 3.2rem); + box-sizing: border-box; + padding: 1.1rem 1.2rem; + background: ${COLOR.GREY_900}; + border-radius: 0.6rem; + bottom: 1rem; + right: 0; + + font-size: 1.3rem; + line-height: 2rem; + color: ${COLOR.TEXT_WHITE}; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + &:after { + content: ''; + position: absolute; + bottom: 0; + right: 2.1rem; + width: 0; + height: 0; + border: 0.7rem solid transparent; + border-top-color: ${COLOR.GREY_900}; + border-bottom: 0; + margin-left: -0.7rem; + margin-bottom: -0.7rem; + } +`; + +const ToolTipIcon = styled.img` + margin-left: 0.6rem; `; export default LandingPage; diff --git a/src/components/MeetingDetailPage/components/AudioMeetBottomSheet.tsx b/src/components/MeetingDetailPage/components/AudioMeetBottomSheet.tsx index cfe6191d..5ebd0001 100644 --- a/src/components/MeetingDetailPage/components/AudioMeetBottomSheet.tsx +++ b/src/components/MeetingDetailPage/components/AudioMeetBottomSheet.tsx @@ -5,7 +5,7 @@ import { logEvent } from '@firebase/analytics'; import { increaseMeetingEnterUserCount } from '../../../api/meeting'; import { analytics } from '../../../App'; -import closeBtn from '../../../assets/icon/nav_close.svg'; +import closeBtn from '../../../assets/icon/common/nav_close.svg'; import agoraBottomSheet from '../../../assets/image/agora_bottom_sheet.png'; import { COLOR } from '../../../constant/color'; import BottomSheet from '../../common/BottomSheet'; @@ -41,17 +41,18 @@ function AudioMeetBottomSheet({ }; const onClickJoinHandler = useCallback(async () => { + logEvent(analytics, 'audio_bottom_sheet_join__click', { + location: 'audio_bottom_sheet', + meeting_id: meetingId, + meeting_name: meetingTitle, + }); const windowReference = window.open( `/#/agora?meeting_code=${code}`, '_blank', ); await increaseMeetingEnterUserCount(meetingId); - logEvent(analytics, 'audio_bottom_sheet_join__click', { - location: 'audio_bottom_sheet', - meeting_id: meetingId, - meeting_name: meetingTitle, - }); + windowReference; onClickJoin && onClickJoin(); closeHandler(); diff --git a/src/components/MeetingDetailPage/components/AlarmFooter.tsx b/src/components/MeetingDetailPage/components/Footer/AlarmFooter.tsx similarity index 64% rename from src/components/MeetingDetailPage/components/AlarmFooter.tsx rename to src/components/MeetingDetailPage/components/Footer/AlarmFooter.tsx index c6a7630f..77c1494c 100644 --- a/src/components/MeetingDetailPage/components/AlarmFooter.tsx +++ b/src/components/MeetingDetailPage/components/Footer/AlarmFooter.tsx @@ -1,27 +1,26 @@ import React, { ReactElement } from 'react'; import styled from '@emotion/styled'; +import { logEvent } from '@firebase/analytics'; import { useCurrentScreen } from '@karrotframe/navigator'; import { MeetingDetail } from 'meeting'; -import { useRecoilState, useSetRecoilState } from 'recoil'; -import fire_emoji from '../../../assets/icon/detailPage/fire_emoji.svg'; -import notification_empty_green from '../../../assets/icon/detailPage/notification_empty_green.svg'; -import notification_fill_white from '../../../assets/icon/detailPage/notification_fill_white.svg'; -import smile_emoji from '../../../assets/icon/detailPage/smile_emoji.svg'; -import { COLOR } from '../../../constant/color'; -import { codeAtom, userInfoAtom, UserInfoType } from '../../../store/user'; -import { authHandler } from '../../../util/withMini'; +import { analytics } from '../../../../App'; +import fire_emoji from '../../../../assets/icon/detailPage/fire_emoji.svg'; +import notification_empty_green from '../../../../assets/icon/detailPage/notification_empty_green.svg'; +import notification_fill_white from '../../../../assets/icon/detailPage/notification_fill_white.svg'; +import smile_emoji from '../../../../assets/icon/detailPage/smile_emoji.svg'; +import { COLOR } from '../../../../constant/color'; +import useMini from '../../../../hook/useMini'; interface Props { data: MeetingDetail | undefined; - alarmHandler: (userInfo: UserInfoType) => (e?: React.MouseEvent) => void; + alarmHandler: () => void; fromFeed: boolean; } -function AlarmFooter({ data, alarmHandler, fromFeed }: Props): ReactElement { - const [userInfo, setUserInfo] = useRecoilState(userInfoAtom); - const setCode = useSetRecoilState(codeAtom); +function AlarmFooter({ data, alarmHandler }: Props): ReactElement { + const { loginWithMini } = useMini(); const { isRoot } = useCurrentScreen(); return ( @@ -31,33 +30,29 @@ function AlarmFooter({ data, alarmHandler, fromFeed }: Props): ReactElement { {data?.alarm_id ? '모임이 시작되면 알림을 보내드릴게요' - : '알림 신청하고 랜동모에서 이웃을 만나보세요!'} + : '알림 신청하고 랜동모에서 이웃을 만나보세요'} )}