diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..04958f5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +dist +build +coverage +pnpm-lock.yaml +*.min.js +*.min.css +public diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..64bf3a6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "printWidth": 100, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/package.json b/package.json index 9f30638..1dc54de 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", + "predeploy": "pnpm run build", + "deploy": "gh-pages -d dist" }, "dependencies": { "@chakra-ui/icons": "2.2.4", @@ -39,6 +43,8 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", + "gh-pages": "^6.3.0", + "prettier": "^3.7.4", "tsx": "^4.17.0", "typescript": "^5.4.5", "vite": "npm:rolldown-vite@latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 704a7bc..8bde46a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,12 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.7 version: 0.4.20(eslint@8.57.1) + gh-pages: + specifier: ^6.3.0 + version: 6.3.0 + prettier: + specifier: ^3.7.4 + version: 3.7.4 tsx: specifier: ^4.17.0 version: 4.19.4 @@ -1243,6 +1249,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1316,6 +1325,13 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1399,6 +1415,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + email-addresses@5.0.0: + resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1441,6 +1460,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1529,13 +1552,29 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} + + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1585,6 +1624,10 @@ packages: framesync@6.1.2: resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1618,6 +1661,11 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + gh-pages@6.3.0: + resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==} + engines: {node: '>=10'} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1650,6 +1698,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1784,6 +1835,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1858,6 +1912,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1897,6 +1955,10 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1985,14 +2047,26 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2051,6 +2125,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + postcss@8.5.4: resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} engines: {node: ^10 || ^12 || >=14} @@ -2063,6 +2141,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2233,6 +2316,10 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -2309,6 +2396,10 @@ packages: strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -2375,6 +2466,10 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -2416,6 +2511,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -3609,6 +3708,8 @@ snapshots: assertion-error@2.0.1: {} + async@3.2.6: {} + asynckit@0.4.0: {} axios@1.11.0: @@ -3689,6 +3790,10 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@13.1.0: {} + + commondir@1.0.1: {} + concat-map@0.0.1: {} convert-source-map@1.9.0: {} @@ -3755,6 +3860,8 @@ snapshots: eastasianwidth@0.2.0: {} + email-addresses@5.0.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3836,6 +3943,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@4.0.0: {} eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): @@ -3948,12 +4057,31 @@ snapshots: dependencies: flat-cache: 3.2.0 + filename-reserved-regex@2.0.0: {} + + filenamify@4.3.0: + dependencies: + filename-reserved-regex: 2.0.0 + strip-outer: 1.0.1 + trim-repeated: 1.0.0 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + find-root@1.1.0: {} + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4000,6 +4128,12 @@ snapshots: dependencies: tslib: 2.4.0 + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -4035,6 +4169,16 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + gh-pages@6.3.0: + dependencies: + async: 3.2.6 + commander: 13.1.0 + email-addresses: 5.0.0 + filenamify: 4.3.0 + find-cache-dir: 3.3.2 + fs-extra: 11.3.3 + globby: 11.1.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4078,6 +4222,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} graphql@16.11.0: {} @@ -4187,6 +4333,12 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4243,6 +4395,10 @@ snapshots: lines-and-columns@1.2.4: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4279,6 +4435,10 @@ snapshots: '@babel/types': 7.27.6 source-map-js: 1.2.1 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + make-dir@4.0.0: dependencies: semver: 7.7.2 @@ -4368,14 +4528,24 @@ snapshots: outvariant@1.4.3: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -4418,6 +4588,10 @@ snapshots: picomatch@4.0.3: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + postcss@8.5.4: dependencies: nanoid: 3.3.11 @@ -4432,6 +4606,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.7.4: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -4606,6 +4782,8 @@ snapshots: scheduler@0.26.0: {} + semver@6.3.1: {} + semver@7.7.2: {} shebang-command@2.0.0: @@ -4668,6 +4846,10 @@ snapshots: dependencies: js-tokens: 9.0.1 + strip-outer@1.0.1: + dependencies: + escape-string-regexp: 1.0.5 + stylis@4.2.0: {} supports-color@7.2.0: @@ -4719,6 +4901,10 @@ snapshots: dependencies: tldts: 7.0.12 + trim-repeated@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + ts-api-utils@1.4.3(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -4748,6 +4934,8 @@ snapshots: undici-types@6.21.0: {} + universalify@2.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 diff --git a/src/App.tsx b/src/App.tsx index 664bf6d..24a784c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,15 @@ -import { ChakraProvider } from "@chakra-ui/react"; -import { ScheduleProvider } from "./ScheduleContext.tsx"; -import { ScheduleTables } from "./ScheduleTables.tsx"; -import ScheduleDndProvider from "./ScheduleDndProvider.tsx"; +import { ChakraProvider } from "@chakra-ui/react" +import { ScheduleProvider } from "./ScheduleContext.tsx" +import { ScheduleTables } from "./ScheduleTables.tsx" function App() { - return ( - - - + - ); + ) } -export default App; +export default App diff --git a/src/ScheduleTable.tsx b/src/ScheduleTable.tsx index ea17b6a..35a9b13 100644 --- a/src/ScheduleTable.tsx +++ b/src/ScheduleTable.tsx @@ -11,52 +11,106 @@ import { PopoverContent, PopoverTrigger, Text, -} from "@chakra-ui/react"; -import { CellSize, DAY_LABELS, 분 } from "./constants.ts"; -import { Schedule } from "./types.ts"; -import { fill2, parseHnM } from "./utils.ts"; -import { useDndContext, useDraggable } from "@dnd-kit/core"; -import { CSS } from "@dnd-kit/utilities"; -import { ComponentProps, Fragment } from "react"; +} from "@chakra-ui/react" +import { CellSize, DAY_LABELS, 분 } from "./constants.ts" +import { Schedule } from "./types.ts" +import { fill2, parseHnM } from "./utils.ts" +import { useDndContext, useDraggable } from "@dnd-kit/core" +import { CSS } from "@dnd-kit/utilities" +import { ComponentProps, Fragment, memo, useCallback } from "react" interface Props { - tableId: string; - schedules: Schedule[]; - onScheduleTimeClick?: (timeInfo: { day: string, time: number }) => void; - onDeleteButtonClick?: (timeInfo: { day: string, time: number }) => void; + tableId: string + schedules: Schedule[] + onScheduleTimeClick?: (timeInfo: { day: string; time: number }) => void + onDeleteButtonClick?: (timeInfo: { day: string; time: number }) => void } const TIMES = [ ...Array(18) .fill(0) .map((v, k) => v + k * 30 * 분) - .map((v) => `${parseHnM(v)}~${parseHnM(v + 30 * 분)}`), + .map(v => `${parseHnM(v)}~${parseHnM(v + 30 * 분)}`), ...Array(6) .fill(18 * 30 * 분) .map((v, k) => v + k * 55 * 분) - .map((v) => `${parseHnM(v)}~${parseHnM(v + 50 * 분)}`), -] as const; + .map(v => `${parseHnM(v)}~${parseHnM(v + 50 * 분)}`), +] as const -const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButtonClick }: Props) => { +const DayRow = memo(({ day }: { day: string }) => { + return ( + + + {day} + + + ) +}) +const TimeDayRow = memo( + ({ + time, + timeIndex, + onClick, + }: { + time: string + timeIndex: number + onClick: ({ day, time }: { day: string; time: number }) => void + }) => { + return ( + + 17 ? "gray.200" : "gray.100"} + > + + + {fill2(timeIndex + 1)} ({time}) + + + + {DAY_LABELS.map(day => ( + 17 ? "gray.100" : "white"} + cursor="pointer" + _hover={{ bg: "yellow.100" }} + onClick={() => onClick({ day, time: timeIndex + 1 })} + /> + ))} + + ) + } +) +const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButtonClick }: Props) => { const getColor = (lectureId: string): string => { - const lectures = [...new Set(schedules.map(({ lecture }) => lecture.id))]; - const colors = ["#fdd", "#ffd", "#dff", "#ddf", "#fdf", "#dfd"]; - return colors[lectures.indexOf(lectureId) % colors.length]; - }; + const lectures = [...new Set(schedules.map(({ lecture }) => lecture.id))] + const colors = ["#fdd", "#ffd", "#dff", "#ddf", "#fdf", "#dfd"] + return colors[lectures.indexOf(lectureId) % colors.length] + } - const dndContext = useDndContext(); + const dndContext = useDndContext() const getActiveTableId = () => { - const activeId = dndContext.active?.id; + const activeId = dndContext.active?.id if (activeId) { - return String(activeId).split(":")[0]; + return String(activeId).split(":")[0] } - return null; + return null } - const activeTableId = getActiveTableId(); + const activeTableId = getActiveTableId() + + const handleScheduleTimeClick = useCallback( + ({ day, time }: { day: string; time: number }) => { + onScheduleTimeClick?.({ day, time }) + }, + [onScheduleTimeClick] + ) return ( 교시 - {DAY_LABELS.map((day) => ( - - - {day} - - + {DAY_LABELS.map(day => ( + ))} {TIMES.map((time, timeIndex) => ( - - 17 ? 'gray.200' : 'gray.100'} - > - - {fill2(timeIndex + 1)} ({time}) - - - {DAY_LABELS.map((day) => ( - 17 ? 'gray.100' : 'white'} - cursor="pointer" - _hover={{ bg: 'yellow.100' }} - onClick={() => onScheduleTimeClick?.({ day, time: timeIndex + 1 })} - /> - ))} - + ))} @@ -117,39 +151,41 @@ const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButton id={`${tableId}:${index}`} data={schedule} bg={getColor(schedule.lecture.id)} - onDeleteButtonClick={() => onDeleteButtonClick?.({ - day: schedule.day, - time: schedule.range[0], - })} + onDeleteButtonClick={() => + onDeleteButtonClick?.({ + day: schedule.day, + time: schedule.range[0], + }) + } /> ))} - ); -}; + ) +} const DraggableSchedule = ({ - id, - data, - bg, - onDeleteButtonClick + id, + data, + bg, + onDeleteButtonClick, }: { id: string; data: Schedule } & ComponentProps & { - onDeleteButtonClick: () => void -}) => { - const { day, range, room, lecture } = data; - const { attributes, setNodeRef, listeners, transform } = useDraggable({ id }); - const leftIndex = DAY_LABELS.indexOf(day as typeof DAY_LABELS[number]); - const topIndex = range[0] - 1; - const size = range.length; + onDeleteButtonClick: () => void + }) => { + const { day, range, room, lecture } = data + const { attributes, setNodeRef, listeners, transform } = useDraggable({ id }) + const leftIndex = DAY_LABELS.indexOf(day as (typeof DAY_LABELS)[number]) + const topIndex = range[0] - 1 + const size = range.length return ( - {lecture.title} + + {lecture.title} + {room} event.stopPropagation()}> - - + + 강의를 삭제하시겠습니까? - - + + + - setSearchInfo({ tableId, ...timeInfo })} - onDeleteButtonClick={({ day, time }) => setSchedulesMap((prev) => ({ - ...prev, - [tableId]: prev[tableId].filter(schedule => schedule.day !== day || !schedule.range.includes(time)) - }))} - /> + + + ))} - setSearchInfo(null)}/> + {searchInfo && } - ); + ) } diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 593951f..27f3ff1 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -1,25 +1,15 @@ -import { useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Box, Button, - Checkbox, - CheckboxGroup, - FormControl, - FormLabel, HStack, - Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, - Select, - Stack, Table, - Tag, - TagCloseButton, - TagLabel, Tbody, Td, Text, @@ -27,177 +17,208 @@ import { Thead, Tr, VStack, - Wrap, -} from "@chakra-ui/react"; -import { useScheduleContext } from "./ScheduleContext.tsx"; -import { Lecture } from "./types.ts"; -import { parseSchedule } from "./utils.ts"; -import axios from "axios"; -import { DAY_LABELS } from "./constants.ts"; +} from "@chakra-ui/react" +import { useScheduleContext } from "./ScheduleContext.tsx" +import { Lecture } from "./types.ts" +import { parseSchedule } from "./utils.ts" +import axios from "axios" +import { SearchQueryField } from "./form/SearchQueryField.tsx" +import { CreditsField } from "./form/CreditsField.tsx" +import { GradesField } from "./form/GradesField.tsx" +import { DaysField } from "./form/DaysField.tsx" +import { TimesField } from "./form/TimesField.tsx" +import { MajorsField } from "./form/MajorsField.tsx" interface Props { searchInfo: { - tableId: string; - day?: string; - time?: number; - } | null; - onClose: () => void; + tableId: string + day?: string + time?: number + } | null + onClose: () => void } interface SearchOption { - query?: string, - grades: number[], - days: string[], - times: number[], - majors: string[], - credits?: number, + query?: string + grades: number[] + days: string[] + times: number[] + majors: string[] + credits?: number } -const TIME_SLOTS = [ - { id: 1, label: "09:00~09:30" }, - { id: 2, label: "09:30~10:00" }, - { id: 3, label: "10:00~10:30" }, - { id: 4, label: "10:30~11:00" }, - { id: 5, label: "11:00~11:30" }, - { id: 6, label: "11:30~12:00" }, - { id: 7, label: "12:00~12:30" }, - { id: 8, label: "12:30~13:00" }, - { id: 9, label: "13:00~13:30" }, - { id: 10, label: "13:30~14:00" }, - { id: 11, label: "14:00~14:30" }, - { id: 12, label: "14:30~15:00" }, - { id: 13, label: "15:00~15:30" }, - { id: 14, label: "15:30~16:00" }, - { id: 15, label: "16:00~16:30" }, - { id: 16, label: "16:30~17:00" }, - { id: 17, label: "17:00~17:30" }, - { id: 18, label: "17:30~18:00" }, - { id: 19, label: "18:00~18:50" }, - { id: 20, label: "18:55~19:45" }, - { id: 21, label: "19:50~20:40" }, - { id: 22, label: "20:45~21:35" }, - { id: 23, label: "21:40~22:30" }, - { id: 24, label: "22:35~23:25" }, -]; +const PAGE_SIZE = 100 -const PAGE_SIZE = 100; +const fetchMajors = () => axios.get(`${import.meta.env.BASE_URL}/schedules-majors.json`) +const fetchLiberalArts = () => + axios.get(`${import.meta.env.BASE_URL}/schedules-liberal-arts.json`) -const fetchMajors = () => axios.get('/schedules-majors.json'); -const fetchLiberalArts = () => axios.get('/schedules-liberal-arts.json'); +const fetchAllLectures = async () => + await Promise.all([ + (console.log("API Call 1", performance.now()), fetchMajors()), + (console.log("API Call 2", performance.now()), fetchLiberalArts()), + (console.log("API Call 3", performance.now()), fetchMajors()), + (console.log("API Call 4", performance.now()), fetchLiberalArts()), + (console.log("API Call 5", performance.now()), fetchMajors()), + (console.log("API Call 6", performance.now()), fetchLiberalArts()), + ]) -// TODO: 이 코드를 개선해서 API 호출을 최소화 해보세요 + Promise.all이 현재 잘못 사용되고 있습니다. 같이 개선해주세요. -const fetchAllLectures = async () => await Promise.all([ - (console.log('API Call 1', performance.now()), await fetchMajors()), - (console.log('API Call 2', performance.now()), await fetchLiberalArts()), - (console.log('API Call 3', performance.now()), await fetchMajors()), - (console.log('API Call 4', performance.now()), await fetchLiberalArts()), - (console.log('API Call 5', performance.now()), await fetchMajors()), - (console.log('API Call 6', performance.now()), await fetchLiberalArts()), -]); +const LectureTableRow = memo( + ({ lecture, onClick }: { lecture: Lecture; onClick: (lecture: Lecture) => void }) => { + const handleClick = () => onClick(lecture) + return ( + + {lecture.id} + {lecture.grade} + {lecture.title} + {lecture.credits} + + + + + + + ) + } +) -// TODO: 이 컴포넌트에서 불필요한 연산이 발생하지 않도록 다양한 방식으로 시도해주세요. const SearchDialog = ({ searchInfo, onClose }: Props) => { - const { setSchedulesMap } = useScheduleContext(); + const { setSchedulesMap } = useScheduleContext() - const loaderWrapperRef = useRef(null); - const loaderRef = useRef(null); - const [lectures, setLectures] = useState([]); - const [page, setPage] = useState(1); + const loaderWrapperRef = useRef(null) + const loaderRef = useRef(null) + const [lectures, setLectures] = useState([]) + const [page, setPage] = useState(1) const [searchOptions, setSearchOptions] = useState({ - query: '', + query: "", grades: [], days: [], times: [], majors: [], - }); + }) - const getFilteredLectures = () => { - const { query = '', credits, grades, days, times, majors } = searchOptions; + const filteredLectures = useMemo(() => { + const { query = "", credits, grades, days, times, majors } = searchOptions return lectures - .filter(lecture => - lecture.title.toLowerCase().includes(query.toLowerCase()) || - lecture.id.toLowerCase().includes(query.toLowerCase()) + .filter( + lecture => + lecture.title.toLowerCase().includes(query.toLowerCase()) || + lecture.id.toLowerCase().includes(query.toLowerCase()) ) .filter(lecture => grades.length === 0 || grades.includes(lecture.grade)) .filter(lecture => majors.length === 0 || majors.includes(lecture.major)) .filter(lecture => !credits || lecture.credits.startsWith(String(credits))) .filter(lecture => { if (days.length === 0) { - return true; + return true } - const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; - return schedules.some(s => days.includes(s.day)); + const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [] + return schedules.some(s => days.includes(s.day)) }) .filter(lecture => { if (times.length === 0) { - return true; + return true } - const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; - return schedules.some(s => s.range.some(time => times.includes(time))); - }); - } + const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [] + return schedules.some(s => s.range.some(time => times.includes(time))) + }) + }, [lectures, searchOptions]) - const filteredLectures = getFilteredLectures(); - const lastPage = Math.ceil(filteredLectures.length / PAGE_SIZE); - const visibleLectures = filteredLectures.slice(0, page * PAGE_SIZE); - const allMajors = [...new Set(lectures.map(lecture => lecture.major))]; + const lastPage = Math.ceil(filteredLectures.length / PAGE_SIZE) - const changeSearchOption = (field: keyof SearchOption, value: SearchOption[typeof field]) => { - setPage(1); - setSearchOptions(({ ...searchOptions, [field]: value })); - loaderWrapperRef.current?.scrollTo(0, 0); - }; + const visibleLectures = useMemo( + () => filteredLectures.slice(0, page * PAGE_SIZE), + [filteredLectures, page] + ) - const addSchedule = (lecture: Lecture) => { - if (!searchInfo) return; + const allMajors = useMemo(() => [...new Set(lectures.map(lecture => lecture.major))], [lectures]) - const { tableId } = searchInfo; + const changeSearchOption = useCallback( + (field: keyof SearchOption, value: SearchOption[typeof field]) => { + setPage(1) + setSearchOptions(prev => ({ ...prev, [field]: value })) + loaderWrapperRef.current?.scrollTo(0, 0) + }, + [] + ) - const schedules = parseSchedule(lecture.schedule).map(schedule => ({ - ...schedule, - lecture - })); + const handlers = useMemo( + () => ({ + query: (value: string) => changeSearchOption("query", value), + credits: (value: string) => changeSearchOption("credits", value), + grades: (value: number[]) => changeSearchOption("grades", value), + days: (value: string[]) => changeSearchOption("days", value), + times: (value: number[]) => changeSearchOption("times", value), + majors: (value: string[]) => changeSearchOption("majors", value), + removeTime: (time: number) => + changeSearchOption( + "times", + searchOptions.times.filter(v => v !== time) + ), + removeMajor: (major: string) => + changeSearchOption( + "majors", + searchOptions.majors.filter(v => v !== major) + ), + }), + [changeSearchOption, searchOptions.times, searchOptions.majors] + ) - setSchedulesMap(prev => ({ - ...prev, - [tableId]: [...prev[tableId], ...schedules] - })); + const addSchedule = useCallback( + (lecture: Lecture) => { + if (!searchInfo) return + + const { tableId } = searchInfo - onClose(); - }; + const schedules = parseSchedule(lecture.schedule).map(schedule => ({ + ...schedule, + lecture, + })) + + setSchedulesMap(prev => ({ + ...prev, + [tableId]: [...prev[tableId], ...schedules], + })) + + onClose() + }, + [onClose, searchInfo, setSchedulesMap] + ) useEffect(() => { - const start = performance.now(); - console.log('API 호출 시작: ', start) + const start = performance.now() + console.log("API 호출 시작: ", start) fetchAllLectures().then(results => { - const end = performance.now(); - console.log('모든 API 호출 완료 ', end) - console.log('API 호출에 걸린 시간(ms): ', end - start) - setLectures(results.flatMap(result => result.data)); + const end = performance.now() + console.log("모든 API 호출 완료 ", end) + console.log("API 호출에 걸린 시간(ms): ", end - start) + setLectures(results.flatMap(result => result.data)) }) - }, []); + }, []) useEffect(() => { - const $loader = loaderRef.current; - const $loaderWrapper = loaderWrapperRef.current; + const $loader = loaderRef.current + const $loaderWrapper = loaderWrapperRef.current if (!$loader || !$loaderWrapper) { - return; + return } const observer = new IntersectionObserver( entries => { if (entries[0].isIntersecting) { - setPage(prevPage => Math.min(lastPage, prevPage + 1)); + setPage(prevPage => Math.min(lastPage, prevPage + 1)) } }, { threshold: 0, root: $loaderWrapper } - ); + ) - observer.observe($loader); + observer.observe($loader) - return () => observer.unobserve($loader); - }, [lastPage]); + return () => observer.unobserve($loader) + }, [lastPage]) useEffect(() => { setSearchOptions(prev => ({ @@ -205,133 +226,41 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { days: searchInfo?.day ? [searchInfo.day] : [], times: searchInfo?.time ? [searchInfo.time] : [], })) - setPage(1); - }, [searchInfo]); + setPage(1) + }, [searchInfo]) return ( - + 수업 검색 - + - - 검색어 - changeSearchOption('query', e.target.value)} - /> - - - - 학점 - - + + - - 학년 - changeSearchOption('grades', value.map(Number))} - > - - {[1, 2, 3, 4].map(grade => ( - {grade}학년 - ))} - - - - - - 요일 - changeSearchOption('days', value as string[])} - > - - {DAY_LABELS.map(day => ( - {day} - ))} - - - + + - - 시간 - changeSearchOption('times', values.map(Number))} - > - - {searchOptions.times.sort((a, b) => a - b).map(time => ( - - {time}교시 - changeSearchOption('times', searchOptions.times.filter(v => v !== time))}/> - - ))} - - - {TIME_SLOTS.map(({ id, label }) => ( - - - {id}교시({label}) - - - ))} - - - - - - 전공 - changeSearchOption('majors', values as string[])} - > - - {searchOptions.majors.map(major => ( - - {major.split("

").pop()} - changeSearchOption('majors', searchOptions.majors.filter(v => v !== major))}/> - - ))} - - - {allMajors.map(major => ( - - - {major.replace(/

/gi, ' ')} - - - ))} - - - + + - - 검색결과: {filteredLectures.length}개 - + 검색결과: {filteredLectures.length}개 @@ -351,28 +280,22 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => {
{visibleLectures.map((lecture, index) => ( - - - - - - - + ))}
{lecture.id}{lecture.grade}{lecture.title}{lecture.credits} - - - -
- + - ); -}; + ) +} -export default SearchDialog; \ No newline at end of file +export default SearchDialog diff --git a/src/form/CreditsField.tsx b/src/form/CreditsField.tsx new file mode 100644 index 0000000..5772dad --- /dev/null +++ b/src/form/CreditsField.tsx @@ -0,0 +1,19 @@ +import { memo } from "react" +import { FormControl, FormLabel, Select } from "@chakra-ui/react" + +interface Props { + value?: number + onChange: (value: string) => void +} + +export const CreditsField = memo(({ value, onChange }: Props) => ( + + 학점 + + +)) diff --git a/src/form/DaysField.tsx b/src/form/DaysField.tsx new file mode 100644 index 0000000..8830d60 --- /dev/null +++ b/src/form/DaysField.tsx @@ -0,0 +1,23 @@ +import { memo } from "react" +import { Checkbox, CheckboxGroup, FormControl, FormLabel, HStack } from "@chakra-ui/react" +import { DAY_LABELS } from "../constants.ts" + +interface Props { + value: string[] + onChange: (value: string[]) => void +} + +export const DaysField = memo(({ value, onChange }: Props) => ( + + 요일 + onChange(val as string[])}> + + {DAY_LABELS.map(day => ( + + {day} + + ))} + + + +)) diff --git a/src/form/GradesField.tsx b/src/form/GradesField.tsx new file mode 100644 index 0000000..7814654 --- /dev/null +++ b/src/form/GradesField.tsx @@ -0,0 +1,22 @@ +import { memo } from "react" +import { Checkbox, CheckboxGroup, FormControl, FormLabel, HStack } from "@chakra-ui/react" + +interface Props { + value: number[] + onChange: (value: number[]) => void +} + +export const GradesField = memo(({ value, onChange }: Props) => ( + + 학년 + onChange(val.map(Number))}> + + {[1, 2, 3, 4].map(grade => ( + + {grade}학년 + + ))} + + + +)) diff --git a/src/form/MajorsField.tsx b/src/form/MajorsField.tsx new file mode 100644 index 0000000..d99f806 --- /dev/null +++ b/src/form/MajorsField.tsx @@ -0,0 +1,53 @@ +import { memo } from "react" +import { + Box, + Checkbox, + CheckboxGroup, + FormControl, + FormLabel, + Stack, + Tag, + TagCloseButton, + TagLabel, + Wrap, +} from "@chakra-ui/react" + +interface Props { + value: string[] + allMajors: string[] + onChange: (value: string[]) => void + onRemove: (major: string) => void +} + +export const MajorsField = memo(({ value, allMajors, onChange, onRemove }: Props) => ( + + 전공 + onChange(val as string[])}> + + {value.map(major => ( + + {major.split("

").pop()} + onRemove(major)} /> + + ))} + + + {allMajors.map(major => ( + + + {major.replace(/

/gi, " ")} + + + ))} + + + +)) diff --git a/src/form/SearchQueryField.tsx b/src/form/SearchQueryField.tsx new file mode 100644 index 0000000..18a1790 --- /dev/null +++ b/src/form/SearchQueryField.tsx @@ -0,0 +1,18 @@ +import { memo } from "react" +import { FormControl, FormLabel, Input } from "@chakra-ui/react" + +interface Props { + value?: string + onChange: (value: string) => void +} + +export const SearchQueryField = memo(({ value, onChange }: Props) => ( + + 검색어 + onChange(e.target.value)} + /> + +)) diff --git a/src/form/TimesField.tsx b/src/form/TimesField.tsx new file mode 100644 index 0000000..8f6b055 --- /dev/null +++ b/src/form/TimesField.tsx @@ -0,0 +1,83 @@ +import { memo, useMemo } from "react" +import { + Box, + Checkbox, + CheckboxGroup, + FormControl, + FormLabel, + Stack, + Tag, + TagCloseButton, + TagLabel, + Wrap, +} from "@chakra-ui/react" + +const TIME_SLOTS = [ + { id: 1, label: "09:00~09:30" }, + { id: 2, label: "09:30~10:00" }, + { id: 3, label: "10:00~10:30" }, + { id: 4, label: "10:30~11:00" }, + { id: 5, label: "11:00~11:30" }, + { id: 6, label: "11:30~12:00" }, + { id: 7, label: "12:00~12:30" }, + { id: 8, label: "12:30~13:00" }, + { id: 9, label: "13:00~13:30" }, + { id: 10, label: "13:30~14:00" }, + { id: 11, label: "14:00~14:30" }, + { id: 12, label: "14:30~15:00" }, + { id: 13, label: "15:00~15:30" }, + { id: 14, label: "15:30~16:00" }, + { id: 15, label: "16:00~16:30" }, + { id: 16, label: "16:30~17:00" }, + { id: 17, label: "17:00~17:30" }, + { id: 18, label: "17:30~18:00" }, + { id: 19, label: "18:00~18:50" }, + { id: 20, label: "18:55~19:45" }, + { id: 21, label: "19:50~20:40" }, + { id: 22, label: "20:45~21:35" }, + { id: 23, label: "21:40~22:30" }, + { id: 24, label: "22:35~23:25" }, +] + +interface Props { + value: number[] + onChange: (value: number[]) => void + onRemove: (time: number) => void +} + +export const TimesField = memo(({ value, onChange, onRemove }: Props) => { + const sortedTimes = useMemo(() => [...value].sort((a, b) => a - b), [value]) + + return ( + + 시간 + onChange(val.map(Number))}> + + {sortedTimes.map(time => ( + + {time}교시 + onRemove(time)} /> + + ))} + + + {TIME_SLOTS.map(({ id, label }) => ( + + + {id}교시({label}) + + + ))} + + + + ) +}) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json index d739292..01e3f9d 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -23,5 +23,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"] + "include": ["src/**/*"] } diff --git a/vite.config.ts b/vite.config.ts index 1cdac55..ccc6c38 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,19 +1,20 @@ -import { defineConfig as defineTestConfig, mergeConfig } from 'vitest/config'; -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react-swc'; +import { defineConfig as defineTestConfig, mergeConfig } from "vitest/config" +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react-swc" export default mergeConfig( defineConfig({ plugins: [react()], }), defineTestConfig({ + base: "/front_7th_chapter4-2", test: { globals: true, - environment: 'jsdom', - setupFiles: './src/setupTests.ts', + environment: "jsdom", + setupFiles: "./src/setupTests.ts", coverage: { reportsDirectory: "./.coverage", - reporter: ['lcov', 'json', 'json-summary'] + reporter: ["lcov", "json", "json-summary"], }, }, })