diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0185aec5..5bbfe3c3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,25 +2,26 @@ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - 'plugin:react-hooks/recommended', - 'plugin:prettier/recommended' + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "prettier", ], - ignorePatterns: ['dist', '.eslintrc.cjs', '**/*.config.js'], - parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - settings: { react: { version: '18.2' } }, - plugins: ['react-refresh'], + ignorePatterns: ["dist", "dist-ssg", ".eslintrc.cjs", "**/*.config.js"], + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, + settings: { react: { version: "18.2" } }, + plugins: ["react-refresh", "@stylistic/jsx"], rules: { - 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', + "react/jsx-no-target-blank": "off", + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], "react/react-in-jsx-scope": "off", "react/prop-types": "off", "react/no-unknown-property": "off", - "prettier/prettier": ["error", { "endOfLine": "auto" }] + "prettier/prettier": "off", // eslint-plugin-prettier 플러그인을 사용하지 않으므로 관련 에러를 off + "brace-style": ["error", "1tbs", { allowSingleLine: true }], }, -} +}; diff --git a/.github/workflows/check_lint.yaml b/.github/workflows/check_lint.yaml new file mode 100644 index 00000000..3b49ba49 --- /dev/null +++ b/.github/workflows/check_lint.yaml @@ -0,0 +1,21 @@ +name: check lint +on: + pull_request: + branches: + - "main" + - "dev" +jobs: + check-lint: + runs-on: ubuntu-latest + steps: + - name: pull code to computer + uses: actions/checkout@v4 + - name: install node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + - name: check lint + run: | + npm install + npm run lint + echo "You're code is pretty!" \ No newline at end of file diff --git a/.github/workflows/deploy_admin_preview.yaml b/.github/workflows/deploy_admin_preview.yaml new file mode 100644 index 00000000..ad26865b --- /dev/null +++ b/.github/workflows/deploy_admin_preview.yaml @@ -0,0 +1,31 @@ +name: deploy admin preview +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_ADMIN_PROJECT_ID }} +on: + push: + branches: + - "dev" + paths: + - "packages/common/**" + - "packages/adminPage/**" + - "public/**" +jobs: + deploy-preview: + runs-on: ubuntu-latest + steps: + - name: pull code to computer + uses: actions/checkout@v4 + - name: install node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + - name: install vercel cli + run: npm install -g vercel + - name: deploy to vercel as preview + run: | + npm install + cd ./packages/adminPage + vercel pull --yes --token=${{ secrets.VERCEL_TOKEN }} + vercel build --token=${{ secrets.VERCEL_TOKEN }} + vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/deploy_admin_production.yaml b/.github/workflows/deploy_admin_production.yaml new file mode 100644 index 00000000..c393432f --- /dev/null +++ b/.github/workflows/deploy_admin_production.yaml @@ -0,0 +1,31 @@ +name: deploy admin preview +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_ADMIN_PROJECT_ID }} +on: + push: + branches: + - "main" + paths: + - "packages/common/**" + - "packages/adminPage/**" + - "public/**" +jobs: + deploy-preview: + runs-on: ubuntu-latest + steps: + - name: pull code to computer + uses: actions/checkout@v4 + - name: install node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + - name: install vercel cli + run: npm install -g vercel + - name: deploy to vercel as preview + run: | + npm install + cd ./packages/adminPage + vercel pull --yes --token=${{ secrets.VERCEL_TOKEN }} + vercel build --token=${{ secrets.VERCEL_TOKEN }} + vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/deploy_preview.yaml b/.github/workflows/deploy_preview.yaml new file mode 100644 index 00000000..6b8c4df5 --- /dev/null +++ b/.github/workflows/deploy_preview.yaml @@ -0,0 +1,31 @@ +name: deploy preview +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + branches: + - "dev" + paths: + - "packages/common/**" + - "packages/mainPage/**" + - "public/**" +jobs: + deploy-preview: + runs-on: ubuntu-latest + steps: + - name: pull code to computer + uses: actions/checkout@v4 + - name: install node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + - name: install vercel cli + run: npm install -g vercel + - name: deploy to vercel as preview + run: | + npm install + cd ./packages/mainPage + vercel pull --yes --token=${{ secrets.VERCEL_TOKEN }} + vercel build --token=${{ secrets.VERCEL_TOKEN }} + vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/deploy_production.yaml b/.github/workflows/deploy_production.yaml new file mode 100644 index 00000000..07b96099 --- /dev/null +++ b/.github/workflows/deploy_production.yaml @@ -0,0 +1,29 @@ +name: deploy production +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + branches: + - "main" + paths: + - "packages/common/**" + - "packages/mainPage/**" + - "public/**" +jobs: + deploy-production: + runs-on: ubuntu-latest + steps: + - name: pull code to computer + uses: actions/checkout@v4 + - name: install node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + - name: install vercel cli + run: npm install -g vercel + - name: deploy to vercel as production + run: | + vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + vercel deploy --prod --prebuilt --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf36..780371fe 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ lerna-debug.log* node_modules dist -dist-ssr +dist-ssg *.local # Editor directories and files @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.vercel diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..7ffdd6fd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "jsxSingleQuote": false, + "singleQuote": false, + "trailingComma": "all", + "semi": true, + "tabWidth": 2, + "useTabs": false, + "printWidth": 100 +} \ No newline at end of file diff --git a/README.md b/README.md index 7345cd29..6044e1d4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Team6-AwesomeOrange +> 백엔드 레포는 여기로! => [백엔드 레포](https://github.com/softeerbootcamp4th/Team6-AwesomeOrange-BE) + ## Contributors -| [@lybell-art](https://github.com/lybell-art) | [@darkdulgi](https://github.com/darkdulgi) | [@ahra1221](https://github.com/blaxsior) | [@win-luck](https://github.com/win-luck) | +| [@lybell-art](https://github.com/lybell-art) | [@darkdulgi](https://github.com/darkdulgi) | [@blaxsior](https://github.com/blaxsior) | [@win-luck](https://github.com/win-luck) | |:---------------------------------------------------------:|:-------------------------------------------------------:|:-------------------------------------------------------:|:-----------------------------------------------------------------:| | | | | | | **Front-End** | **Front-End** | **Back-End** | **Back-End** | @@ -14,21 +16,45 @@ [Convention](https://github.com/softeerbootcamp4th/Team6-AwesomeOrange-BE/wiki/%08%5BTeam-Convention%5D) ## Plan & Design Link +[Plan & Design Link(Figma)](https://www.figma.com/design/XieJv765qFmU9dFuQAG7tQ/%EC%96%B4%EC%8D%B8%EC%98%A4%EB%A0%8C%EC%A7%80_Hand-off_%EC%B5%9C%EC%A2%85(07%2F24)?node-id=33-1157) + +## Schedule +**Front-End** + +| 1주차 | 공통 커스텀 훅 및 인터랙션 인터페이스 추가, 메인 페이지의 인트로, 헤더, 차량 기본정보, QnA, 푸터 구현 | +| --- | --- | +| 2주차 | 인터랙션 페이지, 인터랙션 모달, 각각의 인터랙션 구현 | +| 3주차 | 각각의 인터랙션 구현, 기대평 구현 | +| 4주차 | 선착순 이벤트 구현, 시간 남을 시 어드민 페이지(로그인, 이벤트목록) 구현 | +| 5주차 | 어드민 페이지(이벤트 등록수정, 이벤트 관리, 기대평 관리) 구현 및 리팩토링, 발표자료 제작 | + +**Back-End** + +| 1주차 | JPA Entity 구축, 배포 등 인프라 설정, 유저 로그인, 선착순 이벤트 프로토타입 구현 | +| --- | --- | +| 2주차 | 기대평, 어드민 시스템, 가중치 반영 추첨 구현 (+단위 테스트) | +| 3주차 | 선착순 이벤트 최적화, 서비스 확장성 개선, 테스트코드 작성 | +| 4주차 | 버그 수정, 부하 테스트 기반 서비스 최적화 | + +## Backlog +### Front-End +![image](https://github.com/user-attachments/assets/3fec291d-4aed-4f04-895b-7b2686aecc59) + +### Back-End +![image](https://github.com/user-attachments/assets/d7444775-cbad-48a2-a278-fd73368a1b6e) + +## ERD +image ## Tech Stack ### Front-End -- Javascript ES2020+ -- React -- Tailwindcss -- Vite -- Zustand + ### Back-End -- Spring Boot 3.2.2 -- Java 17 -- MySQL 8.0 -- Redis -- AWS EC2 -- AssertJ -- Docker + + +## Issue & TroubleShooting +[Issue & TroubleShooting](https://github.com/softeerbootcamp4th/Team6-AwesomeOrange-BE/wiki/%5BIssue-&-TroubleShooting%5D) + +## Project Archeitecture diff --git a/index.html b/index.html deleted file mode 100644 index 0c589ecc..00000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React - - -
- - - diff --git a/package-lock.json b/package-lock.json index 1bd7cc68..c21a31e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,40 @@ { - "name": "vite-project", - "version": "0.0.0", + "name": "@awesome-orange", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "vite-project", - "version": "0.0.0", + "name": "@awesome-orange", + "version": "1.0.0", + "workspaces": [ + "packages/common", + "packages/mainPage", + "packages/adminPage" + ], "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", "zustand": "^4.5.4" }, "devDependencies": { + "@stylistic/eslint-plugin-js": "^2.3.0", + "@stylistic/eslint-plugin-jsx": "^2.3.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", + "msw": "^2.3.4", "postcss": "^8.4.39", "prettier": "^3.3.3", "tailwindcss": "^3.4.6", - "vite": "^5.3.4" + "vite": "^5.3.4", + "vite-plugin-svgr": "^4.2.0" } }, "node_modules/@alloc/quick-lru": { @@ -56,6 +64,18 @@ "node": ">=6.0.0" } }, + "node_modules/@awesome-orange/admin": { + "resolved": "packages/adminPage", + "link": true + }, + "node_modules/@awesome-orange/common": { + "resolved": "packages/common", + "link": true + }, + "node_modules/@awesome-orange/main": { + "resolved": "packages/mainPage", + "link": true + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -412,6 +432,37 @@ "node": ">=6.9.0" } }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -917,6 +968,141 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/confirm": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.20.tgz", + "integrity": "sha512-UvG5Plh0MfCqUvZB8RKzBBEWB/EeMzO59Awy/Jg4NgeSjIPqhPaQFnnmxiyWUTwZh4uENB7wCklEFUwckioXWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.0.8", + "@inquirer/type": "^1.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.8.tgz", + "integrity": "sha512-ttnI/BGlP9SxjbQnv1nssv7dPAwiR82KmjJZx2SxSZyi2mGbaEvh4jg0I4yU/4mVQf7QvCVGGr/hGuJFEYhwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.1", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.0.0", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@inquirer/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", + "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.1.tgz", + "integrity": "sha512-m3YgGQlKNS0BM+8AFiJkCsTqHEFCWn6s/Rqye3mYwvqY6LdfUv12eSwbsgNzrYyrLXiy7IrrjDLPysaSBwEfhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1017,6 +1203,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", + "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1055,6 +1259,31 @@ "node": ">= 8" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1066,17 +1295,35 @@ "node": ">=14" } }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, + "node_modules/@remix-run/router": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", + "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==", "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" }, - "funding": { - "url": "https://opencollective.com/unts" + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -1303,6 +1550,296 @@ "win32" ] }, + "node_modules/@stylistic/eslint-plugin-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.3.0.tgz", + "integrity": "sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-js/node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.3.0.tgz", + "integrity": "sha512-tsQ0IEKB195H6X9A4iUSgLLLKBc8gUBWkBIU8tp1/3g2l8stu+PtMQVV/VmK1+3bem5FJCyvfcZIQ/WF1fsizA==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "^2.3.0", + "@types/eslint": "^8.56.10", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1348,6 +1885,23 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "8.56.11", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", + "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1355,6 +1909,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.2.tgz", + "integrity": "sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.11.1" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -1383,6 +1963,27 @@ "@types/react": "*" } }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -1450,6 +2051,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1806,6 +2436,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1868,26 +2510,140 @@ "readdirp": "~3.6.0" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/color-convert": { @@ -1931,6 +2687,42 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2108,6 +2900,16 @@ "node": ">=6.0.0" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2129,6 +2931,27 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -2423,37 +3246,6 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-plugin-react": { "version": "7.35.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", @@ -2712,6 +3504,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2726,16 +3524,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2967,6 +3757,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -3087,6 +3887,16 @@ "dev": true, "license": "MIT" }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3175,6 +3985,13 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3263,6 +4080,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -3471,6 +4294,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3737,6 +4567,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3780,6 +4616,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3856,6 +4701,21 @@ "loose-envify": "cli.js" } }, + "node_modules/lottie-web": { + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", + "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==", + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3866,60 +4726,226 @@ "yallist": "^3.0.2" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.4.tgz", + "integrity": "sha512-sHMlwrajgmZSA2l1o7qRSe+azm/I+x9lvVVcOxAzi4vCtH8uVPJk1K5BQYDkzGl+tt0RvM9huEXXdeGrgcc79g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^3.0.0", + "@mswjs/interceptors": "^0.29.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.7.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/msw/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/msw/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/msw/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "node_modules/msw/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8.6" + "node": ">=8" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/msw/node_modules/type-fest": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz", + "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": "*" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, "license": "ISC", "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -3958,6 +4984,16 @@ "dev": true, "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -4127,6 +5163,13 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4179,6 +5222,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4240,6 +5301,22 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -4497,19 +5574,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4522,6 +5586,13 @@ "react-is": "^16.13.1" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4532,6 +5603,13 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4585,6 +5663,23 @@ "dev": true, "license": "MIT" }, + "node_modules/react-lottie-player": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-lottie-player/-/react-lottie-player-2.1.0.tgz", + "integrity": "sha512-rxSNIVVLWYnwzsIow377vZsh7GDbReu70V7IDD9TbbcdcJWons4pSh3nyuEa4QWIZw0ZBIieoZRTsiqnb6MZ3g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lottie-web": "^5.12.2", + "rfdc": "^1.3.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -4595,6 +5690,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", + "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", + "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.1", + "react-router": "6.26.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4659,6 +5786,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -4698,6 +5842,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4920,6 +6070,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -4930,6 +6090,23 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5226,21 +6403,29 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", - "dev": true, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/swiper": { + "version": "11.1.10", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.10.tgz", + "integrity": "sha512-pAVM6vCb6bumj2B9aSh67l3wP1j5YR8dPQM1YhQKMpnBc33vs+RpyVz6NZYZl/ZopCBSYbbWK5nvESwbmU0QXQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" + "node": ">= 4.7.0" } }, "node_modules/tailwindcss": { @@ -5352,6 +6537,22 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -5363,8 +6564,7 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "license": "0BSD" + "dev": true }, "node_modules/type-check": { "version": "0.4.0", @@ -5485,6 +6685,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", + "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -5526,6 +6743,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -5598,6 +6826,20 @@ } } }, + "node_modules/vite-plugin-svgr": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz", + "integrity": "sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.5", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4 || 5" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5851,6 +7093,16 @@ "dev": true, "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5871,6 +7123,57 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5884,11 +7187,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zustand": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.4.tgz", "integrity": "sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==", - "license": "MIT", "dependencies": { "use-sync-external-store": "1.2.0" }, @@ -5911,6 +7226,31 @@ "optional": true } } + }, + "packages/adminPage": { + "name": "@awesome-orange/admin", + "version": "1.0.0", + "dependencies": { + "@awesome-orange/common": "*", + "react-router-dom": "^6.26.0" + } + }, + "packages/common": { + "name": "@awesome-orange/common", + "version": "1.0.0", + "devDependencies": { + "mime-types": "^2.1.35" + } + }, + "packages/mainPage": { + "name": "@awesome-orange/main", + "version": "1.0.0", + "dependencies": { + "@awesome-orange/common": "*", + "jwt-decode": "^4.0.0", + "react-lottie-player": "^2.1.0", + "swiper": "^11.1.9" + } } } } diff --git a/package.json b/package.json index 59a64251..7949e5d0 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { - "name": "vite-project", + "name": "@awesome-orange", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview", - "lint-fix": "eslint . --ext js,jsx --fix" + "format": "prettier --write 'packages/**/*.{js,jsx,ts,tsx,json,css}'", + "lint": "eslint . --ext js,jsx --max-warnings 0", + "lint-fix": "eslint . --ext js,jsx --fix", + "build": "npm --prefix ./packages/mainPage run build", + "build-admin": "npm --prefix ./packages/adminPage run build", + "preview": "npm --prefix ./packages/mainPage run preview", + "preview-admin": "npm --prefix ./packages/adminPage run preview" }, "dependencies": { "react": "^18.3.1", @@ -16,19 +18,32 @@ "zustand": "^4.5.4" }, "devDependencies": { + "@stylistic/eslint-plugin-js": "^2.3.0", + "@stylistic/eslint-plugin-jsx": "^2.3.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", + "msw": "^2.3.4", "postcss": "^8.4.39", "prettier": "^3.3.3", "tailwindcss": "^3.4.6", - "vite": "^5.3.4" - } + "vite": "^5.3.4", + "vite-plugin-svgr": "^4.2.0" + }, + "msw": { + "workerDirectory": [ + "public" + ] + }, + "workspaces": [ + "packages/common", + "packages/mainPage", + "packages/adminPage" + ] } diff --git a/packages/adminPage/build.js b/packages/adminPage/build.js new file mode 100644 index 00000000..ba07dbb8 --- /dev/null +++ b/packages/adminPage/build.js @@ -0,0 +1,102 @@ +import { build } from "vite"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { readFile, writeFile, mkdir, readdir, copyFile } from "node:fs/promises"; +import config from "./vite.config.js"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const toAbsolute = (p) => resolve(__dirname, p); + +console.log("yeahhhhh!!!!"); + +const buildUrl = [ + "index", + "events", + "events/create", + "login", + "events/[id]", + "comments", + "comments/[id]", + "users", +]; + +async function copyFolder(src, dest) { + // 대상 폴더가 존재하지 않으면 생성 + await mkdir(dest, { recursive: true }); + + // src 폴더 안의 모든 항목 가져오기 + const entries = await readdir(src, { withFileTypes: true }); + + // 모든 항목을 순회하며 복사 + for (let entry of entries) { + const srcPath = resolve(src, entry.name); + const destPath = resolve(dest, entry.name); + + if (entry.isDirectory()) { + // 디렉토리인 경우 재귀적으로 복사 + await copyFolder(srcPath, destPath); + } else { + // 파일인 경우 복사 + await copyFile(srcPath, destPath); + } + } +} + +async function processBuild() { + await Promise.all([buildClient(), buildSSG()]); + await Promise.all([ + copyFolder("../../public/font", `./dist/font`), + copyFolder("../../public/icons", `./dist/shared/icons`), + ]); + await injectSSGToHtml(); +} + +async function buildClient() { + await build({ + ...config, + }); +} + +function buildSSG() { + return build({ + ...config, + build: { + ssr: true, + rollupOptions: { + input: { + entry: `./src/main-server.jsx`, + }, + output: { + dir: `./dist-ssg`, + }, + }, + }, + }); +} + +async function injectSSGToHtml() { + console.log("--ssg result--"); + const { default: render } = await import(`./dist-ssg/entry.js`); + const template = await readFile(`./dist/index.html`, "utf-8"); + + const urlEntryPoint = buildUrl ?? ["index"]; + + const promises = urlEntryPoint.map(async (path) => { + const absolutePath = toAbsolute(`./dist/${path}.html`); + try { + const html = template.replace("", render(path)); + + const dir = dirname(absolutePath); + await mkdir(dir, { recursive: true }); + await writeFile(absolutePath, html); + console.log(`pre-rendered : ${path}`); + } catch (e) { + console.log(`pre-rendered failed : ${path}`); + console.error(e); + } + }); + await Promise.allSettled(promises); + console.log("--successfully build completed!--"); +} + +processBuild(); diff --git a/packages/adminPage/index.html b/packages/adminPage/index.html new file mode 100644 index 00000000..ec623a9c --- /dev/null +++ b/packages/adminPage/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + Awesome Orange - Admin + + +
+ + + diff --git a/packages/adminPage/package.json b/packages/adminPage/package.json new file mode 100644 index 00000000..cb374a39 --- /dev/null +++ b/packages/adminPage/package.json @@ -0,0 +1,15 @@ +{ + "name": "@awesome-orange/admin", + "version": "1.0.0", + "type": "module", + "main": "src/main-client.js", + "scripts": { + "dev": "vite --host", + "build": "node build.js", + "preview": "vite preview" + }, + "dependencies": { + "react-router-dom": "^6.26.0", + "@awesome-orange/common": "*" + } +} diff --git a/packages/adminPage/public/font/HyundaiSansTextKROTFMedium.woff b/packages/adminPage/public/font/HyundaiSansTextKROTFMedium.woff new file mode 100644 index 00000000..703e86e4 Binary files /dev/null and b/packages/adminPage/public/font/HyundaiSansTextKROTFMedium.woff differ diff --git a/packages/adminPage/public/font/HyundaiSansTextKROTFMedium.woff2 b/packages/adminPage/public/font/HyundaiSansTextKROTFMedium.woff2 new file mode 100644 index 00000000..c80d998b Binary files /dev/null and b/packages/adminPage/public/font/HyundaiSansTextKROTFMedium.woff2 differ diff --git a/packages/adminPage/public/icons/search.png b/packages/adminPage/public/icons/search.png new file mode 100644 index 00000000..cfe5fac1 Binary files /dev/null and b/packages/adminPage/public/icons/search.png differ diff --git a/packages/adminPage/public/robots.txt b/packages/adminPage/public/robots.txt new file mode 100644 index 00000000..77470cb3 --- /dev/null +++ b/packages/adminPage/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/packages/adminPage/src/App.jsx b/packages/adminPage/src/App.jsx new file mode 100644 index 00000000..6f2a14d1 --- /dev/null +++ b/packages/adminPage/src/App.jsx @@ -0,0 +1,46 @@ +import { useEffect } from "react"; +import { Route, Routes } from "react-router-dom"; +import LoginPage from "./pages/LoginPage.jsx"; +import EventsPage from "./pages/EventsPage.jsx"; +import EventsDetailPage from "./pages/EventsDetailPage.jsx"; +import EventsCreatePage from "./pages/EventsCreatePage.jsx"; +import EventsEditPage from "./pages/EventsEditPage.jsx"; +import ProtectedRoute from "./pages/ProtectedRoute.jsx"; +import RootRoute from "./pages/RootRoute.jsx"; +import CommentsPage from "./pages/CommentsPage.jsx"; +import CommentsIDPage from "./pages/CommentsIDPage.jsx"; +import UsersPage from "./pages/UsersPage.jsx"; +import ServerTimeInitializer from "./shared/serverTime/ServerTimeInitializer.jsx"; + +import Modal from "@common/modal/modal.jsx"; +import { initLoginState, logout } from "@admin/auth/store.js"; +import useLogoutMiddleware from "@common/dataFetch/initLogoutMiddleware"; + +function App() { + useEffect(() => { + initLoginState(); + }, []); + useLogoutMiddleware(logout); + + return ( + <> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + + + + + ); +} + +export default App; diff --git a/packages/adminPage/src/features/comment/id/Comments.jsx b/packages/adminPage/src/features/comment/id/Comments.jsx new file mode 100644 index 00000000..274cf1ee --- /dev/null +++ b/packages/adminPage/src/features/comment/id/Comments.jsx @@ -0,0 +1,73 @@ +import { useQuery } from "@common/dataFetch/getQuery.js"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import { formatDate } from "@common/utils.js"; +import Pagination from "@admin/components/Pagination"; +import { useState } from "react"; + +export default function Comments({ + eventId, + checkedComments, + setCheckedComments, + setAllId, + searchString, +}) { + const [page, setPage] = useState(1); + const data = useQuery( + eventId, + () => + fetchServer( + `/api/v1/admin/comments?eventId=${eventId}&page=${page - 1}&size=15${searchString && "&search=" + searchString}`, + ) + .then((res) => { + setAllId(res.comments.map((comment) => comment.id)); + return res; + }) + .catch((e) => { + alert("통신 오류로 기대평 로드 실패."); + console.log(e); + return { comments: [] }; + }), + [page, searchString], + ); + + function checkComment(id) { + if (checkedComments.has(id)) { + setCheckedComments((oldSet) => { + const newSet = new Set(oldSet); + newSet.delete(id); + return newSet; + }); + } else { + setCheckedComments((oldSet) => new Set([...oldSet, id])); + } + } + + return ( +
+ {data.comments.map((comment) => ( +
checkComment(comment.id)} + className="w-full py-1 grid grid-cols-[1fr_5fr_15fr] bg-neutral-50 items-center hover:bg-blue-100" + > + checkComment(comment.id)} + checked={checkedComments.has(comment.id)} + className="w-4 h-4 place-self-center" + /> + +
+ {formatDate(comment.createdAt, "YY-MM-DD")} + + {formatDate(comment.createdAt, "hh:mm:ss")} +
+ + {comment.content} +
+ ))} + + +
+ ); +} diff --git a/packages/adminPage/src/features/comment/id/DeleteButton.jsx b/packages/adminPage/src/features/comment/id/DeleteButton.jsx new file mode 100644 index 00000000..dc2bd7db --- /dev/null +++ b/packages/adminPage/src/features/comment/id/DeleteButton.jsx @@ -0,0 +1,53 @@ +import { useMutation } from "@common/dataFetch/getQuery.js"; +import openModal from "@common/modal/openModal.js"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import ConfirmModal from "@admin/modals/ConfirmModal.jsx"; +import AlertModal from "@admin/modals/AlertModal.jsx"; + +export default function DeleteButton({ eventId, checkedComments, setCheckedComments }) { + const num = checkedComments.size; + const mutation = useMutation(eventId, () => + fetchServer("/api/v1/admin/comments", { + method: "DELETE", + body: { + commentIds: [...checkedComments], + }, + }) + .then(() => { + openModal(); + setCheckedComments(new Set()); + }) + .catch((e) => { + console.log(e); + openModal(); + }), + ); + + const deleteConfirmModal = ( + + 이 동작은 다시 돌이킬 수 없습니다. +
+ {num}개의 기대평을 삭제하시겠습니까? + + } + onConfirm={mutation} + /> + ); + + function deleteComments() { + if (!num) return; + openModal(deleteConfirmModal); + } + + return ( + + ); +} diff --git a/packages/adminPage/src/features/comment/id/Loading.jsx b/packages/adminPage/src/features/comment/id/Loading.jsx new file mode 100644 index 00000000..40e4ff40 --- /dev/null +++ b/packages/adminPage/src/features/comment/id/Loading.jsx @@ -0,0 +1,9 @@ +import Spinner from "@common/components/Spinner"; + +export default function Loading() { + return ( +
+ +
+ ); +} diff --git a/packages/adminPage/src/features/comment/id/index.jsx b/packages/adminPage/src/features/comment/id/index.jsx new file mode 100644 index 00000000..557155c6 --- /dev/null +++ b/packages/adminPage/src/features/comment/id/index.jsx @@ -0,0 +1,85 @@ +import Suspense from "@common/components/Suspense"; +import Loading from "./Loading.jsx"; +import Comments from "./Comments.jsx"; +import { useState } from "react"; +import DeleteButton from "./DeleteButton.jsx"; + +export default function AdminCommentID({ eventId }) { + const [checkedComments, setCheckedComments] = useState(new Set()); + const [formString, setFormString] = useState(""); + const [searchString, setSearchString] = useState(""); + const [allId, setAllId] = useState([]); + + function selectAll() { + if (allId.every((id) => checkedComments.has(id))) { + setCheckedComments((oldSet) => { + const newSet = new Set(oldSet); + allId.forEach((id) => { + newSet.delete(id); + }); + return newSet; + }); + } else { + setCheckedComments((oldSet) => new Set([...oldSet, ...allId])); + } + } + + function searchComment(e) { + e.preventDefault(); + setSearchString(formString); + } + + return ( +
+
+
+ 검색 이벤트: + {eventId} + 검색 문자열: + {searchString} +
+ + +
+ +
+ setFormString(e.target.value)} + placeholder="검색 단어 입력" + className="bg-neutral-50 focus:bg-white w-full px-4 py-2 rounded-lg text-body-s" + /> + + 검색 +
+ +
+ + 선택 + + 작성 시간 + 기대평 내용 +
+ + }> + + +
+ ); +} diff --git a/packages/adminPage/src/features/comment/index.jsx b/packages/adminPage/src/features/comment/index.jsx new file mode 100644 index 00000000..a9df5623 --- /dev/null +++ b/packages/adminPage/src/features/comment/index.jsx @@ -0,0 +1,108 @@ +import { useState } from "react"; +import { fetchServer } from "@common/dataFetch/fetchServer"; +import { useNavigate } from "react-router-dom"; + +export default function AdminComment() { + const navigate = useNavigate(); + const [formString, setFormString] = useState(""); + const [isSpread, setIsSpread] = useState(false); + const [searchList, setSearchList] = useState([]); + const [selectedEvent, setSelectedEvent] = useState(-1); + + function autoCorrect(str) { + fetchServer(`/api/v1/admin/events/hints?search=${str}`) + .then((res) => { + setSearchList(res); + }) + .catch((e) => { + console.log(e); + }); + } + + function onChangeForm(e) { + let newString = e.target.value.replace(/[^0-9]/g, ""); + + if (!newString) { + newString = ""; + } else if (newString.length <= 6) { + newString = "HD_" + newString; + } else if (newString.length <= 9) { + newString = "HD_" + newString.slice(0, 6) + "_" + newString.slice(6); + } else return; + + if (newString !== formString) { + if (newString.length >= 6) { + setSelectedEvent(-1); + setIsSpread(true); + autoCorrect(newString); + } else { + setIsSpread(false); + } + } + setFormString(newString); + } + + function searchEvent(e, eventId) { + e.preventDefault(); + + const searchID = eventId ?? formString; + navigate(`/comments/${searchID}`); + } + + function onKeyDown(e) { + if (!isSpread || !searchList.length || (e.key !== "ArrowUp" && e.key !== "ArrowDown")) return; + e.preventDefault(); + let nextIndex = selectedEvent; + + if (e.key === "ArrowUp") { + if (nextIndex === -1) nextIndex++; + nextIndex = (nextIndex - 1 + searchList.length) % searchList.length; + } else { + nextIndex = (nextIndex + 1) % searchList.length; + } + + setSelectedEvent(nextIndex); + setFormString(searchList[nextIndex].eventId); + } + + return ( +
+ + +
+ {searchList.map((evt, index) => ( +
  • setSelectedEvent(index)} + onClick={(e) => searchEvent(e, evt.eventId)} + className={`cursor-pointer list-none w-full rounded px-1 flex ${index === selectedEvent && "bg-blue-200"}`} + > + {evt.eventId} + {evt.name} +
  • + ))} + + + 일치하는 검색 결과가 없습니다. + +
    + + 검색 +
    + ); +} diff --git a/packages/adminPage/src/features/comment/mock.js b/packages/adminPage/src/features/comment/mock.js new file mode 100644 index 00000000..9712ebad --- /dev/null +++ b/packages/adminPage/src/features/comment/mock.js @@ -0,0 +1,42 @@ +import { http, HttpResponse } from "msw"; +import getRandomString from "@common/mock/getRandomString"; + +function getSampleEventList() { + const len = 10; + let eventList = []; + for (let i = 0; i < len; i++) { + eventList = [ + ...eventList, + { + eventId: "HD_240909_00" + i, + name: getRandomString(10), + }, + ]; + } + return eventList; +} + +function getSampleCommentList() { + const len = 15; + let commentList = []; + for (let i = 0; i < len; i++) { + commentList = [ + ...commentList, + { + id: i, + content: getRandomString(150), + userName: getRandomString(5), + createdAt: "2024-08-14T07:11:27.244Z", + }, + ]; + } + return { comments: commentList, totalPages: 15 }; +} + +const handlers = [ + http.get("/api/v1/admin/events/hints", () => HttpResponse.json(getSampleEventList())), + http.get("/api/v1/admin/comments", () => HttpResponse.json(getSampleCommentList())), + http.delete("/api/v1/admin/comments", () => HttpResponse.json(true)), +]; + +export default handlers; diff --git a/packages/adminPage/src/features/eventDetail/EventBaseDataRenderer.jsx b/packages/adminPage/src/features/eventDetail/EventBaseDataRenderer.jsx new file mode 100644 index 00000000..ed28e2cc --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/EventBaseDataRenderer.jsx @@ -0,0 +1,59 @@ +import tableStyle from "./tableStyle.js"; +import EventStatus from "@admin/serverTime/EventStatus.js"; +import DrawButtonHolder from "./drawButton/DrawButtonHolder.jsx"; +import Suspense from "@common/components/Suspense.jsx"; +import ErrorBoundary from "@common/components/ErrorBoundary.jsx"; +import { formatDate } from "@common/utils.js"; + +function EventBaseDataRenderer({ + name, + eventId, + eventFrameId, + startTime, + endTime, + description, + url, + eventType, +}) { + return ( +
    +

    이벤트 명

    +

    {name}

    +

    이벤트 ID

    +

    {eventId}

    +

    이벤트 프레임

    +

    {eventFrameId}

    +

    이벤트 기간

    +
    + {formatDate(startTime, "YYYY-MM-DD hh:mm")} ~ {formatDate(endTime, "YYYY-MM-DD hh:mm")} ( + + + + ) +
    +

    + 이벤트 요약 +

    +
    {description}
    +

    이벤트 URL

    +

    + + {url} + +

    +

    이벤트 종류

    +
    +

    {eventType === "fcfs" ? "선착순" : "추첨"}

    + {eventType === "draw" && ( + + + + + + )} +
    +
    + ); +} + +export default EventBaseDataRenderer; diff --git a/packages/adminPage/src/features/eventDetail/EventDetailFetcher.jsx b/packages/adminPage/src/features/eventDetail/EventDetailFetcher.jsx new file mode 100644 index 00000000..f7ddc04f --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/EventDetailFetcher.jsx @@ -0,0 +1,12 @@ +import EventDetail from "./index.jsx"; +import { useQuery } from "@common/dataFetch/getQuery.js"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; + +function EventDetailFetcher({ eventId }) { + const data = useQuery(`admin-event-list/${eventId}`, () => + fetchServer(`/api/v1/admin/events/${eventId}`), + ); + return ; +} + +export default EventDetailFetcher; diff --git a/packages/adminPage/src/features/eventDetail/draw/EventDrawMetadataItem.jsx b/packages/adminPage/src/features/eventDetail/draw/EventDrawMetadataItem.jsx new file mode 100644 index 00000000..113d8251 --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/draw/EventDrawMetadataItem.jsx @@ -0,0 +1,11 @@ +function EventDrawMetadataItem({ grade, count, prizeInfo }) { + return ( + <> +

    {grade}등

    +

    {count}명

    +

    {prizeInfo}

    + + ); +} + +export default EventDrawMetadataItem; diff --git a/packages/adminPage/src/features/eventDetail/draw/EventDrawPolicyItem.jsx b/packages/adminPage/src/features/eventDetail/draw/EventDrawPolicyItem.jsx new file mode 100644 index 00000000..c259151a --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/draw/EventDrawPolicyItem.jsx @@ -0,0 +1,12 @@ +import { POLICY_ENUM } from "@admin/constants.js"; + +function EventDrawPolicyItem({ action, score }) { + return ( + <> +

    {POLICY_ENUM[action]}

    +

    {score}

    + + ); +} + +export default EventDrawPolicyItem; diff --git a/packages/adminPage/src/features/eventDetail/draw/index.jsx b/packages/adminPage/src/features/eventDetail/draw/index.jsx new file mode 100644 index 00000000..e1de4d4c --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/draw/index.jsx @@ -0,0 +1,45 @@ +import EventDrawMetadataItem from "./EventDrawMetadataItem.jsx"; +import EventDrawPolicyItem from "./EventDrawPolicyItem.jsx"; +import tableStyle from "../tableStyle.js"; + +function EventDrawDataRenderer({ data }) { + const gridCommonStyle = "grid px-2 gap-4 justify-items-center"; + const metadataGridStyle = `${gridCommonStyle} grid-cols-[3rem_6rem_1fr]`; + const policyGridStyle = `${gridCommonStyle} grid-cols-[3fr_1fr]`; + const headerStyle = `h-10 bg-neutral-50 rounded-lg text-black items-center font-bold text-center + *:relative *:w-full *:after:h-full *:after:absolute *:after:-right-2 *:after:border-r *:after:border-neutral-200`; + const titleStyle = "text-center font-bold self-start h-10 flex justify-center items-center"; + return ( + <> +
    +

    당첨 인원수

    +
    +
    +

    등수

    +

    인원 수

    +

    경품

    +
    +
    + {data.metadata.map((data) => ( + + ))} +
    +
    +

    점수 정책

    +
    +
    +

    액션

    +

    배율

    +
    +
    + {data.policies.map((data) => ( + + ))} +
    +
    +
    + + ); +} + +export default EventDrawDataRenderer; diff --git a/packages/adminPage/src/features/eventDetail/drawButton/DrawButton.jsx b/packages/adminPage/src/features/eventDetail/drawButton/DrawButton.jsx new file mode 100644 index 00000000..90061937 --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/drawButton/DrawButton.jsx @@ -0,0 +1,111 @@ +import { useState, useEffect, useRef } from "react"; +import { useParams } from "react-router-dom"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; + +import Button from "@common/components/Button.jsx"; +import openModal from "@common/modal/openModal.js"; +import AlertModal from "@admin/modals/AlertModal.jsx"; + +import DrawResultModal from "./DrawResultModal.jsx"; +import ErrorBoundary from "@common/components/ErrorBoundary.jsx"; +import Suspense from "@common/components/Suspense.jsx"; +import Spinner from "@common/components/Spinner.jsx"; +import DelaySkeleton from "@common/components/DelaySkeleton.jsx"; + +function ResultModalContainer({ eventId }) { + return ( +
    + 에러남
    }> + + + + + + } + > + + + + + ); +} + +function DrawButton() { + const { eventId } = useParams(); + const [drawState, setDrawState] = useState("BEFORE_END"); + const interval = useRef(null); + const timeout = useRef(null); + + useEffect(() => { + fetchServer(`/api/v1/admin/draw/${eventId}/status`) + .then(({ status }) => setDrawState(status)) + .catch(() => { + setDrawState("ERROR"); + }); + }, [eventId]); + + useEffect(() => { + return () => { + clearInterval(interval.current); + clearTimeout(timeout.current); + }; + }, []); + + async function onSubmit() { + function shortPooling() { + fetchServer(`/api/v1/admin/draw/${eventId}/status`) + .then(({ status }) => { + setDrawState(status); + if (status !== "IS_DRAWING") { + clearInterval(interval.current); + } + }) + .catch(() => { + setDrawState("ERROR"); + clearInterval(interval.current); + }); + } + + try { + await fetchServer(`/api/v1/admin/draw/${eventId}/draw`, { method: "post" }); + setDrawState("IS_DRAWING"); + openModal(); + timeout.current = setTimeout(() => { + shortPooling(); + interval.current = setInterval(shortPooling, 5000); + }, 500); + } catch { + openModal(); + } + } + + switch (drawState) { + case "BEFORE_END": + return null; + case "AVAILABLE": + return ( + + ); + case "IS_DRAWING": + return ( +
    추첨 진행중...
    + ); + case "COMPLETE": + return ( + + ); + default: + return
    에러
    ; + } +} + +export default DrawButton; diff --git a/packages/adminPage/src/features/eventDetail/drawButton/DrawButtonHolder.jsx b/packages/adminPage/src/features/eventDetail/drawButton/DrawButtonHolder.jsx new file mode 100644 index 00000000..d24010b1 --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/drawButton/DrawButtonHolder.jsx @@ -0,0 +1,12 @@ +import useServerTimeStore from "@admin/serverTime/store.js"; +import DrawButton from "./DrawButton.jsx"; + +function DrawButtonHolder({ endTime }) { + const getServerTime = useServerTimeStore((store) => store.getData); + const serverTime = getServerTime(); + + if (new Date(endTime) < serverTime) return ; + return null; +} + +export default DrawButtonHolder; diff --git a/packages/adminPage/src/features/eventDetail/drawButton/DrawResultModal.jsx b/packages/adminPage/src/features/eventDetail/drawButton/DrawResultModal.jsx new file mode 100644 index 00000000..3fd30b6d --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/drawButton/DrawResultModal.jsx @@ -0,0 +1,78 @@ +import { Fragment, useMemo } from "react"; +import useScrollControl from "./useScrollControl.js"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import { useQuery } from "@common/dataFetch/getQuery.js"; +import { GroupMap, addHyphen } from "@common/utils.js"; + +function mapResultSubsetGroup({ ranking, name, phoneNumber }) { + return ( + +

    {ranking}등

    +

    {name}

    +

    {addHyphen(phoneNumber)}

    +
    + ); +} + +function DrawResultModal({ eventId }) { + const drawResultData = useQuery(`event-detail-draw-result-${eventId}`, () => { + return fetchServer(`/api/v1/admin/draw/${eventId}/winners`); + }); + const { hullRef, mountMap, scrollTo, intersectState } = useScrollControl(); + + // render logic + const maxGrade = drawResultData.length === 0 ? 0 : drawResultData.at(-1).ranking; + const tableStyle = + "w-full grid grid-cols-[4rem_1fr_2fr] auto-rows-[minmax(2rem,auto)] gap-4 items-center justify-items-center"; + + const drawResultGroup = useMemo(() => { + const groupMap = new GroupMap(); + for (let item of drawResultData) groupMap.set(item.ranking, item); + return groupMap; + }, [drawResultData]); + + function mapResultGroup([key, subset]) { + return ( +
    mountMap(ref, key)} + > + {subset.map(mapResultSubsetGroup)} +
    + ); + } + + return ( +
    +

    당첨자

    +
    + {Array.from({ length: maxGrade }, (_, i) => ( + + ))} +
    +
    +

    등수

    +

    이름

    +

    전화번호

    +
    +
    + {drawResultData.length === 0 ? ( +
    + 저런! 참가자가 없군요! +
    + ) : ( +
    {[...drawResultGroup].map(mapResultGroup)}
    + )} +
    +
    + ); +} + +export default DrawResultModal; diff --git a/packages/adminPage/src/features/eventDetail/drawButton/mock.js b/packages/adminPage/src/features/eventDetail/drawButton/mock.js new file mode 100644 index 00000000..6b12b6a2 --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/drawButton/mock.js @@ -0,0 +1,40 @@ +import { http, HttpResponse } from "msw"; +import getRandomString from "@common/mock/getRandomString.js"; + +let result = []; +let status = "AVAILABLE"; + +function makeDrawComplete() { + const newResult = []; + for (let i = 0; i < 10; i++) { + for (let j = 0; j < 5 * (1 + i); j++) { + newResult.push({ + ranking: i + 1, + name: getRandomString(4), + phoneNumber: + "010" + + Math.floor(Math.random() * 99999999) + .toString() + .padStart(8, "0"), + }); + } + } + return newResult; +} + +const handlers = [ + http.post("/api/v1/admin/draw/:eventId/draw", () => { + result = makeDrawComplete(); + status = "IS_DRAWING"; + setTimeout(() => (status = "COMPLETE"), 3000); + return new HttpResponse(null, { status: 201 }); + }), + http.get("/api/v1/admin/draw/:eventId/winners", () => { + return HttpResponse.json(result); + }), + http.get("/api/v1/admin/draw/:eventId/status", () => { + return HttpResponse.json({ status }); + }), +]; + +export default handlers; diff --git a/packages/adminPage/src/features/eventDetail/drawButton/useScrollControl.js b/packages/adminPage/src/features/eventDetail/drawButton/useScrollControl.js new file mode 100644 index 00000000..e85f5d39 --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/drawButton/useScrollControl.js @@ -0,0 +1,65 @@ +import { useState, useRef, useCallback, useEffect } from "react"; +import { KVMap } from "@common/utils.js"; + +function useScrollControl() { + const [intersectState, setIntersectState] = useState(1); + const hullRef = useRef(null); + const itemRef = useRef(null); + const observedTargetRef = useRef([]); + const observerRef = useRef(null); + function getMap() { + if (itemRef.current === null) itemRef.current = new KVMap(); + return itemRef.current; + } + + const mountMap = useCallback((ref, key) => { + const map = getMap(); + if (ref) { + map.set(key, ref); + observerRef.current?.observe(ref); + } else { + const prevRef = map.getWithKey(key); + if (prevRef instanceof Element) observerRef.current?.unobserve(prevRef); + map.deleteWithKey(key); + } + }, []); + + const scrollTo = useCallback((key) => { + const map = getMap(); + if (!map.hasKey(key)) return; + map.getWithKey(key).scrollIntoView({ behavior: "smooth" }); + }, []); + + useEffect(() => { + observerRef.current = new IntersectionObserver( + (entries) => { + const map = getMap(); + entries.forEach((entry) => { + const key = map.getWithValue(entry.target); + observedTargetRef.current[key] = entry.isIntersecting; + }); + const nextState = observedTargetRef.current.findIndex( + (isIntersect) => isIntersect === true, + ); + setIntersectState(nextState); + }, + { root: hullRef.current ?? null, threshold: 0.01 }, + ); + for (let [, elem] of getMap()) { + observerRef.current.observe(elem); + } + + return () => { + observerRef.current.disconnect(); + }; + }, []); + + return { + hullRef, + mountMap, + scrollTo, + intersectState, + }; +} + +export default useScrollControl; diff --git a/packages/adminPage/src/features/eventDetail/fcfs/EventFcfsDataItem.jsx b/packages/adminPage/src/features/eventDetail/fcfs/EventFcfsDataItem.jsx new file mode 100644 index 00000000..c22d32f3 --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/fcfs/EventFcfsDataItem.jsx @@ -0,0 +1,17 @@ +import { formatDate } from "@common/utils.js"; + +function EventFcfsDataItem({ id, startTime, endTime, participantCount, prizeInfo }) { + return ( + <> +

    {id}

    +

    {formatDate(startTime, "M/DD")}

    +

    + {formatDate(startTime, "hh:mm")} ~ {formatDate(endTime, "hh:mm")} +

    +

    {participantCount}명

    +

    {prizeInfo}

    + + ); +} + +export default EventFcfsDataItem; diff --git a/packages/adminPage/src/features/eventDetail/fcfs/index.jsx b/packages/adminPage/src/features/eventDetail/fcfs/index.jsx new file mode 100644 index 00000000..8b72924a --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/fcfs/index.jsx @@ -0,0 +1,28 @@ +import EventFcfsDataItem from "./EventFcfsDataItem.jsx"; + +function EventFcfsDataRenderer({ data }) { + const gridStyle = + "grid grid-cols-[1fr_3rem_1fr_5rem_3fr] gap-4 justify-center items-center text-body-m"; + const headerStyle = `${gridStyle} min-h-12 py-2 bg-neutral-50 rounded-lg text-black font-bold text-center + *:relative *:w-full *:after:h-full *:after:absolute *:after:-right-2 *:after:border-r *:after:border-neutral-200 + `; + + return ( +
    +
    +
    ID
    +
    날짜
    +
    이벤트 시간
    +
    당첨자 수
    +
    경품
    +
    +
    + {data.map((item) => ( + + ))} +
    +
    + ); +} + +export default EventFcfsDataRenderer; diff --git a/packages/adminPage/src/features/eventDetail/index.jsx b/packages/adminPage/src/features/eventDetail/index.jsx new file mode 100644 index 00000000..eddb1dcd --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/index.jsx @@ -0,0 +1,24 @@ +import { Link } from "react-router-dom"; +import EventBaseDataRenderer from "./EventBaseDataRenderer.jsx"; +import EventFcfsDataRenderer from "./fcfs"; +import EventDrawDataRenderer from "./draw"; +import TitleContainer from "@admin/components/TitleContainer.jsx"; +import Button from "@common/components/Button.jsx"; + +function EventDetail({ data }) { + return ( +
    + +

    이벤트 상세정보

    + + + +
    + + {data.eventType === "fcfs" && } + {data.eventType === "draw" && } +
    + ); +} + +export default EventDetail; diff --git a/packages/adminPage/src/features/eventDetail/tableStyle.js b/packages/adminPage/src/features/eventDetail/tableStyle.js new file mode 100644 index 00000000..d23a97fb --- /dev/null +++ b/packages/adminPage/src/features/eventDetail/tableStyle.js @@ -0,0 +1,3 @@ +const tableStyle = "grid grid-cols-[6rem_1fr] auto-rows-[minmax(2rem,auto)] items-center gap-2"; + +export default tableStyle; diff --git a/packages/adminPage/src/features/eventEdit/DateTimeRangeInput.jsx b/packages/adminPage/src/features/eventEdit/DateTimeRangeInput.jsx new file mode 100644 index 00000000..86b1dec0 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/DateTimeRangeInput.jsx @@ -0,0 +1,115 @@ +import { Input } from "@admin/components/SmallInput.jsx"; +import { formatDate } from "@common/utils.js"; + +function dateToSplittedState(date) { + if (date === null || date === "" || date === undefined) return ["", ""]; + return formatDate(date, "YYYY-MM-DD hh:mm").split(" "); +} + +function applyDateInputToDateObj(inputValue, timeValue, defaultTime = "00:00") { + if (inputValue === "") return null; + const [y, m, d] = inputValue.split("-").map(Number); + + let date = timeValue === null ? new Date(`1970.1.1 ${defaultTime}`) : new Date(timeValue); + + date.setFullYear(y); + date.setMonth(m - 1); + date.setDate(d); + return date; +} + +function applyTimeInputToDateObj(inputValue, timeValue) { + if (inputValue === "") return null; + const [h, m] = inputValue.split(":").map(Number); + + let date = timeValue === null ? new Date() : new Date(timeValue); + + date.setHours(h); + date.setMinutes(Math.round(m / 5) * 5); + return date; +} + +function DateTimeRangeInput({ + range = [null, null], + setRange, + wrapperClass, + inputClass, + required, +} = {}) { + const [startDate, startTime] = dateToSplittedState(range[0]); + const [endDate, endTime] = dateToSplittedState(range[1]); + + function setStartDate(value) { + if (value === "") return setRange([null, range[1]]); + let date = applyDateInputToDateObj(value, range[0]); + if (range[1] === null || date <= range[1]) setRange([date, range[1]]); + else setRange([range[1], range[1]]); + } + + function setStartTime(value) { + if (value === "") return setRange([null, range[1]]); + let date = applyTimeInputToDateObj(value, range[0]); + if (range[1] === null || date <= range[1]) setRange([date, range[1]]); + else setRange([range[1], range[1]]); + } + + function setEndDate(value) { + if (value === "") return setRange([range[0], null]); + let date = applyDateInputToDateObj(value, range[1], "23:55"); + if (range[0] === null || date >= range[0]) setRange([range[0], date]); + else setRange([range[0], range[0]]); + } + + function setEndTime(value) { + if (value === "") return setRange([range[0], null]); + let date = applyTimeInputToDateObj(value, range[1]); + if (range[0] === null || date >= range[0]) setRange([range[0], date]); + else setRange([range[0], range[0]]); + } + + return ( +
    +
    + + +
    + ~ +
    + + +
    +
    + ); +} + +export default DateTimeRangeInput; diff --git a/packages/adminPage/src/features/eventEdit/DrawInput/DrawMetadataInput.jsx b/packages/adminPage/src/features/eventEdit/DrawInput/DrawMetadataInput.jsx new file mode 100644 index 00000000..76b5eabf --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/DrawInput/DrawMetadataInput.jsx @@ -0,0 +1,45 @@ +import { useContext } from "react"; +import { EventEditContext, EventEditDispatchContext } from "../businessLogic/context.js"; +import DrawMetadataItemInput from "./DrawMetadataItemInput.jsx"; + +function DrawMetadataInput() { + const { + draw: { metadata }, + } = useContext(EventEditContext); + const dispatch = useContext(EventEditDispatchContext); + + return ( +
    +
    + + 등수* :{" "} + + +
    +
    +
    +
    등수
    +
    인원 수
    +
    경품
    +
    +
    + {[...metadata].map((data) => ( + + ))} +
    +
    +
    + ); +} + +export default DrawMetadataInput; diff --git a/packages/adminPage/src/features/eventEdit/DrawInput/DrawMetadataItemInput.jsx b/packages/adminPage/src/features/eventEdit/DrawInput/DrawMetadataItemInput.jsx new file mode 100644 index 00000000..0a5bf8c8 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/DrawInput/DrawMetadataItemInput.jsx @@ -0,0 +1,34 @@ +import { useContext } from "react"; +import { EventEditDispatchContext } from "../businessLogic/context.js"; +import { Input } from "@admin/components/SmallInput.jsx"; + +function DrawMetadataItemInput({ grade, count, prizeInfo }) { + const dispatch = useContext(EventEditDispatchContext); + const modify = (value) => + dispatch({ type: "modify_draw_grade_item", value: { grade, ...value } }); + + return ( + <> +
    {grade}등
    +
    + modify({ count: Number.isNaN(+value) ? 0 : +value })} + inputMode="numeric" + pattern="[0-9]+" + size="3" + placeholder="인원" + /> + 명 +
    + modify({ prizeInfo: value })} + placeholder="경품 이름 입력" + /> + + ); +} + +export default DrawMetadataItemInput; diff --git a/packages/adminPage/src/features/eventEdit/DrawInput/DrawPolicyInput.jsx b/packages/adminPage/src/features/eventEdit/DrawInput/DrawPolicyInput.jsx new file mode 100644 index 00000000..b3fa77ba --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/DrawInput/DrawPolicyInput.jsx @@ -0,0 +1,29 @@ +import { useContext } from "react"; +import { EventEditContext, EventEditDispatchContext } from "../businessLogic/context.js"; +import DrawPolicyItemInput from "./DrawPolicyItemInput.jsx"; +import Button from "@common/components/Button.jsx"; + +function DrawPolicyInput() { + const { + draw: { policies }, + } = useContext(EventEditContext); + const dispatch = useContext(EventEditDispatchContext); + + return ( +
    +
    +
    액션
    +
    배율
    +
    삭제
    +
    +
    + {[...policies].map(({ key, ...data }) => ( + + ))} +
    + +
    + ); +} + +export default DrawPolicyInput; diff --git a/packages/adminPage/src/features/eventEdit/DrawInput/DrawPolicyItemInput.jsx b/packages/adminPage/src/features/eventEdit/DrawInput/DrawPolicyItemInput.jsx new file mode 100644 index 00000000..706284e7 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/DrawInput/DrawPolicyItemInput.jsx @@ -0,0 +1,42 @@ +import { useContext } from "react"; +import { EventEditDispatchContext } from "../businessLogic/context.js"; +import { Input } from "@admin/components/SmallInput.jsx"; +import DeleteButton from "@admin/components/DeleteButton"; +import { POLICY_ENUM } from "@admin/constants.js"; + +const POLICY_ENTRIES = Object.entries(POLICY_ENUM); + +function DrawPolicyItemInput({ action, score, uniqueKey }) { + const dispatch = useContext(EventEditDispatchContext); + const modify = (value) => dispatch({ type: "modify_draw_policy", key: uniqueKey, value }); + + return ( + <> + + modify({ score: Number.isNaN(+value) ? 0 : +value })} + inputMode="numeric" + pattern="[0-9]+" + size="6" + placeholder="점수 배율" + /> + dispatch({ type: "delete_draw_policy", key: uniqueKey })} /> + + ); +} + +export default DrawPolicyItemInput; diff --git a/packages/adminPage/src/features/eventEdit/DrawInput/DrawSectionTitle.jsx b/packages/adminPage/src/features/eventEdit/DrawInput/DrawSectionTitle.jsx new file mode 100644 index 00000000..b047ce96 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/DrawInput/DrawSectionTitle.jsx @@ -0,0 +1,9 @@ +function DrawSectionTitle({ children }) { + return ( +

    + {children} +

    + ); +} + +export default DrawSectionTitle; diff --git a/packages/adminPage/src/features/eventEdit/DrawInput/index.jsx b/packages/adminPage/src/features/eventEdit/DrawInput/index.jsx new file mode 100644 index 00000000..383d6b42 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/DrawInput/index.jsx @@ -0,0 +1,20 @@ +import DrawSectionTitle from "./DrawSectionTitle.jsx"; +import DrawMetadataInput from "./DrawMetadataInput.jsx"; +import DrawPolicyInput from "./DrawPolicyInput.jsx"; + +function DrawInput() { + return ( +
    +
    + 인원수 및 경품 설정 + +
    +
    + 정책 설정 + +
    +
    + ); +} + +export default DrawInput; diff --git a/packages/adminPage/src/features/eventEdit/EventBaseDataInput.jsx b/packages/adminPage/src/features/eventEdit/EventBaseDataInput.jsx new file mode 100644 index 00000000..3514d758 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/EventBaseDataInput.jsx @@ -0,0 +1,110 @@ +import { useContext } from "react"; +import { Input, TextBox } from "@admin/components/SmallInput.jsx"; +import { + EventEditContext, + EventEditDispatchContext, + EventEditModeContext, +} from "./businessLogic/context.js"; +import DateTimeRangeInput from "./DateTimeRangeInput.jsx"; + +function EventBaseDataInput() { + const { name, eventId, eventFrameId, startTime, endTime, description, url } = + useContext(EventEditContext); + const dispatch = useContext(EventEditDispatchContext); + const mode = useContext(EventEditModeContext); + + const columnsStyle = "grid grid-cols-[6rem_1fr] items-center gap-2"; + const NAME_MAX_LENGTH = 40; + const DESCRIPTION_MAX_LENGTH = 100; + + return ( + <> + + + + + + + + ); +} + +export default EventBaseDataInput; diff --git a/packages/adminPage/src/features/eventEdit/EventDetailInput.jsx b/packages/adminPage/src/features/eventEdit/EventDetailInput.jsx new file mode 100644 index 00000000..73f44876 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/EventDetailInput.jsx @@ -0,0 +1,57 @@ +import { useContext } from "react"; +import { + EventEditContext, + EventEditDispatchContext, + EventEditModeContext, +} from "./businessLogic/context.js"; + +import FcfsInput from "./FcfsInput"; +import DrawInput from "./DrawInput"; + +function EventTypeSelector({ selected, onClick, children }) { + const selectorBaseStyle = `flex justify-center items-center px-8 py-2 rounded-t-lg text-body-m relative hover:bg-blue-50`; + const selectedStyle = `text-blue-400 font-bold after:w-full after:h-1 after:absolute after:-bottom-px after:border-b-2 after:border-blue-400`; + const unSelectedStyle = `text-neutral-800 hover:text-blue-400`; + + return ( + + ); +} + +function EventDetailInput() { + const { eventType } = useContext(EventEditContext); + const dispatch = useContext(EventEditDispatchContext); + const mode = useContext(EventEditModeContext); + + function selectEventType(type) { + return () => { + if (mode === "edit") return; + dispatch({ type: "set_event_type", value: type }); + }; + } + + return ( +
    +
    + + 선착순 + + + 추첨 + +
    +
    + {eventType === "fcfs" && } + {eventType === "draw" && } +
    +
    + ); +} + +export default EventDetailInput; diff --git a/packages/adminPage/src/features/eventEdit/EventEditFetcher.jsx b/packages/adminPage/src/features/eventEdit/EventEditFetcher.jsx new file mode 100644 index 00000000..c79efd79 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/EventEditFetcher.jsx @@ -0,0 +1,12 @@ +import EventEditor from "./index.jsx"; +import { useQuery } from "@common/dataFetch/getQuery.js"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; + +function EventEditFetcher({ eventId }) { + const data = useQuery(`admin-event-list/${eventId}`, () => + fetchServer(`/api/v1/admin/events/${eventId}`), + ); + return ; +} + +export default EventEditFetcher; diff --git a/packages/adminPage/src/features/eventEdit/FcfsInput/BatchTimeUpdateInput.jsx b/packages/adminPage/src/features/eventEdit/FcfsInput/BatchTimeUpdateInput.jsx new file mode 100644 index 00000000..9810333f --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/FcfsInput/BatchTimeUpdateInput.jsx @@ -0,0 +1,60 @@ +import { useContext } from "react"; +import { EventEditDispatchContext } from "../businessLogic/context.js"; +import { + fcfsBatchControlReducer, + getBatchTimeConfig, + getToggleBatchTimeConfig, +} from "./reducer.js"; +import { Input } from "@admin/components/SmallInput.jsx"; +import Checkbox from "@common/components/Checkbox.jsx"; + +import { padNumber } from "@common/utils.js"; + +const MINUTES = 60; + +function valueToTimeStr(value) { + const m = value % MINUTES; + const h = (value - m) / MINUTES; + return `${padNumber(h)}:${padNumber(m)}`; +} + +function timeStrToValue(str) { + let [h, m] = str.split(":").map(Number); + return h * MINUTES + m; +} + +function BatchTimeUpdateInput({ caption, type, state, dispatch }) { + const eventEditDispatch = useContext(EventEditDispatchContext); + const checked = state[`${type}Check`]; + const timeStr = valueToTimeStr(state[type]); + + function onCheckboxClick(newValue) { + const action = { type: `toggle_${type}_time`, value: newValue }; + const nextState = fcfsBatchControlReducer(state, action); + dispatch({ type: "apply", value: nextState }); + + const config = getToggleBatchTimeConfig(nextState, type); + eventEditDispatch({ type: "modify_all_fcfs_item", value: config }); + } + function setTimeValue(newValue) { + const action = { type: `set_${type}_time`, value: timeStrToValue(newValue) }; + const nextState = fcfsBatchControlReducer(state, action); + dispatch({ type: "apply", value: nextState }); + eventEditDispatch({ + type: "modify_all_fcfs_item", + value: getBatchTimeConfig(nextState, state), + }); + } + + return ( + + ); +} + +export default BatchTimeUpdateInput; diff --git a/packages/adminPage/src/features/eventEdit/FcfsInput/BatchTimeUpdater.jsx b/packages/adminPage/src/features/eventEdit/FcfsInput/BatchTimeUpdater.jsx new file mode 100644 index 00000000..2f1e3427 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/FcfsInput/BatchTimeUpdater.jsx @@ -0,0 +1,41 @@ +import { useReducer, useContext } from "react"; +import { getDefaultState, fcfsBatchControlReducer, getBatchTimeConfig } from "./reducer.js"; +import BatchTimeUpdateInput from "./BatchTimeUpdateInput.jsx"; +import { EventEditContext, EventEditDispatchContext } from "../businessLogic/context.js"; +import Button from "@common/components/Button.jsx"; + +function BatchTimeUpdater() { + const { startTime, endTime } = useContext(EventEditContext); + const eventDispatch = useContext(EventEditDispatchContext); + const [batchTimeState, batchTimeDispatch] = useReducer( + fcfsBatchControlReducer, + null, + getDefaultState, + ); + + function autoFill() { + eventDispatch({ type: "auto_fill_fcfs", config: getBatchTimeConfig(batchTimeState, {}) }); + } + + return ( +
    + + + +
    + ); +} + +export default BatchTimeUpdater; diff --git a/packages/adminPage/src/features/eventEdit/FcfsInput/FcfsItemInput.jsx b/packages/adminPage/src/features/eventEdit/FcfsInput/FcfsItemInput.jsx new file mode 100644 index 00000000..94825ca5 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/FcfsInput/FcfsItemInput.jsx @@ -0,0 +1,86 @@ +import { useContext } from "react"; +import { EventEditDispatchContext } from "../businessLogic/context.js"; +import { Input } from "@admin/components/SmallInput.jsx"; +import DateInput from "@admin/components/DateInput"; +import DeleteButton from "@admin/components/DeleteButton"; +import { formatDate, padNumber, getDayDifference } from "@common/utils.js"; +import fcfsInputGridStyle from "./tableStyle.js"; +import serverTimeStore from "@admin/serverTime/store.js"; + +const MINUTE = 60; + +function minuteIntToString(min) { + const h = Math.floor(min / MINUTE) % 24; + const m = min % MINUTE; + return `${padNumber(h)}:${padNumber(m)}`; +} + +function strToMinutes(str) { + const [h, m] = str.split(":").map(Number); + return h * MINUTE + m; +} + +function FcfsItemInput({ uniqueKey, date, start, end, participantCount, prizeInfo }) { + const dispatch = useContext(EventEditDispatchContext); + const enableDateEdit = uniqueKey.startsWith("auto_made_"); + + function modify(value) { + dispatch({ type: "modify_fcfs_item", key: uniqueKey, value }); + } + + return ( +
    + {enableDateEdit ? ( +
    {formatDate(date, "M/DD")}
    + ) : ( + { + if (getDayDifference(serverTimeStore.getState().serverTime, date) <= 0) return; + dispatch({ type: "modify_fcfs_item", key: uniqueKey, value: { date } }); + }} + required + size="4" + /> + )} + modify({ start: strToMinutes(value) })} + step="300" + size="12" + /> + modify({ end: strToMinutes(value) })} + step="300" + size="12" + /> + modify({ participantCount: Number.isNaN(+value) ? 0 : +value })} + inputMode="numeric" + pattern="[0-9]+" + size="3" + placeholder="인원" + /> + modify({ prizeInfo: value })} + placeholder="경품 이름 입력" + /> + dispatch({ type: "delete_fcfs_item", key: uniqueKey })} + disabled={uniqueKey.startsWith("determined_")} + /> +
    + ); +} + +export default FcfsItemInput; diff --git a/packages/adminPage/src/features/eventEdit/FcfsInput/index.jsx b/packages/adminPage/src/features/eventEdit/FcfsInput/index.jsx new file mode 100644 index 00000000..eb08091a --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/FcfsInput/index.jsx @@ -0,0 +1,39 @@ +import { useContext } from "react"; +import { EventEditContext, EventEditDispatchContext } from "../businessLogic/context.js"; +import BatchTimeUpdater from "./BatchTimeUpdater.jsx"; +import FcfsItemInput from "./FcfsItemInput.jsx"; +import Button from "@common/components/Button.jsx"; +import fcfsInputGridStyle from "./tableStyle.js"; + +function FcfsInput() { + const { fcfs, startTime, endTime } = useContext(EventEditContext); + const dispatch = useContext(EventEditDispatchContext); + + return ( +
    + +
    +
    +
    날짜
    +
    오픈시간
    +
    종료시간
    +
    당첨자 수
    +
    경품
    +
    삭제
    +
    + {[...fcfs].map((value) => ( + + ))} + +
    +
    + ); +} + +export default FcfsInput; diff --git a/packages/adminPage/src/features/eventEdit/FcfsInput/reducer.js b/packages/adminPage/src/features/eventEdit/FcfsInput/reducer.js new file mode 100644 index 00000000..9e47acc9 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/FcfsInput/reducer.js @@ -0,0 +1,53 @@ +import { DEFAULT_END_TIME } from "../businessLogic/constants.js"; + +const STEP = 5; + +export function getDefaultState() { + return { + start: 0, + end: DEFAULT_END_TIME, + startCheck: false, + endCheck: false, + }; +} + +export function fcfsBatchControlReducer(state, action) { + switch (action.type) { + case "toggle_start_time": + return { ...state, startCheck: action.value }; + case "toggle_end_time": + return { ...state, endCheck: action.value }; + case "set_start_time": { + if (!state.startCheck) return state; + const value = Math.round(action.value / STEP) * STEP; + if (state.end > value) return { ...state, start: value }; + if (state.end - STEP < 0) return { ...state, start: 0, end: STEP }; + return { ...state, start: state.end - STEP }; + } + case "set_end_time": { + if (!state.endCheck) return state; + const value = Math.round(action.value / STEP) * STEP; + if (state.start < value) return { ...state, end: value }; + if (state.start + STEP > DEFAULT_END_TIME) + return { ...state, start: DEFAULT_END_TIME - STEP, end: DEFAULT_END_TIME }; + return { ...state, end: state.start + STEP }; + } + case "apply": { + return action.value; + } + } +} + +export function getToggleBatchTimeConfig(state, type) { + const result = {}; + if (type === "start" && state.startCheck) result.start = state.start; + if (type === "end" && state.endCheck) result.end = state.end; + return result; +} + +export function getBatchTimeConfig(state, prevState) { + const result = {}; + if (state.startCheck && state.start !== prevState.start) result.start = state.start; + if (state.endCheck && state.end !== prevState.end) result.end = state.end; + return result; +} diff --git a/packages/adminPage/src/features/eventEdit/FcfsInput/tableStyle.js b/packages/adminPage/src/features/eventEdit/FcfsInput/tableStyle.js new file mode 100644 index 00000000..6c5f6d5a --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/FcfsInput/tableStyle.js @@ -0,0 +1,4 @@ +const fcfsInputGridStyle = + "grid grid-cols-[3rem_2fr_2fr_4rem_3fr_2rem] gap-4 justify-center items-center"; + +export default fcfsInputGridStyle; diff --git a/packages/adminPage/src/features/eventEdit/businessLogic/DrawGradeData.js b/packages/adminPage/src/features/eventEdit/businessLogic/DrawGradeData.js new file mode 100644 index 00000000..3f3707ef --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/businessLogic/DrawGradeData.js @@ -0,0 +1,36 @@ +class DrawGradeData { + constructor(rawData) { + if (rawData == null) this.data = []; + else this.data = [...rawData].sort((a, b) => a.grade - b.grade); + } + get size() { + return this.data.length; + } + modify(data) { + const newData = new DrawGradeData(this.data); + const key = data.grade - 1; + newData.data[key] = { ...this.data[key], ...data }; + return newData; + } + adjustCount(count) { + if (count < 1) return this; + const originLength = this.data.length; + if (count === originLength) return this; + + const newData = new DrawGradeData(this.data); + if (count > originLength) { + for (let i = originLength; i < count; i++) { + newData.data.push({ grade: i + 1, count: 0, prizeInfo: "" }); + } + } else newData.data.splice(count, Infinity); + return newData; + } + *[Symbol.iterator]() { + yield* this.data; + } + toJSON() { + return this.data; + } +} + +export default DrawGradeData; diff --git a/packages/adminPage/src/features/eventEdit/businessLogic/DrawPolicyData.js b/packages/adminPage/src/features/eventEdit/businessLogic/DrawPolicyData.js new file mode 100644 index 00000000..cc0db675 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/businessLogic/DrawPolicyData.js @@ -0,0 +1,49 @@ +import makeUUID from "./makeUUID.js"; + +class DrawPolicyData { + constructor(rawData) { + if (Array.isArray(rawData)) { + const mapArr = rawData.map(({ id, ...rest }, i) => { + return [ + id === undefined ? `temp_saved_${i}` : `determined_${id}`, + { + id, + ...rest, + }, + ]; + }); + + this.map = new Map(mapArr); + } else if (rawData instanceof Map) this.map = new Map(rawData); + else this.map = new Map(); + } + get size() { + return this.map.size; + } + add(data) { + const newData = new DrawPolicyData(this.map); + newData.map.set(`user_created_${makeUUID()}`, data); + return newData; + } + delete(key) { + const newData = new DrawPolicyData(this.map); + newData.map.delete(key); + return newData; + } + modify(key, data = {}) { + const newData = new DrawPolicyData(this.map); + const oldItem = newData.map.get(key); + newData.map.set(key, { ...oldItem, ...data }); + return newData; + } + *[Symbol.iterator]() { + for (let [key, item] of this.map) { + yield { key, ...item }; + } + } + toJSON() { + return [...this.map.values()]; + } +} + +export default DrawPolicyData; diff --git a/packages/adminPage/src/features/eventEdit/businessLogic/FcfsData.js b/packages/adminPage/src/features/eventEdit/businessLogic/FcfsData.js new file mode 100644 index 00000000..e5c961c0 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/businessLogic/FcfsData.js @@ -0,0 +1,243 @@ +import makeUUID from "./makeUUID.js"; +import { DEFAULT_END_TIME, DEFATLT_PARTICIPANT, STEP } from "./constants.js"; + +const MINUTES = 60 * 1000; +const ONE_DAY = 24 * 60 * MINUTES; + +function extractHourMinutes(date) { + return date.getHours() * 60 + date.getMinutes(); +} + +function extractDate(date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +function verityTime(item, { startTime, endTime, prevSnapshot = {} } = {}) { + const { start, end, date, ...rest } = item; + + let [newStart, newEnd] = [start ?? 0, end ?? DEFAULT_END_TIME].map((e) => Math.round(e / 5) * 5); + + // end가 start보다 더 작으면, 반드시 start가 end보다 더 작도록 치환 + if (newStart >= newEnd) { + if (prevSnapshot.start !== newStart) { + if (newEnd === 0) { + newStart = 0; + newEnd = newEnd + STEP; + } else newStart = newEnd - STEP; + } else { + if (newStart === DEFAULT_END_TIME) { + newStart = DEFAULT_END_TIME - STEP; + newEnd = DEFAULT_END_TIME; + } else newEnd = newStart + STEP; + } + } + + // date를 추가한 타임스탬프를 계산 + const startValue = date.valueOf() + newStart * MINUTES; + const endValue = date.valueOf() + newEnd * MINUTES; + + // 만약 종료 시간이 전체 시작 시간보다 더 빠르거나, 시작 시간이 전체 종료 시간보다 더 늦는다면 전체를 무효화시킴.(제거) + if (endTime <= startValue || startTime >= endValue) return null; + + // 시작시간과 종료시간을 전체 시작 시간에 맞도록 클리핑함. + // 클리핑한 대상의 새로운 시작 시간이 23:55이거나 새로운 종료 시간이 00:00이면 무효한 시간으로 판별.(제거) + if (startValue < startTime) { + newStart = extractHourMinutes(startTime); + if (newStart === DEFAULT_END_TIME) return null; + } + if (endValue > endTime) { + newEnd = extractHourMinutes(endTime); + if (newEnd === 0) return null; + } + + return { + date, + start: newStart, + end: newEnd, + ...rest, + }; +} + +function verifyItem(item, { startTime, endTime, prevSnapshot = {} } = {}) { + const { date, ...rest } = item; + + // 만약 date가 입력 중이라면 item 그대로 반환. date가 빈 값이어도 item 그대로 반환. + if (!date) return item; + + // date를 수정했을 때, date가 시작점/끝점에 맞도록 date를 조정함. + let newDate = date; + const startDate = extractDate(startTime); + const endDate = extractDate(endTime); + + if (date < startDate || date > endDate) { + if (prevSnapshot.date !== null) newDate = prevSnapshot.date; + else if (date < startDate) newDate = startDate; + else newDate = endDate; + } + + const verified = verityTime({ date: newDate, ...rest }, { startTime, endTime, prevSnapshot }); + if (verified === null && prevSnapshot.date === null) return prevSnapshot; + return verified; +} + +function verifyItems(map, { startTime, endTime, prevSnapshot = new Map() }) { + const result = new Map(map); + for (let [key, unstableItem] of map) { + const verifiedValue = verifyItem(unstableItem, { + startTime, + endTime, + prevSnapshot: prevSnapshot.get(key), + }); + if (verifiedValue === null) result.delete(key); + else result.set(key, verifiedValue); + } + return result; +} + +function hasDuplicatedDate(newDate, map) { + if (newDate === undefined) return true; + if (newDate === null) return false; + const dateSet = new Set([...map.values()].map(({ date }) => date?.valueOf() ?? null)); + + if (dateSet.has(newDate.valueOf())) return true; + return false; +} + +function getDefaultFcfsArray( + startTime, + endTime, + config = { start: 0, end: DEFAULT_END_TIME, participantCount: DEFATLT_PARTICIPANT }, +) { + if (startTime === null || endTime === null) return []; + + const TIME_ZONE_OFFSET = new Date().getTimezoneOffset() * MINUTES; + const startDate = Math.floor((startTime.valueOf() - TIME_ZONE_OFFSET) / ONE_DAY); + const endDate = Math.floor((endTime.valueOf() - TIME_ZONE_OFFSET) / ONE_DAY); + + const length = endDate - startDate + 1; + const baseDate = startDate * ONE_DAY + TIME_ZONE_OFFSET; + const result = []; + + for (let i = 0; i < length; i++) { + const rawItem = { + date: new Date(baseDate + i * ONE_DAY), + start: config.start ?? 0, + end: config.end ?? DEFAULT_END_TIME, + participantCount: config.participantCount ?? DEFATLT_PARTICIPANT, + prizeInfo: "", + }; + + const item = verifyItem(rawItem, { startTime, endTime }); + if (item !== null) result.push([`auto_made_${i}`, item]); + } + + return result; +} + +function convertServerDataToClient({ id, startTime, endTime, ...rest }) { + const TIME_ZONE_OFFSET = new Date().getTimezoneOffset() * MINUTES; + const startTimeDate = new Date(startTime); + const endTimeDate = new Date(endTime); + + const startDate = Math.floor((startTimeDate.valueOf() - TIME_ZONE_OFFSET) / ONE_DAY); + const trueStartDate = new Date(startDate * ONE_DAY + TIME_ZONE_OFFSET); + + return { + id, + date: trueStartDate, + start: extractHourMinutes(startTimeDate), + end: extractHourMinutes(endTimeDate), + ...rest, + }; +} + +function convertClientDataToServer({ id, date, start, end, ...rest }) { + const dateBase = date.valueOf(); + return { + id, + startTime: new Date(dateBase + start * MINUTES), + endTime: new Date(dateBase + end * MINUTES), + ...rest, + }; +} + +class FcfsData { + constructor(rawData) { + if (Array.isArray(rawData)) { + const mapArr = rawData.map((item, i) => { + return [ + item.id === undefined ? `temp_saved_${i}` : `determined_${item.id}`, + convertServerDataToClient(item), + ]; + }); + + this.map = new Map(mapArr); + } else if (rawData instanceof Map) this.map = new Map(rawData); + else this.map = new Map(); + } + static fillDefault(startTime, endTime, config) { + const mapArr = getDefaultFcfsArray(startTime, endTime, config); + return new FcfsData(new Map(mapArr)); + } + get size() { + return this.map.size; + } + add( + data = { + date: null, + start: 0, + end: DEFAULT_END_TIME, + participantCount: DEFATLT_PARTICIPANT, + prizeInfo: "", + }, + ) { + const newData = new FcfsData(this.map); + newData.map.set(`user_created_${makeUUID()}`, data); + return newData; + } + delete(key) { + const newData = new FcfsData(this.map); + newData.map.delete(key); + return newData; + } + modify(key, data, { startTime, endTime }) { + const newData = new FcfsData(this.map); + const oldItem = newData.map.get(key); + + const verified = verifyItem( + { ...oldItem, ...data }, + { startTime, endTime, prevSnapshot: oldItem }, + ); + if (verified === null) newData.map.delete(key); + else { + if (hasDuplicatedDate(verified.date, this.map)) verified.date = oldItem.date; + + newData.map.set(key, verified); + } + return newData; + } + modifyAll(data, { startTime, endTime }) { + const newData = new FcfsData(this.map); + for (let [key, value] of this.map) { + newData.map.set(key, { ...value, ...data }); + } + newData.map = verifyItems(newData.map, { startTime, endTime, prevSnapshot: this.map }); + return newData; + } + verifyDate(startTime, endTime) { + const verifiedMap = verifyItems(this.map, { startTime, endTime, prevSnapshot: this.map }); + + const newData = new FcfsData(verifiedMap); + return newData; + } + *[Symbol.iterator]() { + for (let [uniqueKey, item] of this.map) { + yield { uniqueKey, ...item }; + } + } + toJSON() { + return [...this.map.values()].map(convertClientDataToServer); + } +} + +export default FcfsData; diff --git a/packages/adminPage/src/features/eventEdit/businessLogic/constants.js b/packages/adminPage/src/features/eventEdit/businessLogic/constants.js new file mode 100644 index 00000000..9ef9dd1f --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/businessLogic/constants.js @@ -0,0 +1,4 @@ +export const DEFAULT_END_TIME = 1435; +export const DEFATLT_PARTICIPANT = 100; +export const MAX_GRADE = 10; +export const STEP = 5; diff --git a/packages/adminPage/src/features/eventEdit/businessLogic/context.js b/packages/adminPage/src/features/eventEdit/businessLogic/context.js new file mode 100644 index 00000000..0dc1c7e4 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/businessLogic/context.js @@ -0,0 +1,6 @@ +import { createContext } from "react"; +import { setDefaultState } from "./reducer.js"; + +export const EventEditContext = createContext(setDefaultState()); +export const EventEditDispatchContext = createContext(() => console.error("context 지정 안 됨")); +export const EventEditModeContext = createContext("create"); diff --git a/packages/adminPage/src/features/eventEdit/businessLogic/makeUUID.js b/packages/adminPage/src/features/eventEdit/businessLogic/makeUUID.js new file mode 100644 index 00000000..41a569c3 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/businessLogic/makeUUID.js @@ -0,0 +1,10 @@ +function randInt(i) { + return Math.floor(Math.random() * i); +} + +export default function makeUUID() { + const rawStr = Array.from({ length: 32 }, () => randInt(16).toString(16)).join(""); + const checksum = (randInt(4) + 8).toString(16); + + return `${rawStr.slice(0, 8)}-${rawStr.slice(8, 12)}-4${rawStr.slice(13, 16)}-${checksum}${rawStr.slice(17, 20)}-${rawStr.slice(20)}`; +} diff --git a/packages/adminPage/src/features/eventEdit/businessLogic/reducer.js b/packages/adminPage/src/features/eventEdit/businessLogic/reducer.js new file mode 100644 index 00000000..22b92893 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/businessLogic/reducer.js @@ -0,0 +1,173 @@ +import FcfsData from "./FcfsData.js"; +import DrawGradeData from "./DrawGradeData.js"; +import DrawPolicyData from "./DrawPolicyData.js"; +import { MAX_GRADE } from "./constants.js"; + +function makeVoidDrawData() { + return { + metadata: new DrawGradeData(), + policies: new DrawPolicyData(), + }; +} + +function makeDefaultDrawData() { + return { + metadata: new DrawGradeData([{ grade: 1, count: 0, prizeInfo: "" }]), + policies: new DrawPolicyData(), + }; +} + +function makeDrawData(rawData) { + return { + id: rawData.id, + metadata: new DrawGradeData(rawData.metadata), + policies: new DrawPolicyData(rawData.policies), + }; +} + +export function setDefaultState(defaultState) { + if (defaultState === null || defaultState === undefined) { + return { + name: "", + description: "", + startTime: null, + endTime: null, + url: "", + eventType: "fcfs", + eventFrameId: "", + fcfs: new FcfsData(), + draw: makeVoidDrawData(), + }; + } + + const tempState = { ...defaultState }; + + if (tempState.startTime !== null) tempState.startTime = new Date(tempState.startTime); + if (tempState.endTime !== null) tempState.endTime = new Date(tempState.endTime); + if (tempState.eventType === "fcfs") { + tempState.fcfs = new FcfsData(defaultState.fcfs); + tempState.draw = makeVoidDrawData(); + } + if (tempState.eventType === "draw") { + tempState.fcfs = new FcfsData(); + tempState.draw = makeDrawData(defaultState.draw); + } + + return tempState; +} + +export function eventEditReducer(state, action) { + switch (action.type) { + case "set_name": + return { ...state, name: action.value }; + case "set_description": + return { ...state, description: action.value }; + case "set_start_date": { + const newState = { ...state, startTime: action.value }; + if (state.eventType === "fcfs") + newState.fcfs = state.fcfs.verifyDate(action.value, state.endTime); + return newState; + } + case "set_end_date": { + const newState = { ...state, endTime: action.value }; + if (state.eventType === "fcfs") + newState.fcfs = state.fcfs.verifyDate(state.startTime, action.value); + return newState; + } + case "set_date_range": { + const newState = { + ...state, + startTime: action.value[0], + endTime: action.value[1], + }; + if (state.eventType === "fcfs") newState.fcfs = state.fcfs.verifyDate(...action.value); + return newState; + } + case "set_url": + return { ...state, url: action.value }; + case "set_event_type": + if (action.value === "draw") { + return { ...state, eventType: "draw", draw: makeDefaultDrawData(), fcfs: new FcfsData() }; + } + return { ...state, eventType: "fcfs", draw: makeVoidDrawData() }; + case "set_event_frame": + return { ...state, eventFrameId: action.value }; + case "auto_fill_fcfs": + if (state.eventType === "draw") return state; + return { + ...state, + fcfs: FcfsData.fillDefault(state.startTime, state.endTime, action.config), + }; + case "add_fcfs_item": + if (state.eventType === "draw") return state; + if (state.startTime === null || state.endTime === null) return state; + return { ...state, fcfs: state.fcfs.add(action.value) }; + case "delete_fcfs_item": + if (state.eventType === "draw") return state; + return { ...state, fcfs: state.fcfs.delete(action.key) }; + case "modify_fcfs_item": { + if (state.eventType === "draw") return state; + const { startTime, endTime } = state; + return { + ...state, + fcfs: state.fcfs.modify(action.key, action.value, { startTime, endTime }), + }; + } + case "modify_all_fcfs_item": { + if (state.eventType === "draw") return state; + const { startTime, endTime } = state; + return { + ...state, + fcfs: state.fcfs.modifyAll(action.value, { startTime, endTime }), + }; + } + case "modify_draw_total_grade": + if (state.eventType === "fcfs") return state; + if (action.value > MAX_GRADE || action.value < 0) return state; + return { + ...state, + draw: { + ...state.draw, + metadata: state.draw.metadata.adjustCount(action.value), + }, + }; + case "modify_draw_grade_item": + if (state.eventType === "fcfs") return state; + return { + ...state, + draw: { + ...state.draw, + metadata: state.draw.metadata.modify(action.value), + }, + }; + case "add_draw_policy": + if (state.eventType === "fcfs") return state; + return { + ...state, + draw: { + ...state.draw, + policies: state.draw.policies.add({ action: "", score: 0 }), + }, + }; + case "delete_draw_policy": + if (state.eventType === "fcfs") return state; + return { + ...state, + draw: { + ...state.draw, + policies: state.draw.policies.delete(action.key), + }, + }; + case "modify_draw_policy": + if (state.eventType === "fcfs") return state; + return { + ...state, + draw: { + ...state.draw, + policies: state.draw.policies.modify(action.key, action.value), + }, + }; + case "apply_external_data": + return setDefaultState(action.value); + } +} diff --git a/packages/adminPage/src/features/eventEdit/index.jsx b/packages/adminPage/src/features/eventEdit/index.jsx new file mode 100644 index 00000000..eae8eba8 --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/index.jsx @@ -0,0 +1,214 @@ +import { useReducer, useContext } from "react"; +import { useNavigate } from "react-router-dom"; +import { eventEditReducer, setDefaultState } from "./businessLogic/reducer.js"; +import { + EventEditContext, + EventEditDispatchContext, + EventEditModeContext, +} from "./businessLogic/context.js"; + +import EventBaseDataInput from "./EventBaseDataInput.jsx"; +import EventDetailInput from "./EventDetailInput.jsx"; +import TitleContainer from "@admin/components/TitleContainer.jsx"; +import Button from "@common/components/Button.jsx"; +import openModal from "@common/modal/openModal.js"; +import AlertModal from "@admin/modals/AlertModal.jsx"; +import ConfirmModal from "@admin/modals/ConfirmModal.jsx"; + +import { useMutation } from "@common/dataFetch/getQuery.js"; +import { fetchServer, handleError, HTTPError } from "@common/dataFetch/fetchServer.js"; + +const tempLoadErrorHandler = { + 401: "인증되지 않은 사용자입니다.", + 404: "임시저장된 데이터가 없습니다.", +}; + +function handleEventSubmitError(e) { + if (e instanceof HTTPError) { + if (e.status === 400) { + e.response.json().then((value) => { + console.log(value); + openModal( + + 사용자 입력이 잘못되었습니다. +
    + {JSON.stringify(value, null, 2)} + + } + />, + ); + }); + } else if (e.status === 401) { + openModal(); + } else if (e.status < 500) { + openModal( + + 클라이언트의 오류가 발생했습니다. +
    + 에러 코드 : {e.status} + + } + />, + ); + } else { + openModal( + + 서버의 오류가 발생했습니다. +
    + 에러 코드 : {e.status} + + } + />, + ); + } + } else { + openModal(); + } +} + +function EventEditor({ initialData = null } = {}) { + const navigate = useNavigate(); + const mode = useContext(EventEditModeContext); + const [state, dispatch] = useReducer(eventEditReducer, initialData, setDefaultState); + const submitMutate = useMutation( + mode === "create" ? "admin-event-list" : `admin-event-list/${state.eventId}`, + () => + fetchServer(mode === "create" ? "/api/v1/admin/events" : "/api/v1/admin/events/edit", { + method: "post", + body: state, + }), + { + onSuccess: () => { + openModal( + , + ).then(() => navigate(mode === "create" ? "/events" : `/events/${state.eventId}`)); + }, + onError: handleEventSubmitError, + }, + ); + + const submitDisabled = + (state.eventType === "fcfs" && state.fcfs.size === 0) || + (state.eventType === "draw" && state.draw.policies.size === 0); + + function onSubmit(e) { + e.preventDefault(); + if (submitDisabled) return; + submitMutate(); + } + + async function submitTempSave() { + try { + await fetchServer("/api/v1/admin/events/temp", { + method: "post", + body: state, + }); + openModal(); + } catch (e) { + handleEventSubmitError(e); + } + } + + async function applyTempSave() { + try { + const data = await fetchServer("/api/v1/admin/events/temp").catch( + handleError(tempLoadErrorHandler), + ); + if ( + data.eventId === state.eventId || + (state.eventId === undefined && !data.eventId.startsWith("HD")) + ) { + dispatch({ type: "apply_external_data", value: data }); + } else { + openModal( + + 임시저장된 데이터가 현재 작성 중인 데이터의 것이 아닙니다! +
    + 임시저장 ID : {data.eventId || "새로 생성될 이벤트"} + + } + />, + ); + } + } catch (e) { + openModal(); + } + } + + const tempSaveConfirmModal = ( + + ); + const tempLoadConfirmModal = ( + + 임시저장된 내용을 불러오겠습니까? +
    + 작성 중인 내용은 대체됩니다. + + } + onConfirm={applyTempSave} + /> + ); + + return ( +
    + +
    +

    + {mode === "edit" ? "이벤트 수정" : "이벤트 등록"} +

    +

    + *는 필수 입력 +

    +
    +
    + + + +
    +
    +
    + + + +
    + + 이벤트 종류* + + +
    +
    +
    +
    +
    + ); +} + +export default EventEditor; diff --git a/packages/adminPage/src/features/eventEdit/mock.js b/packages/adminPage/src/features/eventEdit/mock.js new file mode 100644 index 00000000..3599633c --- /dev/null +++ b/packages/adminPage/src/features/eventEdit/mock.js @@ -0,0 +1,103 @@ +import { http, HttpResponse } from "msw"; +import { makeLorem } from "@common/mock/utils.js"; + +function getEventsDetailMock() { + return new Map( + Array.from({ length: 100 }, (_, i) => { + const startTime = new Date( + Date.now() - 86400 * 120 * 1000 + Math.floor(Math.random() * 86400 * 60 * 1000), + ); + const endTime = new Date(startTime.getTime() + Math.floor(Math.random() * 86400 * 10) * 1000); + const eventType = Math.random() > 0.5 ? "fcfs" : "draw"; + const eventId = `HD_240808_${i.toString().padStart(3, "0")}`; + + const result = { + name: makeLorem(3, 7), + description: makeLorem(5, 10), + eventType, + startTime, + endTime, + url: "https://www.naver.com/", + eventFrameId: "the-new-ioniq-5", + eventId, + }; + + if (eventType === "fcfs") { + const sliceStartTime = new Date(startTime); + sliceStartTime.setHours(0); + sliceStartTime.setMinutes(0); + sliceStartTime.setSeconds(0); + sliceStartTime.setMilliseconds(0); + result.fcfs = [ + { + id: 0, + startTime: sliceStartTime, + endTime: new Date(sliceStartTime.valueOf() + 900 * 60 * 1000), + participantCount: 120, + prizeInfo: "string", + }, + ]; + } else { + result.draw = { + id: 0, + policies: [ + { + id: 0, + action: "WriteComment", + score: 10, + }, + ], + metadata: [ + { + id: 0, + grade: 1, + count: 10, + prizeInfo: "자동차 세트", + }, + { + id: 1, + grade: 2, + count: 100, + prizeInfo: "미니 선풍기", + }, + ], + }; + } + + return [eventId, result]; + }), + ); +} + +const dummyData = getEventsDetailMock(); +let tempData = null; + +const handlers = [ + http.post("/api/v1/admin/events", async ({ request }) => { + const data = await request.json(); + if (data.description === "") + return HttpResponse.json({ description: "디스크립션이 없습니다." }, { status: 400 }); + tempData = null; + return new HttpResponse(null, { status: 201 }); + }), + http.post("/api/v1/admin/events/temp", async ({ request }) => { + tempData = await request.json(); + return new HttpResponse(null, { status: 201 }); + }), + http.get("/api/v1/admin/events/temp", () => { + if (tempData === null) return HttpResponse.json(null, { status: 404 }); + return HttpResponse.json(tempData); + }), + http.get("/api/v1/admin/events/:id", ({ params }) => { + if (!dummyData.has(params.id)) return HttpResponse.json(null, { status: 404 }); + return HttpResponse.json(dummyData.get(params.id)); + }), + http.post("/api/v1/admin/events/edit", async ({ request }) => { + const data = await request.json(); + dummyData.set(data.eventId, data); + tempData = null; + return new HttpResponse(null, { status: 200 }); + }), +]; + +export default handlers; diff --git a/packages/adminPage/src/features/eventList/DeleteButton.jsx b/packages/adminPage/src/features/eventList/DeleteButton.jsx new file mode 100644 index 00000000..8c35224c --- /dev/null +++ b/packages/adminPage/src/features/eventList/DeleteButton.jsx @@ -0,0 +1,66 @@ +import { fetchServer, HTTPError } from "@common/dataFetch/fetchServer.js"; +import { useMutation } from "@common/dataFetch/getQuery.js"; +import ConfirmModal from "@admin/modals/ConfirmModal.jsx"; +import AlertModal from "@admin/modals/AlertModal.jsx"; +import Button from "@common/components/Button.jsx"; +import openModal from "@common/modal/openModal.js"; + +function DeleteButton({ selected, reset }) { + const mutate = useMutation( + "admin-event-list", + () => + fetchServer("/api/v1/admin/events", { + method: "delete", + body: { + eventIds: [...selected], + }, + }), + { + onSuccess: () => { + openModal(); + reset(); + }, + onError: async (e) => { + if (e instanceof HTTPError && e.status === 400) { + return openModal( + , + ); + } + if (e instanceof HTTPError && e.status === 404) { + return openModal(); + } + return openModal(); + }, + }, + ); + const deleteConfirmModal = ( + + 이 동작은 다시 돌이킬 수 없습니다. +
    + + {selected.keys().next().value} + {selected.size > 1 && ` 외 ${selected.size - 1} 개의`} 이벤트를 삭제하시겠습니까? + + + } + onConfirm={mutate} + /> + ); + + function onClick() { + openModal(deleteConfirmModal); + } + return ( + + ); +} + +export default DeleteButton; diff --git a/packages/adminPage/src/features/eventList/Filter.jsx b/packages/adminPage/src/features/eventList/Filter.jsx new file mode 100644 index 00000000..9bcb9bd6 --- /dev/null +++ b/packages/adminPage/src/features/eventList/Filter.jsx @@ -0,0 +1,55 @@ +import Checkbox from "@common/components/Checkbox.jsx"; + +function Filter({ state, dispatch }) { + function changeFilter(target) { + return (value) => dispatch({ type: "set_filter", target, value }); + } + + const fieldsetStyle = "border border-neutral-200 rounded px-3 pb-3"; + const labelStyle = "inline-flex items-center gap-1 text-body-s"; + + return ( +
    + {/*
    + 상태 +
    + + + + + +
    +
    */} +
    + 종류 +
    + + +
    +
    +
    + ); +} + +export default Filter; diff --git a/packages/adminPage/src/features/eventList/SearchBar.jsx b/packages/adminPage/src/features/eventList/SearchBar.jsx new file mode 100644 index 00000000..807e4bdc --- /dev/null +++ b/packages/adminPage/src/features/eventList/SearchBar.jsx @@ -0,0 +1,29 @@ +import { useState } from "react"; +import Input from "@common/components/Input.jsx"; +import search from "./assets/search.svg"; + +function SearchBar({ onSearch = () => {} }) { + const [query, setQuery] = useState(""); + + return ( +
    { + e.preventDefault(); + onSearch(query); + }} + > + + +
    + ); +} + +export default SearchBar; diff --git a/packages/adminPage/src/features/eventList/assets/downArrow.svg b/packages/adminPage/src/features/eventList/assets/downArrow.svg new file mode 100644 index 00000000..0bb1e163 --- /dev/null +++ b/packages/adminPage/src/features/eventList/assets/downArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/adminPage/src/features/eventList/assets/search.svg b/packages/adminPage/src/features/eventList/assets/search.svg new file mode 100644 index 00000000..49586b48 --- /dev/null +++ b/packages/adminPage/src/features/eventList/assets/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/adminPage/src/features/eventList/assets/upArrow.svg b/packages/adminPage/src/features/eventList/assets/upArrow.svg new file mode 100644 index 00000000..f3a4cd70 --- /dev/null +++ b/packages/adminPage/src/features/eventList/assets/upArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/adminPage/src/features/eventList/checkReducer.js b/packages/adminPage/src/features/eventList/checkReducer.js new file mode 100644 index 00000000..9837918d --- /dev/null +++ b/packages/adminPage/src/features/eventList/checkReducer.js @@ -0,0 +1,24 @@ +function checkReducer(state, action) { + switch (action.type) { + case "reset": + return new Set(); + case "check_key": { + const newSet = new Set(state); + if (action.value === true) newSet.add(action.key); + else if (action.value === false) newSet.delete(action.key); + else if (state.has(action.key)) newSet.delete(action.key); + else newSet.add(action.key); + return newSet; + } + case "toggle_keys": { + const newSet = new Set(state); + let allFalse = action.keys.every((key) => state.has(key) === false); + if (allFalse) action.keys.forEach((key) => newSet.add(key)); + else action.keys.forEach((key) => newSet.delete(key)); + return newSet; + } + } + throw Error("unknown action."); +} + +export default checkReducer; diff --git a/packages/adminPage/src/features/eventList/index.jsx b/packages/adminPage/src/features/eventList/index.jsx new file mode 100644 index 00000000..ae2caadd --- /dev/null +++ b/packages/adminPage/src/features/eventList/index.jsx @@ -0,0 +1,53 @@ +import { useReducer, useDeferredValue } from "react"; +import { Link } from "react-router-dom"; + +import { searchReducer, setDefaultState, searchStateToQuery } from "./queryReducer.js"; +import checkReducer from "./checkReducer.js"; + +import SearchBar from "./SearchBar.jsx"; +import Filter from "./Filter.jsx"; +import SearchResult from "./table"; +import DeleteButton from "./DeleteButton.jsx"; +import Button from "@common/components/Button.jsx"; +import Suspense from "@common/components/Suspense.jsx"; +import ErrorBoundary from "@common/components/ErrorBoundary.jsx"; + +function EventList() { + const [state, dispatch] = useReducer(searchReducer, null, setDefaultState); + const [checkSet, setCheck] = useReducer(checkReducer, new Set()); + const query = useDeferredValue(searchStateToQuery(state)); + const resetCheck = () => setCheck({ type: "reset" }); + + return ( +
    +
    + + + +
    + { + dispatch({ type: "set_query", value }); + resetCheck(); + }} + /> + +
    + +
    + Error
    }> + login}> + + + + + ); +} + +export default EventList; diff --git a/packages/adminPage/src/features/eventList/mock.js b/packages/adminPage/src/features/eventList/mock.js new file mode 100644 index 00000000..caecd856 --- /dev/null +++ b/packages/adminPage/src/features/eventList/mock.js @@ -0,0 +1,116 @@ +import { http, HttpResponse } from "msw"; +import { makeLorem } from "@common/mock/utils.js"; + +function getEventsMock() { + return Array.from({ length: 100 }, (_, i) => { + const startTime = new Date( + Date.now() - 86400 * 30 * 1000 + Math.floor(Math.random() * 86400 * 60 * 1000), + ); + const endTime = new Date(startTime.getTime() + Math.floor(Math.random() * 86400 * 120) * 1000); + + return { + name: makeLorem(3, 7), + eventType: Math.random() > 0.5 ? "fcfs" : "draw", + startTime, + endTime, + eventId: `HD_240808_${i.toString().padStart(3, "0")}`, + }; + }); +} + +const dummyData = getEventsMock(); + +function filterData(filterParam) { + const filterKey = filterParam.split(","); + return function (data) { + if (filterKey.length === 0) return true; + for (let key of filterKey) { + if (key === "fcfs" && data.eventType === "fcfs") return true; + if (key === "draw" && data.eventType === "draw") return true; + } + return false; + }; +} + +function compareString(a, b) { + if (a < b) return -1; + if (a > b) return 1; + return 0; +} + +function sortData(sortParam) { + const sortKey = sortParam.split(",").map((keyValue) => keyValue.split(":")); + return function (a, b) { + for (let [key, sorter] of sortKey) { + const pm = sorter === "desc" ? -1 : 1; + if (key === "eventId") { + const compared = compareString(a.eventId, b.eventId) * pm; + if (compared !== 0) return compared; + } + if (key === "name") { + const compared = compareString(a.name, b.name) * pm; + if (compared !== 0) return compared; + } + if (key === "startTime") { + const compared = (a.startTime - b.startTime) * pm; + if (compared !== 0) return compared; + } + if (key === "endTime") { + const compared = (a.endTime - b.endTime) * pm; + if (compared !== 0) return compared; + } + if (key === "eventType") { + const compared = compareString(a.eventType, b.eventType) * pm; + if (compared !== 0) return compared; + } + } + return 0; + }; +} + +const handlers = [ + http.get("/api/v1/admin/events", async ({ request }) => { + const url = new URL(request.url); + const search = url.searchParams.get("search"); + const filter = url.searchParams.get("type"); + const sort = url.searchParams.get("sort"); + const page = +url.searchParams.get("page") ?? 1; + const size = +url.searchParams.get("size") ?? 5; + + const filteredData = dummyData + .filter(({ name }) => (search === null ? true : name.includes(search))) + .filter(filterData(filter)) + .sort(sortData(sort)); + + const contents = filteredData.slice(page * size, (page + 1) * size); + + return HttpResponse.json({ + contents, + totalPages: Math.ceil(filteredData.length / size), + number: page, + size, + }); + }), + http.delete("/api/v1/admin/events/:id", async ({ params }) => { + const { id } = params; + + const index = dummyData.findIndex(({ eventId }) => eventId === id); + if (index === -1) return HttpResponse.json(false); + dummyData.splice(index, 1); + + return HttpResponse.json(true); + }), + http.delete("/api/v1/admin/events", async ({ request }) => { + const { eventIds } = await request.json(); + + for (let id of eventIds) { + const index = dummyData.findIndex(({ eventId }) => eventId === id); + if (index === -1) continue; + dummyData.splice(index, 1); + } + + return HttpResponse.json(true); + }), +]; + +export default handlers; diff --git a/packages/adminPage/src/features/eventList/queryReducer.js b/packages/adminPage/src/features/eventList/queryReducer.js new file mode 100644 index 00000000..4ab90687 --- /dev/null +++ b/packages/adminPage/src/features/eventList/queryReducer.js @@ -0,0 +1,65 @@ +export function setDefaultState() { + return { + query: "", + filter: { + fcfs: true, + draw: true, + }, + sort: { + eventId: "none", + name: "none", + startTime: "none", + endTime: "none", + eventType: "none", + }, + page: 1, + }; +} + +export function searchReducer(state, action) { + switch (action.type) { + case "set_query": + return { ...state, query: action.value }; + case "set_filter": + return { + ...state, + filter: { ...state.filter, [action.target]: !!action.value }, + }; + case "set_sort": + return { + ...state, + sort: { + ...state.sort, + [action.target]: + action.value === "asc" || action.value === "desc" ? action.value : "none", + }, + }; + case "set_page": + return { + ...state, + page: Number.isNaN(+action.value) ? 1 : +action.value, + }; + } + throw Error("unknown action."); +} + +export function searchStateToQuery(state) { + const path = "/api/v1/admin/events"; + const paramObj = { + search: state.query, + type: Object.entries(state.filter) + .filter(([, value]) => value) + .map(([key]) => key) + .join(","), + sort: Object.entries(state.sort) + .filter(([, value]) => value !== "none") + .map(([key, value]) => `${key}:${value}`) + .join(","), + page: state.page - 1, + size: 10, + }; + if (state.query === "") delete paramObj.search; + + const searchParams = new URLSearchParams(paramObj); + return `${path}?${searchParams.toString()}`; +} diff --git a/packages/adminPage/src/features/eventList/table/SearchResultBody.jsx b/packages/adminPage/src/features/eventList/table/SearchResultBody.jsx new file mode 100644 index 00000000..7c3a8495 --- /dev/null +++ b/packages/adminPage/src/features/eventList/table/SearchResultBody.jsx @@ -0,0 +1,18 @@ +import SearchResultItem from "./SearchResultItem.jsx"; + +function SearchResultBody({ data, checkState, setCheck }) { + return ( +
    + {data.map((item) => ( + + ))} +
    + ); +} + +export default SearchResultBody; diff --git a/packages/adminPage/src/features/eventList/table/SearchResultItem.jsx b/packages/adminPage/src/features/eventList/table/SearchResultItem.jsx new file mode 100644 index 00000000..b5c010b8 --- /dev/null +++ b/packages/adminPage/src/features/eventList/table/SearchResultItem.jsx @@ -0,0 +1,44 @@ +import { Link } from "react-router-dom"; + +import tableTemplateCol from "./tableStyle.js"; +import { useEventStatus } from "@admin/serverTime/EventStatus.js"; +import Button from "@common/components/Button.jsx"; +import Checkbox from "@common/components/Checkbox.jsx"; +import { formatDate } from "@common/utils.js"; + +function SearchResultItem({ eventId, name, startTime, endTime, eventType, checked, setCheck }) { + const eventStatus = useEventStatus(startTime, endTime); + + return ( + + ); +} + +export default SearchResultItem; diff --git a/packages/adminPage/src/features/eventList/table/TableHeader.jsx b/packages/adminPage/src/features/eventList/table/TableHeader.jsx new file mode 100644 index 00000000..50aeb618 --- /dev/null +++ b/packages/adminPage/src/features/eventList/table/TableHeader.jsx @@ -0,0 +1,44 @@ +import tableTemplateCol from "./tableStyle.js"; +import TableSorter from "./TableSorter.jsx"; + +const headerData = [ + { key: "eventId", name: "ID" }, + { key: "name", name: "이벤트 명" }, + { key: "startTime", name: "이벤트 오픈시간" }, + { key: "endTime", name: "이벤트 종료시간" }, + { key: "eventType", name: "종류" }, +]; + +function TableHeader({ state, dispatch, checkSelect }) { + function changeSort(target) { + return (value) => dispatch({ type: "set_sort", target, value }); + } + + return ( +
    + + {headerData.map(({ key, name }) => ( + + {name} + + ))} +
    상태
    +
    상세
    +
    + ); +} + +export default TableHeader; diff --git a/packages/adminPage/src/features/eventList/table/TableSorter.jsx b/packages/adminPage/src/features/eventList/table/TableSorter.jsx new file mode 100644 index 00000000..4a3714e1 --- /dev/null +++ b/packages/adminPage/src/features/eventList/table/TableSorter.jsx @@ -0,0 +1,28 @@ +import ascArrow from "../assets/upArrow.svg"; +import descArrow from "../assets/downArrow.svg"; + +function TableSorter({ className, children, state, setState }) { + function onClick() { + if (state === "asc") return setState("desc"); + if (state === "desc") return setState("none"); + return setState("asc"); + } + return ( + + ); +} + +export default TableSorter; diff --git a/packages/adminPage/src/features/eventList/table/index.jsx b/packages/adminPage/src/features/eventList/table/index.jsx new file mode 100644 index 00000000..01904a6e --- /dev/null +++ b/packages/adminPage/src/features/eventList/table/index.jsx @@ -0,0 +1,44 @@ +import TableHeader from "./TableHeader.jsx"; +import SearchResultBody from "./SearchResultBody.jsx"; +import Pagination from "@admin/components/Pagination.jsx"; + +import { useQuery } from "@common/dataFetch/getQuery.js"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import serverTimeStore from "@admin/serverTime/store.js"; + +function SearchResult({ query, queryState, queryDispatch, checkState, checkDispatch }) { + const { contents, totalPages } = useQuery(`admin-event-list@${query}`, () => fetchServer(query), { + deferred: true, + }); + + const checkSelect = () => { + const keys = contents + .filter(({ startTime }) => { + return new Date(startTime) > serverTimeStore.getState().serverTime; + }) + .map(({ eventId }) => eventId); + checkDispatch({ type: "toggle_keys", keys }); + }; + + return ( + <> + + { + return (value) => checkDispatch({ type: "check_key", key, value }); + }} + /> +
    + queryDispatch({ type: "set_page", value })} + maxPage={totalPages} + /> +
    + + ); +} + +export default SearchResult; diff --git a/packages/adminPage/src/features/eventList/table/tableStyle.js b/packages/adminPage/src/features/eventList/table/tableStyle.js new file mode 100644 index 00000000..55d9cba8 --- /dev/null +++ b/packages/adminPage/src/features/eventList/table/tableStyle.js @@ -0,0 +1,3 @@ +const tableTemplateCol = `grid grid-cols-[0.5fr_1.5fr_3fr_2fr_2fr_0.5fr_0.5fr_1fr]`; + +export default tableTemplateCol; diff --git a/packages/adminPage/src/features/users/Loading.jsx b/packages/adminPage/src/features/users/Loading.jsx new file mode 100644 index 00000000..40e4ff40 --- /dev/null +++ b/packages/adminPage/src/features/users/Loading.jsx @@ -0,0 +1,9 @@ +import Spinner from "@common/components/Spinner"; + +export default function Loading() { + return ( +
    + +
    + ); +} diff --git a/packages/adminPage/src/features/users/Users.jsx b/packages/adminPage/src/features/users/Users.jsx new file mode 100644 index 00000000..69c5f6bd --- /dev/null +++ b/packages/adminPage/src/features/users/Users.jsx @@ -0,0 +1,41 @@ +import { useQuery } from "@common/dataFetch/getQuery.js"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import Pagination from "@admin/components/Pagination"; +import { useState } from "react"; + +export default function Comments({ searchParams }) { + const [page, setPage] = useState(1); + const data = useQuery( + "admin-users", + () => + fetchServer( + `/api/v1/admin/event-users?page=${page - 1}&search=${searchParams.get("search") ?? ""}&field=${searchParams.get("field") ?? "userName"}&size=15`, + ) + .then((res) => { + return res; + }) + .catch((e) => { + alert("통신 오류로 유저 목록 로드 실패."); + console.log(e); + return { users: [] }; + }), + [page, searchParams], + ); + + return ( +
    + {data.users.map((user) => ( +
    + {user.userName} + {user.phoneNumber} + {user.frameId} +
    + ))} + + +
    + ); +} diff --git a/packages/adminPage/src/features/users/index.jsx b/packages/adminPage/src/features/users/index.jsx new file mode 100644 index 00000000..7705508c --- /dev/null +++ b/packages/adminPage/src/features/users/index.jsx @@ -0,0 +1,69 @@ +import Suspense from "@common/components/Suspense"; +import Loading from "./Loading.jsx"; +import Users from "./Users.jsx"; +import { useState } from "react"; +import { useSearchParams } from "react-router-dom"; + +export default function AdminCommentID() { + const [formString, setFormString] = useState(""); + const [searchString, setSearchString] = useState(""); + const [category, setCategory] = useState("userName"); + + const [searchParams, setSearchParams] = useSearchParams(); + + function searchComment(e) { + e.preventDefault(); + setSearchString(formString); + setSearchParams({ search: formString, field: category }); + } + + return ( +
    +
    + 검색 문자열: + {searchString} +
    + +
    + setFormString(e.target.value)} + placeholder="유저 성명 검색" + className="bg-neutral-50 focus:bg-white w-full px-4 py-2 rounded-lg text-body-s" + /> + +
    + + + 검색 +
    +
    + +
    + 성명 + 전화번호 + 이벤트 frameId +
    + + }> + + +
    + ); +} diff --git a/packages/adminPage/src/features/users/mock.js b/packages/adminPage/src/features/users/mock.js new file mode 100644 index 00000000..7005b05d --- /dev/null +++ b/packages/adminPage/src/features/users/mock.js @@ -0,0 +1,23 @@ +import { http, HttpResponse } from "msw"; +import getRandomString from "@common/mock/getRandomString"; + +function getRandomUsers() { + let users = []; + const num = 15; + for (let i = 0; i < num; i++) { + users = [ + ...users, + { + id: i, + userName: getRandomString(3), + phoneNumber: "010-0000-0000", + frameId: "event-test", + }, + ]; + } + return { users: users, totalPage: 15 }; +} + +const handlers = [http.get("/api/v1/admin/event-users", () => HttpResponse.json(getRandomUsers()))]; + +export default handlers; diff --git a/packages/adminPage/src/index.css b/packages/adminPage/src/index.css new file mode 100644 index 00000000..0df6a469 --- /dev/null +++ b/packages/adminPage/src/index.css @@ -0,0 +1,23 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@font-face { + font-family: "hdsans"; + src: + url("/font/HyundaiSansTextKROTFMedium.woff2") format("woff2"), + url("/font/HyundaiSansTextKROTFMedium.woff") format("woff"); + font-weight: 500; + font-display: swap; +} + +@layer base { + body { + font-family: "hdsans", sans-serif; + } + body.scrollLocked { + position: fixed; + width: 100%; + overflow-y: auto; + } +} diff --git a/packages/adminPage/src/main-client.jsx b/packages/adminPage/src/main-client.jsx new file mode 100644 index 00000000..a470ab26 --- /dev/null +++ b/packages/adminPage/src/main-client.jsx @@ -0,0 +1,37 @@ +import { StrictMode } from "react"; +import { createRoot, hydrateRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App.jsx"; +import "./index.css"; + +const $root = document.getElementById("root"); + +if (import.meta.env.DEV) { + // 개발 시 + const enableMocking = async function () { + // 실서버와 연동시 //return;의 주석 지워서 테스트해주세요 + // return; + const worker = (await import("./mock.js")).default; + await worker.start({ onUnhandledRequest: "bypass" }); + }; + enableMocking().then(() => { + const root = createRoot($root); + root.render( + + + + + , + ); + }); +} else { + // 배포 시 + hydrateRoot( + $root, + + + + + , + ); +} diff --git a/packages/adminPage/src/main-server.jsx b/packages/adminPage/src/main-server.jsx new file mode 100644 index 00000000..55aa0715 --- /dev/null +++ b/packages/adminPage/src/main-server.jsx @@ -0,0 +1,30 @@ +import { StrictMode } from "react"; +import { renderToString } from "react-dom/server"; +import { StaticRouter } from "react-router-dom/server"; +import App from "./App.jsx"; + +export default function render(url) { + const path = url === "index" ? "/" : `/${url}`; + const nye = renderToString( + + + + + , + ); + return nye; +} + +/** + * 우리의 메인 컴포넌트를 문자열로 렌더링하는 함수를 반환합니다. + * + * 향후 페이지가 추가된다면, + * + * import SecondPage from "./SecondPage.jsx"; + * + * export default function render(url) { + * // 여기에서 url에 따라 분기처리를 하면 됩니다. + * } + * + * 현재로서는 단일 페이지이므로 render 함수 내에 분기처리를 하지 않습니다. + */ diff --git a/packages/adminPage/src/mock.js b/packages/adminPage/src/mock.js new file mode 100644 index 00000000..dd937ea3 --- /dev/null +++ b/packages/adminPage/src/mock.js @@ -0,0 +1,21 @@ +import { setupWorker } from "msw/browser"; +import authHandler from "@admin/auth/mock.js"; +import commentHandler from "./features/comment/mock.js"; +import serverTimeHandler from "@admin/serverTime/mock.js"; +import eventSearchHandler from "./features/eventList/mock.js"; +import eventCreateHandler from "./features/eventEdit/mock.js"; +import drawHandler from "./features/eventDetail/drawButton/mock.js"; +import usersHandler from "./features/users/mock.js"; + +// mocking은 기본적으로 각 feature 폴더 내의 mock.js로 정의합니다. +// 새로운 feature의 mocking을 추가하셨으면, mock.js의 setupWorker 내부 함수에 인자를 spread 연산자를 이용해 추가해주세요. +// 예시 : export default setupWorker(...authHandler, ...questionHandler, ...articleHandler); +export default setupWorker( + ...authHandler, + ...eventSearchHandler, + ...serverTimeHandler, + ...commentHandler, + ...eventCreateHandler, + ...drawHandler, + ...usersHandler, +); diff --git a/packages/adminPage/src/pages/CommentsIDPage.jsx b/packages/adminPage/src/pages/CommentsIDPage.jsx new file mode 100644 index 00000000..a273b457 --- /dev/null +++ b/packages/adminPage/src/pages/CommentsIDPage.jsx @@ -0,0 +1,17 @@ +import Container from "@admin/components/Container.jsx"; +import AdminCommentID from "../features/comment/id"; +import { useParams } from "react-router-dom"; + +export default function CommentsPage() { + const { eventId } = useParams(); + + return ( + +
    + 기대평 + + +
    +
    + ); +} diff --git a/packages/adminPage/src/pages/CommentsPage.jsx b/packages/adminPage/src/pages/CommentsPage.jsx new file mode 100644 index 00000000..28f980db --- /dev/null +++ b/packages/adminPage/src/pages/CommentsPage.jsx @@ -0,0 +1,14 @@ +import Container from "@admin/components/Container.jsx"; +import AdminComment from "../features/comment"; + +export default function CommentsPage() { + return ( + +
    + 기대평 + + +
    +
    + ); +} diff --git a/packages/adminPage/src/pages/EventsCreatePage.jsx b/packages/adminPage/src/pages/EventsCreatePage.jsx new file mode 100644 index 00000000..188ed5b0 --- /dev/null +++ b/packages/adminPage/src/pages/EventsCreatePage.jsx @@ -0,0 +1,15 @@ +import Container from "@admin/components/Container.jsx"; +import EventEditor from "../features/eventEdit/index.jsx"; +import { EventEditModeContext } from "../features/eventEdit/businessLogic/context.js"; + +function EventsCreatePage() { + return ( + + + + + + ); +} + +export default EventsCreatePage; diff --git a/packages/adminPage/src/pages/EventsDetailPage.jsx b/packages/adminPage/src/pages/EventsDetailPage.jsx new file mode 100644 index 00000000..8b70364e --- /dev/null +++ b/packages/adminPage/src/pages/EventsDetailPage.jsx @@ -0,0 +1,21 @@ +import { useParams } from "react-router-dom"; +import Container from "@admin/components/Container.jsx"; +import Suspense from "@common/components/Suspense.jsx"; +import ErrorBoundary from "@common/components/ErrorBoundary.jsx"; +import EventDetailFetcher from "../features/eventDetail/EventDetailFetcher.jsx"; + +function EventsDetailPage() { + const { eventId } = useParams(); + + return ( + + error}> + loading}> + + + + + ); +} + +export default EventsDetailPage; diff --git a/packages/adminPage/src/pages/EventsEditPage.jsx b/packages/adminPage/src/pages/EventsEditPage.jsx new file mode 100644 index 00000000..fff3bdbb --- /dev/null +++ b/packages/adminPage/src/pages/EventsEditPage.jsx @@ -0,0 +1,24 @@ +import { useParams } from "react-router-dom"; +import Container from "@admin/components/Container.jsx"; +import Suspense from "@common/components/Suspense.jsx"; +import ErrorBoundary from "@common/components/ErrorBoundary.jsx"; +import { EventEditModeContext } from "../features/eventEdit/businessLogic/context.js"; +import EventEditFetcher from "../features/eventEdit/EventEditFetcher.jsx"; + +function EventsEditPage() { + const { eventId } = useParams(); + + return ( + + error}> + loading}> + + + + + + + ); +} + +export default EventsEditPage; diff --git a/packages/adminPage/src/pages/EventsPage.jsx b/packages/adminPage/src/pages/EventsPage.jsx new file mode 100644 index 00000000..80a44bec --- /dev/null +++ b/packages/adminPage/src/pages/EventsPage.jsx @@ -0,0 +1,12 @@ +import Container from "@admin/components/Container.jsx"; +import EventList from "../features/eventList"; + +function EventsPage() { + return ( + + + + ); +} + +export default EventsPage; diff --git a/packages/adminPage/src/pages/LoginPage.jsx b/packages/adminPage/src/pages/LoginPage.jsx new file mode 100644 index 00000000..1b2d9ba9 --- /dev/null +++ b/packages/adminPage/src/pages/LoginPage.jsx @@ -0,0 +1,11 @@ +import Container from "@admin/components/Container.jsx"; +import LoginSection from "@admin/auth/LoginSection.jsx"; +function LoginPage() { + return ( + + + + ); +} + +export default LoginPage; diff --git a/packages/adminPage/src/pages/ProtectedRoute.jsx b/packages/adminPage/src/pages/ProtectedRoute.jsx new file mode 100644 index 00000000..ca46999f --- /dev/null +++ b/packages/adminPage/src/pages/ProtectedRoute.jsx @@ -0,0 +1,21 @@ +import { Navigate, Outlet } from "react-router-dom"; + +import Container from "@admin/components/Container.jsx"; +import useUserStore from "@admin/auth/store.js"; + +function ProtectedRoute() { + const isLogin = useUserStore((store) => store.isLogin); + const initialized = useUserStore((store) => store.initialized); + + if (!initialized) return ; + if (!isLogin) + return ( + <> + + + + ); + return ; +} + +export default ProtectedRoute; diff --git a/packages/adminPage/src/pages/RootRoute.jsx b/packages/adminPage/src/pages/RootRoute.jsx new file mode 100644 index 00000000..c80b5931 --- /dev/null +++ b/packages/adminPage/src/pages/RootRoute.jsx @@ -0,0 +1,11 @@ +import { Navigate } from "react-router-dom"; +import useUserStore from "@admin/auth/store.js"; + +function RootRoute() { + const isLogin = useUserStore((store) => store.isLogin); + + if (isLogin) return ; + return ; +} + +export default RootRoute; diff --git a/packages/adminPage/src/pages/UsersPage.jsx b/packages/adminPage/src/pages/UsersPage.jsx new file mode 100644 index 00000000..85ec1a52 --- /dev/null +++ b/packages/adminPage/src/pages/UsersPage.jsx @@ -0,0 +1,16 @@ +import Container from "@admin/components/Container.jsx"; +import Users from "../features/users"; + +function UsersPage() { + return ( + +
    + 유저 조회 + + +
    +
    + ); +} + +export default UsersPage; diff --git a/packages/adminPage/src/shared/auth/LoginSection.jsx b/packages/adminPage/src/shared/auth/LoginSection.jsx new file mode 100644 index 00000000..4003359e --- /dev/null +++ b/packages/adminPage/src/shared/auth/LoginSection.jsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { login } from "./store.js"; +import { fetchServer, handleError } from "@common/dataFetch/fetchServer.js"; +import Input from "@common/components/Input.jsx"; +import Button from "@common/components/Button.jsx"; + +const loginErrorHandler = { + 400: "잘못된 입력입니다!", + 401: "로그인에 실패했습니다!", +}; + +function LoginSection() { + const navigate = useNavigate(); + const [id, setId] = useState(""); + const [password, setPassword] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + async function onSubmit(e) { + e.preventDefault(); + const config = { method: "post", body: { userName: id, password } }; + setErrorMessage(""); + fetchServer("/api/v1/admin/auth/signin", config) + .then(({ token }) => { + login(token); + navigate("/events", { replace: true }); + }) + .catch(handleError(loginErrorHandler)) + .catch((e) => { + setId(""); + setPassword(""); + setErrorMessage(e.message); + }); + } + return ( +
    +
    + + +
    +
    + + {errorMessage} +
    +
    + ); +} + +export default LoginSection; diff --git a/packages/adminPage/src/shared/auth/mock.js b/packages/adminPage/src/shared/auth/mock.js new file mode 100644 index 00000000..74b91375 --- /dev/null +++ b/packages/adminPage/src/shared/auth/mock.js @@ -0,0 +1,14 @@ +import { http, HttpResponse } from "msw"; + +const handlers = [ + http.post("/api/v1/admin/auth/signin", async ({ request }) => { + const { username, password } = await request.json(); + if (username !== "admin" && password !== "password1!") { + return HttpResponse.json({ return: false }, { status: 401 }); + } + + return HttpResponse.json({ token: "test_token" }); + }), +]; + +export default handlers; diff --git a/packages/adminPage/src/shared/auth/store.js b/packages/adminPage/src/shared/auth/store.js new file mode 100644 index 00000000..cb5ea08a --- /dev/null +++ b/packages/adminPage/src/shared/auth/store.js @@ -0,0 +1,27 @@ +import { create } from "zustand"; +import tokenSaver from "@common/dataFetch/tokenSaver.js"; +import { ADMIN_TOKEN_ID } from "@common/constants.js"; + +const userStore = create(() => ({ + initialized: false, + isLogin: false, +})); + +export function login(token) { + tokenSaver.set(token); + userStore.setState(() => ({ isLogin: true, initialized: true })); +} + +export function logout() { + tokenSaver.remove(); + userStore.setState(() => ({ isLogin: false, initialized: true })); +} + +export function initLoginState() { + tokenSaver.init(ADMIN_TOKEN_ID); + const token = tokenSaver.get(ADMIN_TOKEN_ID); + if (token === null) userStore.setState(() => ({ isLogin: false, initialized: true })); + else userStore.setState(() => ({ isLogin: true, initialized: true })); +} + +export default userStore; diff --git a/packages/adminPage/src/shared/components/Container.jsx b/packages/adminPage/src/shared/components/Container.jsx new file mode 100644 index 00000000..38e4a8c4 --- /dev/null +++ b/packages/adminPage/src/shared/components/Container.jsx @@ -0,0 +1,16 @@ +import NavBar from "./NavBar.jsx"; + +function Container({ children, shouldCenter = false }) { + return ( +
    + +
    + {children} +
    +
    + ); +} + +export default Container; diff --git a/packages/adminPage/src/shared/components/DateInput/index.jsx b/packages/adminPage/src/shared/components/DateInput/index.jsx new file mode 100644 index 00000000..a63735b0 --- /dev/null +++ b/packages/adminPage/src/shared/components/DateInput/index.jsx @@ -0,0 +1,25 @@ +import { formatDate } from "@common/utils.js"; +import style from "./style.module.css"; + +export default function DateTextInput({ date, setDate, ...otherProps }) { + const text = date === null ? "" : formatDate(date, "YYYY-MM-DD"); + const visibleText = date === null ? "-/--" : formatDate(date, "M/DD"); + function onChange(e) { + const value = e.target.value; + setDate(value === "" ? null : new Date(value.replace(/-/g, "/"))); + } + return ( +
    +
    + {visibleText} +
    + +
    + ); +} diff --git a/packages/adminPage/src/shared/components/DateInput/style.module.css b/packages/adminPage/src/shared/components/DateInput/style.module.css new file mode 100644 index 00000000..f12410bf --- /dev/null +++ b/packages/adminPage/src/shared/components/DateInput/style.module.css @@ -0,0 +1,24 @@ +.hiddenDateInput { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + background: transparent; + color: transparent; + z-index: -1; +} + +.hiddenDateInput::active { + background: transparent; + color: transparent; +} + +.hiddenDateInput::-webkit-calendar-picker-indicator { + width: 100%; + height: 999px; + background: transparent; + color: transparent; + cursor: pointer; +} diff --git a/packages/adminPage/src/shared/components/DeleteButton/close.svg b/packages/adminPage/src/shared/components/DeleteButton/close.svg new file mode 100644 index 00000000..d1368fea --- /dev/null +++ b/packages/adminPage/src/shared/components/DeleteButton/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/adminPage/src/shared/components/DeleteButton/index.jsx b/packages/adminPage/src/shared/components/DeleteButton/index.jsx new file mode 100644 index 00000000..265f5e8c --- /dev/null +++ b/packages/adminPage/src/shared/components/DeleteButton/index.jsx @@ -0,0 +1,17 @@ +import CloseIcon from "./close.svg?react"; + +function DeleteButton({ onClick, disabled }) { + return ( + + ); +} + +export default DeleteButton; diff --git a/packages/adminPage/src/shared/components/NavBar.jsx b/packages/adminPage/src/shared/components/NavBar.jsx new file mode 100644 index 00000000..c3f91b22 --- /dev/null +++ b/packages/adminPage/src/shared/components/NavBar.jsx @@ -0,0 +1,36 @@ +import { useNavigate } from "react-router-dom"; + +import NavBarItem from "./NavBarItem.jsx"; +import useAuthStore, { logout } from "@admin/auth/store.js"; + +function NavBar() { + const navigate = useNavigate(); + const isLogin = useAuthStore((store) => store.isLogin); + function onLogoutClick() { + logout(); + navigate("/login"); + } + + return ( + + ); +} + +export default NavBar; diff --git a/packages/adminPage/src/shared/components/NavBarItem.jsx b/packages/adminPage/src/shared/components/NavBarItem.jsx new file mode 100644 index 00000000..eea56530 --- /dev/null +++ b/packages/adminPage/src/shared/components/NavBarItem.jsx @@ -0,0 +1,30 @@ +import { NavLink } from "react-router-dom"; + +function NavBarItem({ to, onClick, disabled, children }) { + const commonStyle = `w-full h-full flex justify-center items-center + hover:border-2 hover:border-white + active:border-2 active:border-neutral-400`; + + const commonTextStyle = `text-neutral-200 hover:text-white active:text-neutral-400 invalid:text-neutral-600`; + + const navLink = ( + + `${commonStyle} ${disabled ? "pointer-events-none text-neutral-600" : ""} ${isActive ? "text-blue-400" : commonTextStyle}` + } + > + {children} + + ); + + const button = ( + + ); + + return
  • {to ? navLink : button}
  • ; +} + +export default NavBarItem; diff --git a/packages/adminPage/src/shared/components/Pagination.jsx b/packages/adminPage/src/shared/components/Pagination.jsx new file mode 100644 index 00000000..9ed91b97 --- /dev/null +++ b/packages/adminPage/src/shared/components/Pagination.jsx @@ -0,0 +1,53 @@ +import { clamp } from "@common/utils.js"; + +function getPaginationItem(currentPage, maxPage, length) { + let prevDelta = length % 2 === 1 ? (length - 1) / 2 : length / 2 - 1; + let postDelta = length % 2 === 1 ? (length - 1) / 2 : length / 2; + + if (maxPage < length) return Array.from({ length: maxPage }, (_, i) => i + 1); + if (currentPage - prevDelta <= 0) return Array.from({ length }, (_, i) => i + 1); + if (currentPage + postDelta > maxPage) + return Array.from({ length }, (_, i) => maxPage - length + i + 1); + return Array.from({ length }, (_, i) => currentPage - prevDelta + i); +} + +function PaginationButton({ onClick, disabled, highlighted, children }) { + return ( + + ); +} + +function Pagination({ currentPage, setPage: _setPage, maxPage, length = 5 }) { + const setPage = (index) => () => _setPage(clamp(index, 1, maxPage)); + + return ( +
    + + << + + + < + + {getPaginationItem(currentPage, maxPage, length).map((i) => ( + + {i} + + ))} + maxPage}> + > + + maxPage}> + >> + +
    + ); +} + +export default Pagination; diff --git a/packages/adminPage/src/shared/components/SmallInput.jsx b/packages/adminPage/src/shared/components/SmallInput.jsx new file mode 100644 index 00000000..1ad21ced --- /dev/null +++ b/packages/adminPage/src/shared/components/SmallInput.jsx @@ -0,0 +1,36 @@ +const inputboxStyle = `p-3 py-1 bg-neutral-50 border-2 border-neutral-400 outline-0 rounded text-body-m font-medium +focus:bg-white focus:border-neutral-800 +disabled:bg-neutral-100 +placeholder:text-neutral-400`; + +const errorStyle = `bg-white border-red-500 focus:border-red-500`; + +const errorInputStyle = `invalid:border-red-500 +invalid:focus:border-red-500`; + +function getInputClass(text, isError, className) { + return `${inputboxStyle} ${isError ? errorStyle : ""} ${/^\s*$/.test(text) ? "" : errorInputStyle} ${className ?? ""}`; +} + +export function Input({ text, setText, isError, className, ...otherProps }) { + return ( + setText?.(e.target.value)} + {...otherProps} + /> + ); +} + +export function TextBox({ text, setText, isError, className, ...otherProps }) { + return ( +