diff --git a/.eslintcache b/.eslintcache
deleted file mode 100644
index e05dc52..0000000
--- a/.eslintcache
+++ /dev/null
@@ -1 +0,0 @@
-[{"/Users/museop/Documents/project/song-request/frontend/src/index.js":"1","/Users/museop/Documents/project/song-request/frontend/src/App.js":"2"},{"size":197,"mtime":1610732524756,"results":"3","hashOfConfig":"4"},{"size":80,"mtime":1610731870030,"results":"5","hashOfConfig":"4"},{"filePath":"6","messages":"7","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1koacnd",{"filePath":"8","messages":"9","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/museop/Documents/project/song-request/frontend/src/index.js",[],"/Users/museop/Documents/project/song-request/frontend/src/App.js",[]]
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4d29575..2a1bdf9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+
+# eslint
+.eslintcache
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 88b52e3..33a9a27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1136,6 +1136,29 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
},
+ "@emotion/is-prop-valid": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+ "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+ "requires": {
+ "@emotion/memoize": "0.7.4"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
+ },
+ "@emotion/stylis": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
+ "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
+ },
+ "@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+ },
"@eslint/eslintrc": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz",
@@ -2989,6 +3012,11 @@
"postcss-value-parser": "^4.1.0"
}
},
+ "autosize": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.2.tgz",
+ "integrity": "sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA=="
+ },
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -3189,6 +3217,22 @@
"resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz",
"integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw=="
},
+ "babel-plugin-styled-components": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz",
+ "integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==",
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.0.0",
+ "@babel/helper-module-imports": "^7.0.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "lodash": "^4.17.11"
+ }
+ },
+ "babel-plugin-syntax-jsx": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
+ "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
+ },
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
@@ -3845,6 +3889,11 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg=="
},
+ "camelize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+ },
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -4146,6 +4195,11 @@
}
}
},
+ "computed-style": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz",
+ "integrity": "sha1-fzRP2FhLLkJb7cpKGvwOMAuwXXQ="
+ },
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4422,6 +4476,11 @@
"postcss": "^7.0.5"
}
},
+ "css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
+ },
"css-color-names": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
@@ -4505,6 +4564,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "css-to-react-native": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
+ "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -7005,6 +7074,19 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
+ "history": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+ "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "loose-envify": "^1.2.0",
+ "resolve-pathname": "^3.0.0",
+ "tiny-invariant": "^1.0.2",
+ "tiny-warning": "^1.0.0",
+ "value-equal": "^1.0.1"
+ }
+ },
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -7015,6 +7097,14 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
+ "hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "requires": {
+ "react-is": "^16.7.0"
+ }
+ },
"hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -9558,6 +9648,14 @@
"type-check": "~0.4.0"
}
},
+ "line-height": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz",
+ "integrity": "sha1-SxIF7d4YKHKl76PI9iCzGHqcVMk=",
+ "requires": {
+ "computed-style": "~0.1.3"
+ }
+ },
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -9871,6 +9969,15 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
},
+ "mini-create-react-context": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
+ "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
+ "requires": {
+ "@babel/runtime": "^7.12.1",
+ "tiny-warning": "^1.0.3"
+ }
+ },
"mini-css-extract-plugin": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz",
@@ -12186,6 +12293,16 @@
"whatwg-fetch": "^3.4.1"
}
},
+ "react-autosize-textarea": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz",
+ "integrity": "sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==",
+ "requires": {
+ "autosize": "^4.0.2",
+ "line-height": "^0.3.1",
+ "prop-types": "^15.5.6"
+ }
+ },
"react-dev-utils": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.1.tgz",
@@ -12307,16 +12424,79 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz",
"integrity": "sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw=="
},
+ "react-icons": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.1.0.tgz",
+ "integrity": "sha512-FCXBg1JbbR0vWALXIxmFAfozHdVIJmmwCD81Jk0EKOt7Ax4AdBNcaRkWhR0NaKy9ugJgoY3fFvo0PHpte55pXg=="
+ },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "react-redux": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",
+ "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==",
+ "requires": {
+ "@babel/runtime": "^7.12.1",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.13.1"
+ }
+ },
"react-refresh": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
"integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
},
+ "react-router": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
+ "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "history": "^4.9.0",
+ "hoist-non-react-statics": "^3.1.0",
+ "loose-envify": "^1.3.1",
+ "mini-create-react-context": "^0.4.0",
+ "path-to-regexp": "^1.7.0",
+ "prop-types": "^15.6.2",
+ "react-is": "^16.6.0",
+ "tiny-invariant": "^1.0.2",
+ "tiny-warning": "^1.0.0"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "path-to-regexp": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+ "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "requires": {
+ "isarray": "0.0.1"
+ }
+ }
+ }
+ },
+ "react-router-dom": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
+ "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "history": "^4.9.0",
+ "loose-envify": "^1.3.1",
+ "prop-types": "^15.6.2",
+ "react-router": "5.2.0",
+ "tiny-invariant": "^1.0.2",
+ "tiny-warning": "^1.0.0"
+ }
+ },
"react-scripts": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.1.tgz",
@@ -12498,6 +12678,20 @@
"strip-indent": "^3.0.0"
}
},
+ "redux": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
+ "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "symbol-observable": "^1.2.0"
+ }
+ },
+ "redux-thunk": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
+ "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
+ },
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -12753,6 +12947,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
},
+ "resolve-pathname": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
+ "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
+ },
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@@ -13386,6 +13585,11 @@
"kind-of": "^6.0.2"
}
},
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -14071,6 +14275,28 @@
"schema-utils": "^2.7.0"
}
},
+ "styled-components": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.1.tgz",
+ "integrity": "sha512-sBdgLWrCFTKtmZm/9x7jkIabjFNVzCUeKfoQsM6R3saImkUnjx0QYdLwJHBjY9ifEcmjDamJDVfknWm1yxZPxQ==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^0.8.8",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ }
+ },
+ "styled-reset": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/styled-reset/-/styled-reset-4.3.4.tgz",
+ "integrity": "sha512-1UmkWmRwSPfzolKleyPjbZdBqkxSXv5ImqTP5WeSjWk0Z7IvEzsrYhrqinZfCg10eM1P6BEtFly8+puQJnN/0A=="
+ },
"stylehacks": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
@@ -14150,6 +14376,11 @@
"util.promisify": "~1.0.0"
}
},
+ "symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+ },
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -14427,6 +14658,16 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
+ "tiny-invariant": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
+ "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
+ },
+ "tiny-warning": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+ },
"tmpl": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
@@ -14897,6 +15138,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
+ "value-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
+ "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
+ },
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/package.json b/package.json
index 0a928bc..6666f44 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,16 @@
"@testing-library/react": "^11.2.3",
"@testing-library/user-event": "^12.6.0",
"react": "^17.0.1",
+ "react-autosize-textarea": "^7.1.0",
"react-dom": "^17.0.1",
+ "react-icons": "^4.1.0",
+ "react-redux": "^7.2.2",
+ "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
+ "redux": "^4.0.5",
+ "redux-thunk": "^2.3.0",
+ "styled-components": "^5.2.1",
+ "styled-reset": "^4.3.4",
"web-vitals": "^0.2.4"
},
"scripts": {
diff --git a/src/App.js b/src/App.js
index 958f593..85f5c7c 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,31 @@
+import {
+ BrowserRouter as Router,
+ Route,
+ Switch,
+ Redirect
+} from "react-router-dom";
+import GlobalStyle from "./styles/GlobalStyle";
+import Header from "./components/Header/Header";
+import Letters from "./pages/Letters";
+import Footer from "./components/Footer/Footer";
+
function App() {
- return
Initialize project
;
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
}
export default App;
diff --git a/src/api/letters.js b/src/api/letters.js
new file mode 100644
index 0000000..19c8613
--- /dev/null
+++ b/src/api/letters.js
@@ -0,0 +1,33 @@
+import letters from "../mockData";
+
+const sleep = n => new Promise(resolve => setTimeout(resolve, n));
+
+export const getLetters = async () => {
+ await sleep(500);
+ return letters;
+};
+
+export const getLetterById = async id => {
+ await sleep(100);
+ return letters.find(letter => letter.id === id);
+};
+
+export const updateLetter = async (id, payload) => {
+ await sleep(100);
+ const { title, artist, imageUrl } = payload;
+ const songStory = payload.songStory;
+
+ const foundLetter = letters.find(letter => letter.id === id);
+
+ const updatedLetter = {
+ ...foundLetter,
+ song: { title, artist, imageUrl },
+ songStory
+ };
+
+ const index = letters.indexOf(foundLetter);
+ letters.splice(index, 1);
+ letters.push(updatedLetter);
+
+ return updatedLetter;
+};
diff --git a/src/api/song.js b/src/api/song.js
new file mode 100644
index 0000000..821fbe6
--- /dev/null
+++ b/src/api/song.js
@@ -0,0 +1,8 @@
+import song from "../mockData/song";
+
+const sleep = n => new Promise(resolve => setTimeout(resolve, n));
+
+export const searchSong = async (artist, title) => {
+ await sleep(100);
+ return song;
+};
diff --git a/src/assets/realpiano_logo_white.jpg b/src/assets/realpiano_logo_white.jpg
new file mode 100644
index 0000000..d03df9d
Binary files /dev/null and b/src/assets/realpiano_logo_white.jpg differ
diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx
new file mode 100644
index 0000000..bc3ca34
--- /dev/null
+++ b/src/components/Footer/Footer.jsx
@@ -0,0 +1,24 @@
+import React from "react";
+import styled from "styled-components";
+
+const Footer = () => {
+ return (
+
+ © Museop Kim
+
+ );
+};
+
+const FooterBlock = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 8rem;
+ margin-top: 6rem;
+ background-color: #f5f5f7;
+ font-weight: 500;
+ box-shadow: 0px -10px 35px 5px #f1f1f1;
+`;
+
+export default Footer;
diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx
new file mode 100644
index 0000000..1dfeb08
--- /dev/null
+++ b/src/components/Header/Header.jsx
@@ -0,0 +1,41 @@
+import React from "react";
+import styled from "styled-components";
+import HeaderTitle from "./HeaderTitle";
+import Navigation from "./Navigation";
+import UserProfile from "./UserProfile";
+
+const Header = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const HeaderBlock = styled.div`
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ position: fixed;
+ box-shadow: 0px 15px 30px 10px #f1f1f1;
+ z-index: 9;
+`;
+
+const HeaderInner = styled.div`
+ padding: 0rem 5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ top: 0;
+ left: 0;
+ max-width: 1100px;
+ width: 100%;
+ background-color: #ffffff;
+ height: 80px;
+`;
+
+export default Header;
diff --git a/src/components/Header/HeaderTitle.jsx b/src/components/Header/HeaderTitle.jsx
new file mode 100644
index 0000000..468440f
--- /dev/null
+++ b/src/components/Header/HeaderTitle.jsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import styled from "styled-components";
+import realpiano_logo from "../../assets/realpiano_logo_white.jpg";
+
+const HeaderTitle = () => {
+ return (
+
+
+
+ REALPIANO
+
+
+ );
+};
+
+const HeaderTitleBlock = styled.div`
+ height: 100%;
+ display: flex;
+ align-items: center;
+ color: inherit;
+ cursor: pointer;
+
+ .logo {
+ max-width: 2.5rem;
+ max-height: 2.5rem;
+ width: auto;
+ height: auto;
+ }
+
+ .title {
+ font-size: 1.8rem;
+ font-weight: 500;
+ margin-left: 5px;
+ margin-top: 10px;
+ }
+`;
+
+export default HeaderTitle;
diff --git a/src/components/Header/Navigation.jsx b/src/components/Header/Navigation.jsx
new file mode 100644
index 0000000..a53177a
--- /dev/null
+++ b/src/components/Header/Navigation.jsx
@@ -0,0 +1,47 @@
+import React from "react";
+import styled from "styled-components";
+import { Link } from "react-router-dom";
+
+const Navigation = () => {
+ return (
+
+ 신청곡
+ 신청곡 순위
+ 신청곡 유튜브 영상
+ 리얼피아노 악보집
+
+ );
+};
+
+const NavigationBlock = styled.div`
+ height: 100%;
+ display: flex;
+ align-items: center;
+
+ & > * {
+ /* padding: 25px 5px 10px 5px; */
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ margin: 0px 30px;
+ padding-top: 10px;
+ font-size: 1.2rem;
+ font-weight: 600;
+ text-decoration: none;
+ align-self: center;
+ color: inherit;
+ opacity: 0.5;
+ height: 100%;
+ /* border-bottom: 1px solid rgba(0, 0, 0, 0); */
+ border-bottom: 3px solid rgba(250, 162, 193, 0);
+ transition: 0.3s;
+
+ :hover {
+ opacity: 1;
+ /* color: #faa2c1; */
+ border-bottom: 3px solid rgba(250, 162, 193, 1);
+ }
+ }
+`;
+
+export default Navigation;
diff --git a/src/components/Header/UserProfile.jsx b/src/components/Header/UserProfile.jsx
new file mode 100644
index 0000000..950b173
--- /dev/null
+++ b/src/components/Header/UserProfile.jsx
@@ -0,0 +1,45 @@
+import React from "react";
+import styled from "styled-components";
+import { MdKeyboardArrowDown } from "react-icons/md";
+
+const avatarUrl =
+ "https://avatars.githubusercontent.com/u/49878687?s=460&u=e739e45e9f39b5200339cca6dc293f934fa03bc0&v=4";
+
+const UserProfile = () => {
+ return (
+
+
+ Museop Kim
+
+
+ );
+};
+
+const UserProfileBlock = styled.div`
+ display: flex;
+ align-items: center;
+ margin-top: 3px;
+ cursor: pointer;
+
+ .username {
+ padding: 0px 9px;
+ margin-top: 3px;
+ opacity: 0.8;
+ font-size: 1.2rem;
+ font-weight: 500;
+ }
+
+ .user-avatar {
+ max-width: 32px;
+ max-height: 32px;
+ border-radius: 50%;
+ margin-right: 1px;
+ }
+
+ .reversed-triangle {
+ font-size: 1.6rem;
+ opacity: 0.8;
+ }
+`;
+
+export default UserProfile;
diff --git a/src/components/Letter/Letter.jsx b/src/components/Letter/Letter.jsx
new file mode 100644
index 0000000..bbed95b
--- /dev/null
+++ b/src/components/Letter/Letter.jsx
@@ -0,0 +1,147 @@
+import React from "react";
+import styled from "styled-components";
+
+const MAX_LENGTH = 100;
+
+const Letter = ({
+ id,
+ user,
+ song,
+ songStory,
+ createdDateTime,
+ onReadLetter
+}) => {
+ const { username, avatarUrl } = user;
+ const { title, artist, imageUrl } = song;
+
+ return (
+ <>
+ onReadLetter(id)}>
+
+
+
+ {title}
+ {artist}
+
+
+
+ {songStory.length > MAX_LENGTH
+ ? `${songStory.slice(0, MAX_LENGTH)} ...`
+ : songStory}
+
+
+ {createdDateTime}
+
+

+
{username}
+
+
+
+ >
+ );
+};
+
+const LetterBlock = styled.li`
+ width: 25rem;
+ height: 23rem;
+ margin: 1.2rem;
+ margin-bottom: 5rem;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ padding: 0.7rem;
+ border: #ffdeeb;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ transition: 0.3s;
+ box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.2),
+ 0 8px 16px -8px rgba(0, 0, 0, 0.2), 0 -6px 16px -6px rgba(0, 0, 0, 0.025);
+
+ &:hover {
+ box-shadow: 0px 25px 30px 3px rgba(0, 0, 0, 0.3);
+ }
+
+ .album-image {
+ width: 10rem;
+ height: 10rem;
+ }
+`;
+
+const SongBlock = styled.div`
+ display: flex;
+ justify-content: space-between;
+
+ .album-image {
+ position: relative;
+ top: -2.5rem;
+ box-shadow: 3px 2px 5px 1px rgba(0, 0, 0, 0.3);
+ border-radius: 5px;
+ }
+
+ .song-about {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-right: auto;
+ margin-left: 2rem;
+
+ .song-about__title {
+ overflow-x: hidden;
+ font-size: 1.4rem;
+ font-weight: 500;
+ color: #2c2c2c;
+ opacity: 0.9;
+ }
+
+ .song-about__artist {
+ margin-top: 0.5rem;
+ font-size: 1.2rem;
+ opacity: 0.8;
+ }
+ }
+`;
+
+const SongStory = styled.p`
+ width: 100%;
+ max-height: 8rem;
+ line-break: anywhere;
+ margin-top: -0.3rem;
+ font-size: 1.2rem;
+ line-height: 1.9rem;
+ opacity: 0.9;
+ padding: 0rem 0.5rem;
+`;
+
+const UserBlock = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: auto 5px 3px 5px;
+
+ .created-time {
+ font-size: 1.1rem;
+ margin-top: 0.8rem;
+ opacity: 0.7;
+ }
+
+ .user-about {
+ display: flex;
+ align-items: center;
+ }
+
+ .user-about__avatar {
+ max-width: 2.5rem;
+ max-height: 2.5rem;
+ border-radius: 50%;
+ box-shadow: 3px 2px 10px 1px rgba(0, 0, 0, 0.3);
+ }
+
+ .user-about__name {
+ font-size: 1.1rem;
+ margin-top: 0.5rem;
+ margin-left: 0.5rem;
+ opacity: 0.8;
+ }
+`;
+
+export default Letter;
diff --git a/src/components/Letter/LetterList.jsx b/src/components/Letter/LetterList.jsx
new file mode 100644
index 0000000..693767b
--- /dev/null
+++ b/src/components/Letter/LetterList.jsx
@@ -0,0 +1,29 @@
+import React from "react";
+import styled from "styled-components";
+import LetterContainer from "../../containers/Letter/LetterContainer";
+
+const LetterList = ({ letters }) => {
+ return (
+
+ {letters.map(letter => (
+
+ ))}
+
+ );
+};
+
+const LetterListBlock = styled.ul`
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: 4.5rem;
+`;
+
+export default LetterList;
diff --git a/src/components/LetterDetails/LetterDetails.jsx b/src/components/LetterDetails/LetterDetails.jsx
new file mode 100644
index 0000000..8ada13e
--- /dev/null
+++ b/src/components/LetterDetails/LetterDetails.jsx
@@ -0,0 +1,30 @@
+import React, { useEffect, useState } from "react";
+import ModalTemplate from "../Template/Modal/ModalTemplate";
+import LetterDetailsSong from "./LetterDetailsSong";
+import LetterDetailsSongStory from "./LetterDetailsSongStory";
+import LetterModalTemplate from "../Template/LetterModal/LetterModalTemplate";
+import LetterDetailsUser from "./LetterDetailsUser";
+import LetterModalDiv from "../LetterModal/LetterModalContents/LetterModalDiv";
+import LetterModalHiddenButtonContainer from "../../containers/LetterModal/LetterModalHiddenButtonContainer";
+import LetterModalButtonContainer from "../../containers/LetterModal/LetterModalButtonContainer";
+import { useSelector } from "react-redux";
+
+const LetterDetails = ({ letter }) => {
+ const { song, songStory, createdDateTime, user } = letter;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LetterDetails;
diff --git a/src/components/LetterDetails/LetterDetailsHiddenMenu.jsx b/src/components/LetterDetails/LetterDetailsHiddenMenu.jsx
new file mode 100644
index 0000000..6099537
--- /dev/null
+++ b/src/components/LetterDetails/LetterDetailsHiddenMenu.jsx
@@ -0,0 +1,45 @@
+import React, { useEffect } from "react";
+import { MdMoreHoriz } from "react-icons/md";
+import styled, { css } from "styled-components";
+import LetterDetailsHiddenMenuButton from "./LetterDetailsHiddenMenuButton";
+
+const LetterDetailsHiddenMenu = ({
+ isMouseEnter,
+ isMenuOpen,
+ onToggle,
+ changeToEdit
+}) => {
+ return (
+
+
+ {isMenuOpen && (
+
+ )}
+
+ );
+};
+
+const ButtonBlock = styled.div`
+ ${props =>
+ props.isMouseEnter
+ ? css`
+ visibility: visible;
+ opacity: 1;
+ `
+ : css`
+ visibility: hidden;
+ opacity: 0;
+ `}
+ transition: 0.7s;
+`;
+
+const HiddenMenu = styled(MdMoreHoriz)`
+ position: absolute;
+ bottom: 93%;
+ right: 3%;
+ cursor: pointer;
+ font-size: 2.1rem;
+ color: gray;
+`;
+
+export default LetterDetailsHiddenMenu;
diff --git a/src/components/LetterDetails/LetterDetailsHiddenMenuButton.jsx b/src/components/LetterDetails/LetterDetailsHiddenMenuButton.jsx
new file mode 100644
index 0000000..82f278c
--- /dev/null
+++ b/src/components/LetterDetails/LetterDetailsHiddenMenuButton.jsx
@@ -0,0 +1,71 @@
+import React from "react";
+import styled from "styled-components";
+import { TiEdit, TiDeleteOutline } from "react-icons/ti";
+
+const LetterDetailsHiddenMenuButton = ({ changeToEdit }) => {
+ return (
+
+ );
+};
+
+const Menu = styled.ul`
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ border-radius: 0.35rem;
+ position: absolute;
+ bottom: 77.7%;
+ right: 4.9%;
+ width: 5.5rem;
+ box-shadow: 7px 3px 30px 1px rgba(3, 3, 3, 0.1);
+ z-index: 9;
+
+ .menu__item {
+ padding: 0.7rem 0.3rem;
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ cursor: pointer;
+ background-color: #fbfbfd;
+
+ &:hover {
+ &,
+ .menu__icon,
+ .menu__name {
+ opacity: 1;
+ background-color: #e64980;
+ color: #fff;
+ font-weight: 700;
+ }
+ }
+ }
+
+ .menu__icon {
+ font-size: 1.8rem;
+ opacity: 0.5;
+ }
+
+ .menu__icon.edit {
+ color: #1098ad;
+ }
+
+ .menu__icon.delete {
+ color: #a61e4d;
+ }
+
+ .menu__name {
+ font-size: 1.25rem;
+ line-height: 1.77rem;
+ margin-left: 0.37rem;
+ opacity: 0.7;
+ }
+`;
+
+export default LetterDetailsHiddenMenuButton;
diff --git a/src/components/LetterDetails/LetterDetailsSong.jsx b/src/components/LetterDetails/LetterDetailsSong.jsx
new file mode 100644
index 0000000..f96dcd1
--- /dev/null
+++ b/src/components/LetterDetails/LetterDetailsSong.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+import LetterModalSong from "../LetterModal/LetterModalSong/LetterModalSong";
+import SongAbout from "../LetterModal/LetterModalSong/SongAbout";
+import SongArticle from "../LetterModal/LetterModalSong/SongArticle";
+import SongArticleItem from "../LetterModal/LetterModalSong/SongArticleItem";
+import SongArticleName from "../LetterModal/LetterModalSong/SongArticleName";
+import SongImage from "../LetterModal/LetterModalSong/SongImage";
+
+const LetterDetailsSong = ({ song }) => {
+ const { title, artist, imageUrl } = song;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LetterDetailsSong;
diff --git a/src/components/LetterDetails/LetterDetailsSongStory.jsx b/src/components/LetterDetails/LetterDetailsSongStory.jsx
new file mode 100644
index 0000000..1bba629
--- /dev/null
+++ b/src/components/LetterDetails/LetterDetailsSongStory.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import LetterModalSongStory from "../LetterModal/LetterModalSongStory/LetterModalSongStory";
+
+const LetterDetailsSongStory = ({ songStory }) => {
+ return ;
+};
+
+export default LetterDetailsSongStory;
diff --git a/src/components/LetterDetails/LetterDetailsUser.jsx b/src/components/LetterDetails/LetterDetailsUser.jsx
new file mode 100644
index 0000000..5c970e2
--- /dev/null
+++ b/src/components/LetterDetails/LetterDetailsUser.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import LetterModalUser from "../LetterModal/LetterModalUser/LetterModalUser";
+
+const LetterDetailsUser = ({ user, createdDateTime }) => {
+ return ;
+};
+
+export default LetterDetailsUser;
diff --git a/src/components/LetterEditor/LetterEditor.jsx b/src/components/LetterEditor/LetterEditor.jsx
new file mode 100644
index 0000000..575c088
--- /dev/null
+++ b/src/components/LetterEditor/LetterEditor.jsx
@@ -0,0 +1,36 @@
+import React, { useEffect } from "react";
+import ModalTemplate from "../Template/Modal/ModalTemplate";
+import LetterModalForm from "../LetterModal/LetterModalContents/LetterModalForm";
+import LetterModalTemplate from "../Template/LetterModal/LetterModalTemplate";
+import LetterEditorSong from "./LetterEditorSong";
+import LetterEditorSongStory from "./LetterEditorSongStory";
+import LetterEditorUser from "./LetterEditorUser";
+import LetterModalHiddenButtonContainer from "../../containers/LetterModal/LetterModalHiddenButtonContainer";
+import LetterModalButtonContainer from "../../containers/LetterModal/LetterModalButtonContainer";
+import LetterEditorSearchButton from "./LetterEditorSearchButton";
+
+const LetterEditor = ({ letterForm, user, onChange }) => {
+ const { title, artist, imageUrl, songStory } = letterForm;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LetterEditor;
diff --git a/src/components/LetterEditor/LetterEditorSearchButton.jsx b/src/components/LetterEditor/LetterEditorSearchButton.jsx
new file mode 100644
index 0000000..50cedfd
--- /dev/null
+++ b/src/components/LetterEditor/LetterEditorSearchButton.jsx
@@ -0,0 +1,48 @@
+import React from "react";
+import styled from "styled-components";
+import { BiSearch } from "react-icons/bi";
+import useModal from "../../hooks/useModal";
+import SongSearchModal from "./SongSearchModal/SongSearchModal";
+
+const LetterEditorSearchButton = () => {
+ const [isOpened, onOpenModal, onCloseModal] = useModal();
+
+ return (
+ <>
+
+
+ 신청곡 검색
+
+
+ >
+ );
+};
+
+const StyledButton = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ width: 11rem;
+ top: 14%;
+ right: 15%;
+
+ padding: 0.35rem 0rem;
+ border-style: none;
+ border-radius: 0.5rem;
+ font-size: 1.2rem;
+ background-color: #f06595;
+ box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.15);
+ color: #fff;
+ font-weight: 500;
+ z-index: 1;
+ cursor: pointer;
+`;
+
+const SearchIcon = styled(BiSearch)`
+ font-size: 1.35rem;
+ margin-right: 0.35rem;
+ opacity: 0.9;
+`;
+
+export default LetterEditorSearchButton;
diff --git a/src/components/LetterEditor/LetterEditorSong.jsx b/src/components/LetterEditor/LetterEditorSong.jsx
new file mode 100644
index 0000000..69637d4
--- /dev/null
+++ b/src/components/LetterEditor/LetterEditorSong.jsx
@@ -0,0 +1,42 @@
+import React from "react";
+import LetterModalSong from "../LetterModal/LetterModalSong/LetterModalSong";
+import SongAbout from "../LetterModal/LetterModalSong/SongAbout";
+import SongArticle from "../LetterModal/LetterModalSong/SongArticle";
+import SongArticleName from "../LetterModal/LetterModalSong/SongArticleName";
+import SongImage from "../LetterModal/LetterModalSong/SongImage";
+import SongInput from "../LetterModal/LetterModalSong/SongInput";
+
+const LetterEditorSong = ({ title, artist, imageUrl, onChange }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LetterEditorSong;
diff --git a/src/components/LetterEditor/LetterEditorSongStory.jsx b/src/components/LetterEditor/LetterEditorSongStory.jsx
new file mode 100644
index 0000000..a2802e6
--- /dev/null
+++ b/src/components/LetterEditor/LetterEditorSongStory.jsx
@@ -0,0 +1,55 @@
+import React from "react";
+import styled from "styled-components";
+import TextareaAutosize from "react-autosize-textarea";
+
+const LetterEditorSongStory = ({ songStory, onChange }) => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+const BottomLine = styled.div`
+ height: 1.5px;
+ background-color: rgba(0, 0, 0, 0.15);
+ margin-top: 0.35rem;
+ margin-right: 0.7rem;
+ margin-left: 0.5rem;
+ transition: 0.3s;
+`;
+
+const Textarea = styled(TextareaAutosize)`
+ margin: 2rem 0.7rem;
+ max-height: 12rem;
+ line-height: 1.7rem;
+ font-size: 1.2rem;
+ font-family: inherit;
+ box-sizing: border-box;
+ min-height: 3.6rem;
+ width: 98%;
+ border: none;
+ outline: none;
+ resize: none;
+ background-color: #fbfbfd;
+ padding: 0rem 0.7rem 0rem 0rem;
+ margin-bottom: 0.1rem;
+ color: inherit;
+
+ &:focus {
+ & + div {
+ background-color: rgba(230, 73, 128, 0.7);
+ }
+ }
+`;
+
+export default LetterEditorSongStory;
diff --git a/src/components/LetterEditor/LetterEditorUser.jsx b/src/components/LetterEditor/LetterEditorUser.jsx
new file mode 100644
index 0000000..eca1acf
--- /dev/null
+++ b/src/components/LetterEditor/LetterEditorUser.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import LetterModalUser from "../LetterModal/LetterModalUser/LetterModalUser";
+
+const LetterEditorUser = ({ user, createdDateTime }) => {
+ return ;
+};
+
+export default LetterEditorUser;
diff --git a/src/components/LetterEditor/SongSearchModal/ArticleNames.jsx b/src/components/LetterEditor/SongSearchModal/ArticleNames.jsx
new file mode 100644
index 0000000..6bbb9ea
--- /dev/null
+++ b/src/components/LetterEditor/SongSearchModal/ArticleNames.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+import styled from "styled-components";
+
+const ArticleNames = () => {
+ return (
+
+ ALBUM
+ TITLE
+ SELECT
+
+ );
+};
+
+const ArticleNamesBlock = styled.div`
+ display: flex;
+ justify-content: space-around;
+`;
+
+const ArticleName = styled.span`
+ box-sizing: border-box;
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: #adb5bd;
+
+ &:nth-child(1) {
+ flex: 6;
+ margin-left: 1rem;
+ }
+ &:nth-child(2) {
+ flex: 8;
+ }
+ &:nth-child(3) {
+ flex: 2;
+ text-align: center;
+ }
+`;
+
+export default ArticleNames;
diff --git a/src/components/LetterEditor/SongSearchModal/SearchBy.jsx b/src/components/LetterEditor/SongSearchModal/SearchBy.jsx
new file mode 100644
index 0000000..db37d1e
--- /dev/null
+++ b/src/components/LetterEditor/SongSearchModal/SearchBy.jsx
@@ -0,0 +1,39 @@
+import React from "react";
+import styled from "styled-components";
+
+const SearchBy = () => {
+ return (
+
+
+ Search By
+ ManiaDB
+
+
+ );
+};
+
+const SearchByBlock = styled.a`
+ position: absolute;
+ bottom: 8%;
+ right: 15%;
+ display: flex;
+ flex-direction: column;
+ opacity: 0.7;
+`;
+
+const APINameWrap = styled.div`
+ margin-left: auto;
+`;
+
+const Prefix = styled.span`
+ font-size: 0.3rem;
+ color: #adb5bd;
+`;
+
+const APIName = styled.span`
+ font-size: 1rem;
+ color: #adb5bd;
+ font-weight: 500;
+`;
+
+export default SearchBy;
diff --git a/src/components/LetterEditor/SongSearchModal/SearchForm.jsx b/src/components/LetterEditor/SongSearchModal/SearchForm.jsx
new file mode 100644
index 0000000..4cd7d0b
--- /dev/null
+++ b/src/components/LetterEditor/SongSearchModal/SearchForm.jsx
@@ -0,0 +1,108 @@
+import React from "react";
+import styled from "styled-components";
+import { BiSearch } from "react-icons/bi";
+import { useDispatch } from "react-redux";
+import { searchSong } from "../../../modules/song";
+
+const SearchForm = ({ onCloseModal }) => {
+ const dispatch = useDispatch();
+
+ const onSearchSong = event => {
+ event.preventDefault();
+ dispatch(searchSong());
+ };
+
+ return (
+
+ );
+};
+
+const Form = styled.form`
+ box-sizing: border-box;
+ width: 95%;
+ display: flex;
+ justify-content: center;
+ margin: 0rem auto 3.5rem auto;
+`;
+
+const SearchLabel = styled.span`
+ display: flex;
+ align-items: center;
+ position: absolute;
+ top: 0rem;
+ left: 0rem;
+ height: 2.37rem;
+ font-size: 1rem;
+ background-color: #727272;
+ padding: 0rem 0.7rem;
+ border-radius: 0.3rem 0rem 0rem 0.3rem;
+ color: white;
+ font-weight: 600;
+ box-shadow: 1px 0px 3px 1px rgba(0, 0, 0, 0.1);
+`;
+
+const SearchInput = styled.input`
+ border: none;
+ outline: none;
+ color: #868e96;
+ width: 100%;
+ margin: 0rem 0.5rem 0rem 5.5rem;
+ font-size: 1.2rem;
+ background-color: #fbfbfd;
+`;
+
+const SearchInputWrap = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ border: 1px solid #e9ecef;
+ border-radius: 1rem 0rem 0rem 1rem;
+ padding: 0.1rem 0rem;
+ background: #fbfbfd;
+ height: 2.1rem;
+ flex: 11;
+
+ &:nth-child(2) {
+ & > ${SearchLabel} {
+ border-radius: 0.2rem;
+ margin-left: -0.25rem;
+ }
+
+ & > ${SearchInput} {
+ margin-left: 3.8rem;
+ }
+ }
+`;
+
+const SearchButton = styled.button`
+ display: flex;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+ border: none;
+ outline: none;
+ cursor: pointer;
+ background-color: #e1999c;
+ border-radius: 0rem 0.5rem 0.5rem 0rem;
+ flex: 1;
+ box-shadow: 1px 0px 3px 1px rgba(0, 0, 0, 0.1);
+`;
+
+const SearchIcon = styled(BiSearch)`
+ color: #fff;
+ font-size: 1.4rem;
+`;
+
+export default SearchForm;
diff --git a/src/components/LetterEditor/SongSearchModal/SongSearchModal.jsx b/src/components/LetterEditor/SongSearchModal/SongSearchModal.jsx
new file mode 100644
index 0000000..c96db57
--- /dev/null
+++ b/src/components/LetterEditor/SongSearchModal/SongSearchModal.jsx
@@ -0,0 +1,46 @@
+import React, { useEffect } from "react";
+import styled from "styled-components";
+import SearchForm from "./SearchForm";
+import ArticleNames from "./ArticleNames";
+import SongSearchResultContainer from "../../../containers/LetterEditor/SongSearchModal/SongSearchResultContainer";
+import SearchBy from "./SearchBy";
+
+const SongSearchModal = ({ isOpened, onCloseModal }) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+const SongSearchModalBlock = styled.div`
+ box-sizing: border-box;
+ display: ${props => (props.isOpened ? "block" : "none")};
+ width: 100%;
+ position: absolute;
+ top: -21%;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 145%;
+ min-height: 147%;
+ max-height: 180%;
+ background-color: #fbfbfd;
+ box-shadow: 1px 1px 15px 1px rgba(253, 253, 253, 0.3);
+ border-radius: 0.3rem;
+ padding: 5rem;
+ z-index: 99;
+`;
+
+const SearchResultWrap = styled.div`
+ margin-left: 0.1rem;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+`;
+
+export default SongSearchModal;
diff --git a/src/components/LetterEditor/SongSearchModal/SongSearchResult.jsx b/src/components/LetterEditor/SongSearchModal/SongSearchResult.jsx
new file mode 100644
index 0000000..c3cf9e4
--- /dev/null
+++ b/src/components/LetterEditor/SongSearchModal/SongSearchResult.jsx
@@ -0,0 +1,26 @@
+import React from "react";
+import styled from "styled-components";
+import SongSearchResultItem from "./SongSearchResultItem";
+
+const SongSearchResult = ({ songs, mapSongToForm, onCloseModal }) => {
+ return (
+
+ {songs.map((song, index) => (
+
+ ))}
+
+ );
+};
+
+const SongSearchResultList = styled.ul`
+ margin-top: 1.5rem;
+ height: 50vh;
+ overflow-y: scroll;
+`;
+
+export default SongSearchResult;
diff --git a/src/components/LetterEditor/SongSearchModal/SongSearchResultItem.jsx b/src/components/LetterEditor/SongSearchModal/SongSearchResultItem.jsx
new file mode 100644
index 0000000..1ce0b06
--- /dev/null
+++ b/src/components/LetterEditor/SongSearchModal/SongSearchResultItem.jsx
@@ -0,0 +1,74 @@
+import React from "react";
+import styled from "styled-components";
+import { HiCheck } from "react-icons/hi";
+
+const SongSearchResultItem = ({ song, mapSongToForm, onCloseModal }) => {
+ const { title, artist, imageUrl } = song;
+ return (
+
+
+
+ {title}
+ {artist}
+
+ {
+ mapSongToForm(song);
+ onCloseModal();
+ }}
+ className="select-button"
+ />
+
+ );
+};
+
+const Song = styled.li`
+ display: flex;
+ margin-bottom: 3rem;
+ margin-left: 0.8rem;
+
+ &:hover {
+ & .select-button {
+ color: #f06595;
+ }
+ }
+
+ &:nth-child(1) {
+ margin-top: 0.3rem;
+ }
+`;
+
+const SongImage = styled.img`
+ width: 6rem;
+ height: 6rem;
+ box-shadow: 3px 2px 5px 1px rgba(0, 0, 0, 0.3);
+`;
+
+const SongDetails = styled.div`
+ display: flex;
+ flex-direction: column;
+ margin-top: 1.1rem;
+ margin-left: 24.5%;
+`;
+
+const SongTitle = styled.span`
+ font-size: 1.4rem;
+ font-weight: 600;
+`;
+
+const SongArtist = styled.span`
+ margin-top: 0.7rem;
+ color: #868e96;
+`;
+
+const SelectButton = styled(HiCheck)`
+ opacity: 0.5;
+ font-size: 2.5rem;
+ margin-top: 0.7rem;
+ margin-left: auto;
+ margin-right: 3.5%;
+ cursor: pointer;
+ transition: 0.25s;
+`;
+
+export default SongSearchResultItem;
diff --git a/src/components/LetterModal/LetterModal.jsx b/src/components/LetterModal/LetterModal.jsx
new file mode 100644
index 0000000..c66a097
--- /dev/null
+++ b/src/components/LetterModal/LetterModal.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { LETTER_MODAL } from "../../constants/types";
+import LetterEditContainer from "../../containers/LetterEditor/LetterEditContainer";
+import LetterDetails from "../LetterDetails/LetterDetails";
+
+const LetterModal = ({ letter, modalType }) => {
+ if (modalType === LETTER_MODAL.READ) {
+ return ;
+ }
+
+ if (modalType === LETTER_MODAL.EDIT) {
+ return ;
+ }
+
+ return null;
+};
+
+export default LetterModal;
diff --git a/src/components/LetterModal/LetterModalButton/LetterModalButton.jsx b/src/components/LetterModal/LetterModalButton/LetterModalButton.jsx
new file mode 100644
index 0000000..62fd585
--- /dev/null
+++ b/src/components/LetterModal/LetterModalButton/LetterModalButton.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+const LetterModalButton = ({ children, type, onClick, isMouseEnter }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const StyledButton = styled.button`
+ ${props =>
+ props.isMouseEnter
+ ? css`
+ visibility: visible;
+ opacity: 1;
+ `
+ : css`
+ visibility: none;
+ opacity: 0;
+ `}
+ position: absolute;
+ width: 100%;
+ height: 3.3rem;
+ bottom: 0;
+ left: 0;
+ color: #fff;
+ font-weight: 600;
+ font-size: 1.1rem;
+ background-color: #f06595;
+ border: none;
+ cursor: pointer;
+ transition: 0.7s;
+ outline: none;
+`;
+
+export default LetterModalButton;
diff --git a/src/components/LetterModal/LetterModalContents/LetterModalDiv.jsx b/src/components/LetterModal/LetterModalContents/LetterModalDiv.jsx
new file mode 100644
index 0000000..bfff53a
--- /dev/null
+++ b/src/components/LetterModal/LetterModalContents/LetterModalDiv.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+import styled from "styled-components";
+
+const LetterModalDiv = ({ children }) => {
+ return {children};
+};
+
+const StyledDiv = styled.div``;
+
+export default LetterModalDiv;
diff --git a/src/components/LetterModal/LetterModalContents/LetterModalForm.jsx b/src/components/LetterModal/LetterModalContents/LetterModalForm.jsx
new file mode 100644
index 0000000..1b9591e
--- /dev/null
+++ b/src/components/LetterModal/LetterModalContents/LetterModalForm.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+import styled from "styled-components";
+
+const LetterModalForm = ({ children }) => {
+ return {children};
+};
+
+const StyledForm = styled.form``;
+
+export default LetterModalForm;
diff --git a/src/components/LetterModal/LetterModalSong/LetterModalSong.jsx b/src/components/LetterModal/LetterModalSong/LetterModalSong.jsx
new file mode 100644
index 0000000..d49578c
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSong/LetterModalSong.jsx
@@ -0,0 +1,14 @@
+import React from "react";
+import styled from "styled-components";
+
+const LetterModalSong = ({ children }) => {
+ return {children};
+};
+
+const LetterModalSongBlock = styled.div`
+ display: flex;
+ padding-bottom: 2rem;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.07);
+`;
+
+export default LetterModalSong;
diff --git a/src/components/LetterModal/LetterModalSong/SongAbout.jsx b/src/components/LetterModal/LetterModalSong/SongAbout.jsx
new file mode 100644
index 0000000..546b108
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSong/SongAbout.jsx
@@ -0,0 +1,16 @@
+import React from "react";
+import styled from "styled-components";
+
+const SongAbout = ({ children }) => {
+ return {children};
+};
+
+const SongAboutBlock = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ margin-left: 2rem;
+ margin-bottom: 0.2rem;
+`;
+
+export default SongAbout;
diff --git a/src/components/LetterModal/LetterModalSong/SongArticle.jsx b/src/components/LetterModal/LetterModalSong/SongArticle.jsx
new file mode 100644
index 0000000..099b551
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSong/SongArticle.jsx
@@ -0,0 +1,14 @@
+import React from "react";
+import styled from "styled-components";
+
+const SongArticle = ({ children }) => {
+ return {children};
+};
+
+const SongArticleBlock = styled.div`
+ display: flex;
+ flex-direction: column;
+ margin-top: 1.9rem;
+`;
+
+export default SongArticle;
diff --git a/src/components/LetterModal/LetterModalSong/SongArticleItem.jsx b/src/components/LetterModal/LetterModalSong/SongArticleItem.jsx
new file mode 100644
index 0000000..b8eb6b5
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSong/SongArticleItem.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+const SongArticleItem = ({ articleName, item }) => {
+ return (
+
+ {item}
+
+ );
+};
+
+const SongArticleItemBlock = styled.span`
+ ${props =>
+ props.articleName === "TITLE"
+ ? css`
+ font-size: 1.2rem;
+ font-weight: 600;
+ `
+ : css`
+ font-size: 1.2rem;
+ opacity: 0.9;
+ `}
+
+ height: 1.445rem;
+ padding: 0.38rem 0.1rem 0.3rem 0.1rem;
+`;
+
+export default SongArticleItem;
diff --git a/src/components/LetterModal/LetterModalSong/SongArticleName.jsx b/src/components/LetterModal/LetterModalSong/SongArticleName.jsx
new file mode 100644
index 0000000..9a4215c
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSong/SongArticleName.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import styled from "styled-components";
+
+const SongArticleName = ({ articleName }) => {
+ return {articleName};
+};
+
+const StyledSongArticleName = styled.div`
+ display: inline-block;
+ opacity: 0.7;
+ font-weight: 800;
+ font-size: 1rem;
+ margin-bottom: 0.07rem;
+ color: #d6336c;
+`;
+
+export default SongArticleName;
diff --git a/src/components/LetterModal/LetterModalSong/SongImage.jsx b/src/components/LetterModal/LetterModalSong/SongImage.jsx
new file mode 100644
index 0000000..539dde1
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSong/SongImage.jsx
@@ -0,0 +1,14 @@
+import React from "react";
+import styled from "styled-components";
+
+const SongImage = ({ imageUrl }) => {
+ return ;
+};
+
+const StyledSongImage = styled.img`
+ width: 14rem;
+ height: 14rem;
+ border-radius: 0.5rem;
+`;
+
+export default SongImage;
diff --git a/src/components/LetterModal/LetterModalSong/SongInput.jsx b/src/components/LetterModal/LetterModalSong/SongInput.jsx
new file mode 100644
index 0000000..d9d522d
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSong/SongInput.jsx
@@ -0,0 +1,43 @@
+import React from "react";
+import styled from "styled-components";
+
+const SongInput = ({ type, maxLength, placeholder, name, onChange, value }) => {
+ return (
+
+ );
+};
+
+const StyledInput = styled.input`
+ border: none;
+ outline: none;
+ font-size: 1.2rem;
+ color: #27273e;
+ opacity: 0.8;
+ padding: 0.25rem 0.09rem 0.32rem 0.1rem;
+ background-color: #fbfbfd;
+ border-bottom: 1.5px solid rgba(0, 0, 0, 0.2);
+ transition: 0.3s;
+
+ &:focus {
+ &::placeholder {
+ opacity: 0;
+ }
+
+ border-bottom: 1.5px solid rgba(230, 73, 128, 0.7);
+ }
+
+ &::placeholder {
+ font-size: 1.2rem;
+ opacity: 0.7;
+ }
+`;
+
+export default SongInput;
diff --git a/src/components/LetterModal/LetterModalSongStory/LetterModalSongStory.jsx b/src/components/LetterModal/LetterModalSongStory/LetterModalSongStory.jsx
new file mode 100644
index 0000000..9fd75e5
--- /dev/null
+++ b/src/components/LetterModal/LetterModalSongStory/LetterModalSongStory.jsx
@@ -0,0 +1,19 @@
+import React from "react";
+import styled from "styled-components";
+
+const LetterModalSongStory = ({ songStory }) => {
+ return {songStory};
+};
+
+const SongStoryBlock = styled.p`
+ margin: 2rem 0.7rem;
+ min-height: 3.6rem;
+ max-height: 12rem;
+ line-height: 1.7rem;
+ font-size: 1.2rem;
+ font-family: inherit;
+ overflow-y: scroll;
+ padding-bottom: 0.9rem;
+`;
+
+export default LetterModalSongStory;
diff --git a/src/components/LetterModal/LetterModalUser/LetterModalUser.jsx b/src/components/LetterModal/LetterModalUser/LetterModalUser.jsx
new file mode 100644
index 0000000..b9b1317
--- /dev/null
+++ b/src/components/LetterModal/LetterModalUser/LetterModalUser.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+import styled from "styled-components";
+import UserAbout from "./UserAbout";
+
+const LetterModalUser = ({ user, createdDateTime }) => {
+ const { username, avatarUrl } = user;
+ return (
+
+ {createdDateTime}
+
+
+ );
+};
+
+const LetterUserModalBlock = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 3.5rem;
+`;
+
+const CreatedDateTime = styled.span`
+ margin-top: 1.2rem;
+ font-size: 0.8rem;
+ opacity: 0.6;
+`;
+
+export default LetterModalUser;
diff --git a/src/components/LetterModal/LetterModalUser/UserAbout.jsx b/src/components/LetterModal/LetterModalUser/UserAbout.jsx
new file mode 100644
index 0000000..30c1ef9
--- /dev/null
+++ b/src/components/LetterModal/LetterModalUser/UserAbout.jsx
@@ -0,0 +1,31 @@
+import React from "react";
+import styled from "styled-components";
+
+const UserAbout = ({ username, avatarUrl }) => {
+ return (
+
+
+ {username}
+
+ );
+};
+
+const UserAboutBlock = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const UserImage = styled.img`
+ width: 4.2rem;
+ height: 4.2rem;
+ border-radius: 50%;
+ box-shadow: 0px 3px 8px 1px rgba(0, 0, 0, 0.5);
+`;
+
+const Username = styled.span`
+ margin-left: 1rem;
+ font-size: 1.2rem;
+ font-weight: 500;
+`;
+
+export default UserAbout;
diff --git a/src/components/Template/LetterModal/LetterModalTemplate.jsx b/src/components/Template/LetterModal/LetterModalTemplate.jsx
new file mode 100644
index 0000000..34f988f
--- /dev/null
+++ b/src/components/Template/LetterModal/LetterModalTemplate.jsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { useDispatch } from "react-redux";
+import styled from "styled-components";
+import { mouseEnter, mouseLeave } from "../../../modules/letterModal";
+
+const LetterModalTemplate = ({ children }) => {
+ const dispatch = useDispatch();
+ const onMouseEnter = () => dispatch(mouseEnter());
+ const onMouseLeave = () => dispatch(mouseLeave());
+
+ return (
+
+ {children}
+
+ );
+};
+
+const LetterModalTemplateBlock = styled.div`
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 38rem;
+ min-height: 35vh;
+ box-shadow: 3px -3px 5px 1px rgba(253, 253, 253, 0.03);
+ background-color: #fbfbfd;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ padding: 4.5rem 4.5rem 6rem 4.5rem;
+`;
+
+export default LetterModalTemplate;
diff --git a/src/components/Template/Main/MainTemplate.jsx b/src/components/Template/Main/MainTemplate.jsx
new file mode 100644
index 0000000..085ddfd
--- /dev/null
+++ b/src/components/Template/Main/MainTemplate.jsx
@@ -0,0 +1,26 @@
+import React from "react";
+import styled from "styled-components";
+
+const MainTemplate = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const MainTemplateBlock = styled.div`
+ width: 100%;
+ height: calc(100vh - 16rem);
+ display: flex;
+ justify-content: center;
+ padding-top: 15rem;
+`;
+
+const MainTemplateInner = styled.div`
+ max-width: 1100px;
+ width: 100%;
+ padding: 0rem 5rem;
+`;
+
+export default MainTemplate;
diff --git a/src/components/Template/Modal/ModalTemplate.jsx b/src/components/Template/Modal/ModalTemplate.jsx
new file mode 100644
index 0000000..fc627c8
--- /dev/null
+++ b/src/components/Template/Modal/ModalTemplate.jsx
@@ -0,0 +1,19 @@
+import React, { useEffect } from "react";
+import { useSelector } from "react-redux";
+import styled from "styled-components";
+
+const ModalTemplate = ({ children }) => {
+ return {children};
+};
+
+const ModalTemplateBlock = styled.div`
+ position: fixed;
+ width: 100vw;
+ height: 100vh;
+ top: 0;
+ left: 0;
+ z-index: 99;
+ background-color: rgba(0, 0, 0, 0.5);
+`;
+
+export default ModalTemplate;
diff --git a/src/constants/types.js b/src/constants/types.js
new file mode 100644
index 0000000..35290ae
--- /dev/null
+++ b/src/constants/types.js
@@ -0,0 +1,5 @@
+export const LETTER_MODAL = {
+ READ: "LETTER_MODAL_READ",
+ WRITE: "LETTER_MODAL_WRITE",
+ EDIT: "LETTER_MODAL_EDIT"
+};
diff --git a/src/containers/Letter/LetterContainer.jsx b/src/containers/Letter/LetterContainer.jsx
new file mode 100644
index 0000000..6b8e950
--- /dev/null
+++ b/src/containers/Letter/LetterContainer.jsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { useDispatch } from "react-redux";
+import Letter from "../../components/Letter/Letter";
+import {
+ changeModalType,
+ loadLetter,
+ openModal
+} from "../../modules/letterModal";
+import { LETTER_MODAL } from "../../constants/types";
+
+const LetterContainer = ({ id, user, song, songStory, createdDateTime }) => {
+ const dispatch = useDispatch();
+ const onReadLetter = letterId => {
+ dispatch(openModal());
+ dispatch(changeModalType(LETTER_MODAL.READ));
+ dispatch(loadLetter(letterId));
+ };
+
+ return (
+
+ );
+};
+
+export default LetterContainer;
diff --git a/src/containers/Letter/LetterListContainer.jsx b/src/containers/Letter/LetterListContainer.jsx
new file mode 100644
index 0000000..b10f09e
--- /dev/null
+++ b/src/containers/Letter/LetterListContainer.jsx
@@ -0,0 +1,29 @@
+import React, { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { getLetters } from "../../modules/letters";
+import LetterList from "../../components/Letter/LetterList";
+
+const LetterListContainer = () => {
+ const { data: letters, loading, error } = useSelector(state => state.letters);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(getLetters());
+ }, [dispatch]);
+
+ if (loading) {
+ return Now Loading...
;
+ }
+
+ if (error) {
+ return ERROR!
;
+ }
+
+ if (!letters) {
+ return 아직 등록된 신청곡이 없습니다. 신청곡을 등록 해주세요.
;
+ }
+
+ return ;
+};
+
+export default LetterListContainer;
diff --git a/src/containers/LetterEditor/LetterEditContainer.jsx b/src/containers/LetterEditor/LetterEditContainer.jsx
new file mode 100644
index 0000000..96fdf1e
--- /dev/null
+++ b/src/containers/LetterEditor/LetterEditContainer.jsx
@@ -0,0 +1,25 @@
+import React, { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import LetterEditor from "../../components/LetterEditor/LetterEditor";
+import { initializeForm, updateForm } from "../../modules/letterForm";
+
+const LetterEditContainer = ({ letter }) => {
+ const { letterForm } = useSelector(state => state);
+ const dispatch = useDispatch();
+ const user = letter.user;
+
+ const onChange = event => {
+ const { name, value } = event.target;
+ dispatch(updateForm(name, value));
+ };
+
+ useEffect(() => {
+ dispatch(initializeForm(letter));
+ }, [dispatch, letter]);
+
+ return (
+
+ );
+};
+
+export default LetterEditContainer;
diff --git a/src/containers/LetterEditor/SongSearchModal/SongSearchResultContainer.jsx b/src/containers/LetterEditor/SongSearchModal/SongSearchResultContainer.jsx
new file mode 100644
index 0000000..21cf75b
--- /dev/null
+++ b/src/containers/LetterEditor/SongSearchModal/SongSearchResultContainer.jsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { useDispatch, useSelector } from "react-redux";
+import SongSearchResult from "../../../components/LetterEditor/SongSearchModal/SongSearchResult";
+import { updateForm } from "../../../modules/letterForm";
+
+const SongSearchResultContainer = ({ onCloseModal }) => {
+ const { data: songs, loading, error } = useSelector(state => state.song);
+ const dispatch = useDispatch();
+ const mapSongToForm = song => {
+ for (const property in song) {
+ dispatch(updateForm(property, song[property]));
+ }
+ };
+
+ if (error) {
+ return ERROR!
;
+ }
+
+ if (!songs) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+export default SongSearchResultContainer;
diff --git a/src/containers/LetterModal/LetterModalButtonContainer.jsx b/src/containers/LetterModal/LetterModalButtonContainer.jsx
new file mode 100644
index 0000000..0f769f4
--- /dev/null
+++ b/src/containers/LetterModal/LetterModalButtonContainer.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import { useDispatch, useSelector } from "react-redux";
+import LetterModalButton from "../../components/LetterModal/LetterModalButton/LetterModalButton";
+import { LETTER_MODAL } from "../../constants/types";
+import { changeModalType, closeModal } from "../../modules/letterModal";
+import { clearForm } from "../../modules/letterForm";
+import { updateLetter } from "../../modules/letter";
+import { getLetters } from "../../modules/letters";
+
+const LetterModalButtonContainer = ({ user }) => {
+ const { modalType, letterId, isMouseEnter } = useSelector(
+ state => state.letterModal
+ );
+ const { letterForm } = useSelector(state => state);
+ const dispatch = useDispatch();
+
+ const close = () => {
+ dispatch(closeModal());
+ };
+
+ const update = event => {
+ event.preventDefault();
+ dispatch(
+ updateLetter(letterId, {
+ letterId,
+ user,
+ ...letterForm
+ })
+ );
+ dispatch(clearForm());
+ dispatch(changeModalType(LETTER_MODAL.READ));
+ dispatch(getLetters());
+ };
+
+ if (modalType === LETTER_MODAL.READ) {
+ return (
+
+ CLOSE
+
+ );
+ }
+
+ if (modalType === LETTER_MODAL.EDIT) {
+ return (
+
+ EDIT
+
+ );
+ }
+ return null;
+};
+
+export default LetterModalButtonContainer;
diff --git a/src/containers/LetterModal/LetterModalContainer.jsx b/src/containers/LetterModal/LetterModalContainer.jsx
new file mode 100644
index 0000000..0f797cc
--- /dev/null
+++ b/src/containers/LetterModal/LetterModalContainer.jsx
@@ -0,0 +1,42 @@
+import React, { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import LetterModal from "../../components/LetterModal/LetterModal";
+import { getLetterById } from "../../modules/letter";
+
+const LetterModalContainer = () => {
+ const { modalType, letterId } = useSelector(state => state.letterModal);
+ const { data: letter, loading, error } = useSelector(state => state.letter);
+ const dispatch = useDispatch();
+
+ const openModal = () => {
+ document.body.style.overflow = "hidden";
+ document.body.scroll = "no";
+ };
+
+ const closeModal = () => {
+ document.body.style.overflow = "scroll";
+ document.body.scroll = "yes";
+ };
+
+ useEffect(() => {
+ dispatch(getLetterById(letterId));
+
+ return () => {
+ closeModal();
+ };
+ }, [letterId, dispatch]);
+
+ if (error) {
+ return ERROR!
;
+ }
+
+ if (!letter) {
+ return null;
+ }
+
+ openModal();
+
+ return ;
+};
+
+export default LetterModalContainer;
diff --git a/src/containers/LetterModal/LetterModalHiddenButtonContainer.jsx b/src/containers/LetterModal/LetterModalHiddenButtonContainer.jsx
new file mode 100644
index 0000000..08fd8e4
--- /dev/null
+++ b/src/containers/LetterModal/LetterModalHiddenButtonContainer.jsx
@@ -0,0 +1,30 @@
+import React, { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import LetterDetailsHiddenMenu from "../../components/LetterDetails/LetterDetailsHiddenMenu";
+import { LETTER_MODAL } from "../../constants/types";
+import { toggleMenu, changeModalType } from "../../modules/letterModal";
+
+const LetterModalHiddenButtonContainer = () => {
+ const { modalType, isMouseEnter, isMenuOpen } = useSelector(
+ state => state.letterModal
+ );
+
+ const dispatch = useDispatch();
+ const onToggle = () => dispatch(toggleMenu());
+ const changeToEdit = () => {
+ dispatch(changeModalType(LETTER_MODAL.EDIT));
+ };
+
+ if (modalType === LETTER_MODAL.READ || modalType === LETTER_MODAL.EDIT) {
+ return (
+
+ );
+ }
+};
+
+export default LetterModalHiddenButtonContainer;
diff --git a/src/hooks/useForm.js b/src/hooks/useForm.js
new file mode 100644
index 0000000..569ec01
--- /dev/null
+++ b/src/hooks/useForm.js
@@ -0,0 +1,20 @@
+import { useState } from "react";
+
+const useForm = (initialForm = {}) => {
+ const [form, setForm] = useState(initialForm);
+
+ const onChange = event => {
+ setForm({
+ ...form,
+ [event.target.name]: event.target.value
+ });
+ };
+
+ const onClear = () => {
+ setForm(initialForm);
+ };
+
+ return [form, onChange, onClear];
+};
+
+export default useForm;
diff --git a/src/hooks/useModal.js b/src/hooks/useModal.js
new file mode 100644
index 0000000..85b23d0
--- /dev/null
+++ b/src/hooks/useModal.js
@@ -0,0 +1,12 @@
+import { useState } from "react";
+
+const useModal = () => {
+ const [isOpened, setIsOpened] = useState(false);
+
+ const onOpenModal = () => setIsOpened(true);
+ const onCloseModal = () => setIsOpened(false);
+
+ return [isOpened, onOpenModal, onCloseModal];
+};
+
+export default useModal;
diff --git a/src/hooks/useMouseOver.js b/src/hooks/useMouseOver.js
new file mode 100644
index 0000000..2d76b2c
--- /dev/null
+++ b/src/hooks/useMouseOver.js
@@ -0,0 +1,17 @@
+import { useState } from "react";
+
+const useMouseEnter = () => {
+ const [mouseEnter, setMouseEnter] = useState(false);
+
+ const onMouseEnter = () => {
+ setMouseEnter(true);
+ };
+
+ const onMouseLeave = () => {
+ setMouseEnter(false);
+ };
+
+ return [mouseEnter, onMouseEnter, onMouseLeave];
+};
+
+export default useMouseEnter;
diff --git a/src/hooks/useToggle.js b/src/hooks/useToggle.js
new file mode 100644
index 0000000..c3c33c1
--- /dev/null
+++ b/src/hooks/useToggle.js
@@ -0,0 +1,24 @@
+import { useState } from "react";
+
+const useToggle = () => {
+ const [openMenu, setOpenMenu] = useState(false);
+ const [isForm, setIsForm] = useState(false);
+
+ const changeToForm = () => {
+ setIsForm(true);
+ setOpenMenu(false);
+ };
+
+ const changeToRead = () => {
+ setIsForm(false);
+ setOpenMenu(false);
+ };
+
+ const toggleMenu = () => {
+ setOpenMenu(!openMenu);
+ };
+
+ return { isForm, changeToForm, changeToRead, openMenu, toggleMenu };
+};
+
+export default useToggle;
diff --git a/src/index.js b/src/index.js
index b1ef1c0..1ff5f8f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,10 +1,18 @@
import React from "react";
import ReactDOM from "react-dom";
+import { Provider } from "react-redux";
+import { applyMiddleware, createStore } from "redux";
+import ReduxThunk from "redux-thunk";
import App from "./App";
+import rootReducer from "./modules";
+
+const store = createStore(rootReducer, applyMiddleware(ReduxThunk));
ReactDOM.render(
-
+
+
+
,
document.getElementById("root")
);
diff --git a/src/mockData/index.js b/src/mockData/index.js
new file mode 100644
index 0000000..2ff0e14
--- /dev/null
+++ b/src/mockData/index.js
@@ -0,0 +1,91 @@
+const userData = {
+ id: 1,
+ username: "Museop Kim",
+ avatarUrl:
+ "https://avatars.githubusercontent.com/u/49878687?s=460&u=e739e45e9f39b5200339cca6dc293f934fa03bc0&v=4"
+};
+
+const firstSong = {
+ id: 1,
+ title: "밤편지",
+ artist: "아이유",
+ imageUrl: "http://i.maniadb.com/images/album/742/742576_1_f.jpg"
+};
+
+const secondSong = {
+ id: 2,
+ title: "감사",
+ artist: "김동률",
+ imageUrl: "http://i.maniadb.com/images/album/148/148138_1_f.jpg"
+};
+
+const thirdSong = {
+ id: 3,
+ title: "나는 그 사람이 아프다 featuring 타루",
+ artist: "에피톤 프로젝트",
+ imageUrl: "http://i.maniadb.com/images/album/207/207778_1_f.jpg"
+};
+
+const fourthSong = {
+ id: 4,
+ title: "그랬나봐",
+ artist: "김형중",
+ imageUrl: "http://i.maniadb.com/images/album/107/107223_cda_f.jpg"
+};
+
+const fifthSong = {
+ id: 5,
+ title: "내 사람",
+ artist: "김동률",
+ imageUrl: "http://i.maniadb.com/images/album/723/723527_1_f.jpg"
+};
+
+const mockLetters = [
+ {
+ id: 1,
+ song: firstSong,
+ songStory:
+ "사연입니다. 사연 내용은 다음과 같습니다. 사연은 100자까지만 보여 드릴 예정 입니다.",
+ requestStatus: "WAITING",
+ createdDateTime: "2021-01-27 23:41",
+ user: userData
+ },
+ {
+ id: 2,
+ song: secondSong,
+ songStory:
+ "사연입니다. 사연 내용은 다음과 같습니다. 사연은 100자까지만 보여 드릴 예정 입니다.",
+ requestStatus: "WAITING",
+ createdDateTime: "2021-01-27 23:41",
+ user: userData
+ },
+ {
+ id: 3,
+ song: thirdSong,
+ songStory:
+ "사연입니다. 사연 내용은 다음과 같습니다. 사연은 100자까지만 보여 드릴 예정 입니다.",
+ requestStatus: "WAITING",
+ createdDateTime: "2021-01-27 23:41",
+ user: userData
+ },
+ {
+ id: 4,
+ song: fourthSong,
+ songStory:
+ "사연입니다. 사연 내용은 다음과 같습니다. 사연은 100자까지만 보여 드릴 예정 입니다.",
+ requestStatus: "WAITING",
+ createdDateTime: "2021-01-27 23:41",
+ user: userData
+ },
+ {
+ id: 5,
+ song: fifthSong,
+ songStory:
+ "사연입니다. 사연 내용은 다음과 같습니다. 사연은 100자까지만 보여 드릴 예정 입니다.",
+ requestStatus: "WAITING",
+ createdDateTime: "2021-01-27 23:41",
+ user: userData
+ }
+];
+
+export default mockLetters;
diff --git a/src/mockData/song.js b/src/mockData/song.js
new file mode 100644
index 0000000..81a6b15
--- /dev/null
+++ b/src/mockData/song.js
@@ -0,0 +1,54 @@
+const song = [
+ {
+ title: "감사",
+ artist: "김동률",
+ imageUrl: "http://i.maniadb.com/images/album/148/148138_1_f.jpg"
+ },
+ {
+ title: "끝나지않은노래",
+ artist: "노 리플라이",
+ imageUrl: "http://i.maniadb.com/images/album/407/407690_1_f.jpg"
+ },
+ {
+ title: "영원히",
+ artist: "성시경",
+ imageUrl: "http://i.maniadb.com/images/album/803/803789_1_f.jpg"
+ },
+ {
+ title: "내가 되었으면",
+ artist: "노 리플라이",
+ imageUrl: "http://i.maniadb.com/images/album/639/639756_1_f.jpg"
+ },
+ {
+ title: "밤편지",
+ artist: "아이유",
+ imageUrl: "http://i.maniadb.com/images/album/742/742576_1_f.jpg"
+ },
+ {
+ title: "수고했어, 오늘도",
+ artist: "옥상달빛",
+ imageUrl: "http://i.maniadb.com/images/album/661/661987_1_f.jpg"
+ },
+ {
+ title: "그대 내게 기대",
+ artist: "에피톤 프로젝트",
+ imageUrl: "http://i.maniadb.com/images/album/728/728107_1_f.jpg"
+ },
+ {
+ title: "두 사람",
+ artist: "성시경",
+ imageUrl: "http://i.maniadb.com/images/album/137/137851_1_f.jpg"
+ },
+ {
+ title: "민물장어의 꿈",
+ artist: "신해철",
+ imageUrl: "http://i.maniadb.com/images/album/105/105444_cda_f.jpg"
+ },
+ {
+ title: "사랑하기 때문에",
+ artist: "양희은",
+ imageUrl: "http://i.maniadb.com/images/album/132/132298_1_f.jpg"
+ }
+];
+
+export default song;
diff --git a/src/modules/index.js b/src/modules/index.js
new file mode 100644
index 0000000..2af54a1
--- /dev/null
+++ b/src/modules/index.js
@@ -0,0 +1,16 @@
+import { combineReducers } from "redux";
+import letters from "./letters";
+import letter from "./letter";
+import letterModal from "./letterModal";
+import song from "./song";
+import letterForm from "./letterForm";
+
+const rootReducer = combineReducers({
+ letters,
+ letter,
+ letterModal,
+ song,
+ letterForm
+});
+
+export default rootReducer;
diff --git a/src/modules/letter.js b/src/modules/letter.js
new file mode 100644
index 0000000..1f650f0
--- /dev/null
+++ b/src/modules/letter.js
@@ -0,0 +1,83 @@
+import * as lettersAPI from "../api/letters";
+
+const GET_LETTER = "letter/GET_LETTER";
+const GET_LETTER_SUCCESS = "letter/GET_LETTER_SUCCESS";
+const GET_LETTER_ERROR = "letter/GET_LETTER_ERROR";
+
+const UPDATE_LETTER = "letter/UPDATE_LETTER";
+const UPDATE_LETTER_SUCCESS = "letter/UPDATE_LETTER_SUCCESS";
+const UPDATE_LETTER_ERROR = "letter/UPDATE_LETTER_ERROR";
+
+export const getLetterById = id => async dispatch => {
+ dispatch({ type: GET_LETTER });
+
+ try {
+ const letter = await lettersAPI.getLetterById(id);
+ dispatch({ type: GET_LETTER_SUCCESS, letter });
+ } catch (error) {
+ dispatch({ type: GET_LETTER_ERROR, error });
+ }
+};
+
+export const updateLetter = (id, payload) => async dispatch => {
+ dispatch({ type: UPDATE_LETTER });
+
+ try {
+ const letter = await lettersAPI.updateLetter(id, payload);
+ dispatch({ type: UPDATE_LETTER_SUCCESS, letter });
+ } catch (error) {
+ dispatch({ type: UPDATE_LETTER_ERROR, error });
+ }
+};
+
+const initialState = {
+ data: null,
+ loading: false,
+ error: null
+};
+
+function letter(state = initialState, action) {
+ switch (action.type) {
+ case GET_LETTER:
+ return {
+ data: null,
+ loading: true,
+ error: null
+ };
+ case GET_LETTER_SUCCESS:
+ return {
+ data: action.letter,
+ loading: false,
+ error: null
+ };
+ case GET_LETTER_ERROR:
+ return {
+ data: null,
+ loading: false,
+ error: action.error
+ };
+ case UPDATE_LETTER:
+ return {
+ data: null,
+ loading: true,
+ error: null
+ };
+ case UPDATE_LETTER_SUCCESS:
+ return {
+ data: action.letter,
+ loading: false,
+ error: null
+ };
+ case UPDATE_LETTER_ERROR:
+ return {
+ data: null,
+ loading: false,
+ error: action.error
+ };
+
+ default:
+ return state;
+ }
+}
+
+export default letter;
diff --git a/src/modules/letterForm.js b/src/modules/letterForm.js
new file mode 100644
index 0000000..ae79d7d
--- /dev/null
+++ b/src/modules/letterForm.js
@@ -0,0 +1,41 @@
+const INITIALIZE_FORM = "letterForm/INITIALIZE_FORM";
+const UPDATE_FORM = "letterForm/UPDATE_FORM";
+const CLEAR_FORM = "letterForm/CLEAR_FORM";
+
+export const initializeForm = letter => ({ type: INITIALIZE_FORM, letter });
+export const updateForm = (name, value) => ({ type: UPDATE_FORM, name, value });
+export const clearForm = () => ({ type: CLEAR_FORM });
+
+const initialState = {
+ title: "",
+ artist: "",
+ imageUrl: "",
+ songStory: ""
+};
+
+function letterForm(state = initialState, action) {
+ switch (action.type) {
+ case INITIALIZE_FORM:
+ const { title, artist, imageUrl } = action.letter.song;
+ const songStory = action.letter.songStory;
+ return {
+ title,
+ artist,
+ imageUrl,
+ songStory
+ };
+ case UPDATE_FORM:
+ return {
+ ...state,
+ [action.name]: action.value
+ };
+ case CLEAR_FORM:
+ return {
+ ...initialState
+ };
+ default:
+ return state;
+ }
+}
+
+export default letterForm;
diff --git a/src/modules/letterModal.js b/src/modules/letterModal.js
new file mode 100644
index 0000000..5452519
--- /dev/null
+++ b/src/modules/letterModal.js
@@ -0,0 +1,69 @@
+const OPEN_MODAL = "letterModal/OPEN_MODAL";
+const CLOSE_MODAL = "letterModal/CLOSE_MODAL";
+const LOAD_LETTER = "letterModal/LOAD_LETTER";
+const CHANGE_MODAL_TYPE = "letterModal/CHANGE_MODAL_TYPE";
+const MOUSE_ENTER = "letterModal/MOUSE_ENTER";
+const MOUSE_LEAVE = "letterModal/MOUSE_LEAVE";
+const TOGGLE_MENU = "letterModal/TOGGLE_MENU";
+
+export const openModal = () => ({ type: OPEN_MODAL });
+export const closeModal = () => ({ type: CLOSE_MODAL });
+export const loadLetter = letterId => ({ type: LOAD_LETTER, letterId });
+export const changeModalType = modalType => ({
+ type: CHANGE_MODAL_TYPE,
+ modalType
+});
+export const mouseEnter = () => ({ type: MOUSE_ENTER });
+export const mouseLeave = () => ({ type: MOUSE_LEAVE });
+export const toggleMenu = () => ({ type: TOGGLE_MENU });
+
+const initialState = {
+ isOpened: false,
+ modalType: null,
+ letterId: null,
+ isMouseEnter: false,
+ isMenuOpen: false
+};
+
+function letterModal(state = initialState, action) {
+ switch (action.type) {
+ case OPEN_MODAL:
+ return {
+ ...state,
+ isOpened: true
+ };
+ case CLOSE_MODAL:
+ return {
+ ...initialState
+ };
+ case CHANGE_MODAL_TYPE:
+ return {
+ ...state,
+ modalType: action.modalType
+ };
+ case LOAD_LETTER:
+ return {
+ ...state,
+ letterId: action.letterId
+ };
+ case MOUSE_ENTER:
+ return {
+ ...state,
+ isMouseEnter: true
+ };
+ case MOUSE_LEAVE:
+ return {
+ ...state,
+ isMouseEnter: false
+ };
+ case TOGGLE_MENU:
+ return {
+ ...state,
+ isMenuOpen: !state.isMenuOpen
+ };
+ default:
+ return state;
+ }
+}
+
+export default letterModal;
diff --git a/src/modules/letters.js b/src/modules/letters.js
new file mode 100644
index 0000000..e0d1480
--- /dev/null
+++ b/src/modules/letters.js
@@ -0,0 +1,49 @@
+import * as lettersAPI from "../api/letters";
+
+const GET_LETTERS = "letters/GET_LETTERS";
+const GET_LETTERS_SUCCESS = "letters/GET_SUCCESS";
+const GET_LETTERS_ERROR = "letters/GET_LETTERS_ERROR";
+
+export const getLetters = () => async dispatch => {
+ dispatch({ type: GET_LETTERS });
+
+ try {
+ const letters = await lettersAPI.getLetters();
+ dispatch({ type: GET_LETTERS_SUCCESS, letters });
+ } catch (error) {
+ dispatch({ type: GET_LETTERS_ERROR, error });
+ }
+};
+
+const initialState = {
+ data: null,
+ loading: false,
+ error: null
+};
+
+function letters(state = initialState, action) {
+ switch (action.type) {
+ case GET_LETTERS:
+ return {
+ data: null,
+ loading: true,
+ error: null
+ };
+ case GET_LETTERS_SUCCESS:
+ return {
+ data: action.letters,
+ loading: false,
+ error: null
+ };
+ case GET_LETTERS_ERROR:
+ return {
+ letters: null,
+ loading: false,
+ error: action.error
+ };
+ default:
+ return state;
+ }
+}
+
+export default letters;
diff --git a/src/modules/song.js b/src/modules/song.js
new file mode 100644
index 0000000..e1ded8c
--- /dev/null
+++ b/src/modules/song.js
@@ -0,0 +1,49 @@
+import * as songAPI from "../api/song";
+
+const SEARCH_SONG = "song/SEARCH_SONG";
+const SEARCH_SONG_SUCCESS = "song/SEARCH_SONG_SUCCESS";
+const SEARCH_SONG_ERROR = "song/SEARCH_SONG_ERROR";
+
+export const searchSong = (artist, title) => async dispatch => {
+ dispatch({ type: SEARCH_SONG });
+
+ try {
+ const song = await songAPI.searchSong(artist, title);
+ dispatch({ type: SEARCH_SONG_SUCCESS, song });
+ } catch (error) {
+ dispatch({ type: SEARCH_SONG_ERROR });
+ }
+};
+
+const initialState = {
+ data: null,
+ loading: false,
+ error: null
+};
+
+function song(state = initialState, action) {
+ switch (action.type) {
+ case SEARCH_SONG:
+ return {
+ data: null,
+ loading: true,
+ error: null
+ };
+ case SEARCH_SONG_SUCCESS:
+ return {
+ data: action.song,
+ loading: false,
+ error: null
+ };
+ case SEARCH_SONG_ERROR:
+ return {
+ data: null,
+ loading: false,
+ error: action.error
+ };
+ default:
+ return state;
+ }
+}
+
+export default song;
diff --git a/src/pages/Letters.jsx b/src/pages/Letters.jsx
new file mode 100644
index 0000000..ec6153b
--- /dev/null
+++ b/src/pages/Letters.jsx
@@ -0,0 +1,29 @@
+import React from "react";
+import styled from "styled-components";
+import MainTemplate from "../components/Template/Main/MainTemplate";
+import LetterListContainer from "../containers/Letter/LetterListContainer";
+import LetterModalContainer from "../containers/LetterModal/LetterModalContainer";
+
+const Letters = () => {
+ return (
+
+
+ 신청 대기중
+
+
+
+
+ );
+};
+
+const LettersBlock = styled.div``;
+
+const ListTitle = styled.h2`
+ display: inline-block;
+ font-size: 1.6rem;
+ font-weight: 500;
+ padding: 0 0 1rem 0;
+ border-bottom: 2px solid #f06595;
+`;
+
+export default Letters;
diff --git a/src/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx
new file mode 100644
index 0000000..c8cb880
--- /dev/null
+++ b/src/styles/GlobalStyle.jsx
@@ -0,0 +1,27 @@
+import { createGlobalStyle } from "styled-components";
+import reset from "styled-reset";
+
+const GlobalStyle = createGlobalStyle`
+ ${reset}
+
+ html {
+ font-size: 62.5%;
+ }
+
+ body {
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500&display=swap');
+ font-family: 'Noto Sans KR', sans-serif;
+ font-size: 1.2rem;
+ color: #495057;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ }
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+`;
+
+export default GlobalStyle;