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}}
+ />
+
+
+
+ 모임 제목
+
+ setForm(prevState => ({ ...prevState, title: value }))
+ }
+ />
+
+
+ {form.title.length > 40 && (
+ 모임 제목은 최대 40자까지 입력할 수 있어요.
+ )}
+ {submitState.state === 'submit' && form.title.length === 0 && (
+ 모임 제목을 입력해주세요.
+ )}
+
+
+
+
+
+ 모임 내용
+
+ 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}
+
+ 진행중
+ {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}
-
- {meetings && meetings.length.toString()}
-
- {LANDING.UPCOMING_MEETING_02}
-
- ) : (
-
- {title}
-
- {meetings && meetings.length.toString()}
-
-
- )}
+
+ 다가오는 모임
+
+ {meetings && meetings.length.toString()}
+
+
+ {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
? '모임이 시작되면 알림을 보내드릴게요'
- : '알림 신청하고 랜동모에서 이웃을 만나보세요!'}
+ : '알림 신청하고 랜동모에서 이웃을 만나보세요'}
)}