diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 00000000..e4aafdcb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,12 @@ +## 확인 사항 + +- assigness, labels도 추가해주세요 + +## 설명 + +- 설명을 적어주세요! + +## Todo + +- [ ] 해야 할 일 1 +- [ ] 해야 할 일 2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..cb281178 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +close #ISSUE_NUMBER + +## 확인 사항 + +- merge dev로 향하는지 확인 +- assigness, labels도 추가해주세요 + +## 작업 내용 + +- 작업 내용 적어주세요 +- 스크린샷도 좋아요👍🏻 + +## 주의 사항 + +- ex) 라이브러리 설치 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..1e30bd1c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,34 @@ +name: Deploy + +on: + push: + branches: ['dev'] + +jobs: + build: + runs-on: ubuntu-latest + container: pandoc/latex + steps: + - uses: actions/checkout@v2 + + - name: Install mustache (to update the date) + run: apk add ruby && gem install mustache + + - name: creates output + run: sh ./build.sh + + - name: Pushes to another repository + id: push_directory + uses: cpina/github-action-push-to-another-repository@main + env: + API_TOKEN_GITHUB: ${{ secrets.AUTO_ACTIONS }} + with: + source-directory: 'output' + destination-github-username: MINJI121 + destination-repository-name: Taskify4 + user-email: ${{ secrets.EMAIL }} + commit-message: ${{ github.event.commits[0].message }} + target-branch: main + + - name: Test get variable exported by push-to-another-repository + run: echo $DESTINATION_CLONED_DIRECTORY diff --git a/.gitignore b/.gitignore index 5ef6a520..cd01f46a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +.env.local # vercel .vercel diff --git a/README.md b/README.md index ef0e47e3..7a6ac13e 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,135 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). +
+ + Taskify + +
-## Getting Started +- Taskify 일정관리 서비스 +- 개발 기간 : 25.03.18 ~ 25.04.04 -First, run the development server: +# Team + +### 황혜진 + +- 팀장 +- 공통 컴포넌트 Button, Modal을 크기 및 버튼 개수 설정 등 유동적인 UI로 구성 +- 주요 컴포넌트 작성: Card, CardList, Column +- CardList, Column의 스크롤 바닥 감지를 통한 무한 스크롤 기능 구현 +- [dashboardId]index에서 칼럼 및 카드 데이터 동적 렌더링 +- 카드 생성 / 삭제 / 상세조회 기능을 포함한 모달 기반 UI 작성 (AddColumnModal, ColumnDeleteModal, ColumnManageModal) +- 프로젝트 초기 컨벤션(파일명, 커밋 메시지, 브랜치 네이밍 등) 설정 및 팀 내 공유 + + +### 임용균 + +- 프로젝트 세팅 +- 컴포넌트 작성 Input, SideMenu, TodoModal, TaskModal +- 페이지 작성 landing, MyDashboard +- SideMenu 접기/펴기 기능 및 반응형 +- MyDashboard Page 검색어 기반 필터링 및 페이지네이션 연동 +- TodoModal, TaskModal Api 연동 및 업로드 기능 구현 + + +### 조민지 + +- Style: globals.css, custom toast +- 컴포넌트 작성 Gnb +- 페이지 작성 login/signup +- login/logout 전역 상태 관리 -Zustand, UseAuthGuard +- mydashboard에 대시보드 편집 모드 추가 +- 대시보드 멤버 목록 드롭다운 메뉴 기능 +- 404 페이지 작성 +- QA + + +### 김교연 + +- 컴포넌트 작성 invited/ MemberList, inviteRecords, invitedDashBoard, card, Modal +- invitedDashBoard 검색, 무한스크롤, 데이터 별 컴포넌트 분리 +- MemberList 프로필이미지 출력, Modal 대시보드 이름 변경 기능 +- 카드 프로필 및 비밀번호 변경 +- 대시보드 수정 페이지- 이름 변경, 구성원 관리, 대시보드 초대, 삭제 기능 디자인 및 기능 +- toast 알람으로 피드백 추가 + +### 정종우 + +- apiRoutes 설정 +- 컴포넌트 작성 ModalDashBoard, Button(card, Columns,Todo) +- 페이지 작성 mypage +- mypage 프로필 변경, 비밀번호 변경 기능 작성 +- 대시보드 카드 모달 삭제기능 + + +# Images + +https://github.com/user-attachments/assets/64c0e04f-a5da-42c0-a576-1f27519447fb + + + +# Skill Stacks + +## Environment -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. +Git GitHub VSCode Vercel Figma -[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. -This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## Development -## Learn More -To learn more about Next.js, take a look at the following resources: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. + Tailwind CSS TypeScript Next.js -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Libraries -## Deploy on Vercel +Axios clsx -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. +# Package Structure + + +``` +taskify +├─ public +│ ├─ svgs # 아이콘 리소스 +│ └─ images # 이미지 리소스 +├─ src +│ ├─ api # API 사용을 위한 세팅 +│ ├─ components # 주요 컴포넌트 +│ ├─ constants # +│ ├─ hocks # 인증, 모달 컨텍스트 프로바이더 +│ ├─ pages # 커스텀 훅 +│ ├─ shared # +| ├─ store # +├─ styles +│ └─ globals.css # 폰트 +└─ types # 스타일 +``` + +# Installation + +1. Clone the repository + +```bash +git clone https://github.com/part3-4team-Taskify +``` + +2. Install dependencies + +```bash +npm install +``` + +3. Start the development server + +```bash +npm start dev +``` + +4. Open the project in your browser + +```bash +http://localhost:3000 +``` diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..94a42303 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh +cd ../ +mkdir output +cp -R ./Taskify/* ./output +cp -R ./output ./Taskify/ diff --git a/declares.d.ts b/declares.d.ts new file mode 100644 index 00000000..cbe652db --- /dev/null +++ b/declares.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/next.config.ts b/next.config.ts index b0fabfaa..ed1381a5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,11 @@ const nextConfig: NextConfig = { /* config options here */ reactStrictMode: true, devIndicators: false, + images: { + domains: [ + "sprint-fe-project.s3.ap-northeast-2.amazonaws.com", // 에러에 나온 도메인 등록 + ], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 55f6c6a3..10b0d109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,23 @@ "version": "0.1.0", "dependencies": { "@tanstack/react-query": "^5.68.0", - "autoprefixer": "^10.4.21", "axios": "^1.8.3", - "next": "15.2.2", - "postcss": "^8.5.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lodash": "^4.17.21", + "lucide-react": "^0.485.0", + "moment": "^2.30.1", + "next": "^15.2.4", "prettier-plugin-tailwindcss": "^0.6.11", "react": "^19.0.0", + "react-datepicker": "^8.2.1", + "react-datetime": "^3.3.1", "react-dom": "^19.0.0", - "react-hook-form": "^7.54.2" + "react-hook-form": "^7.54.2", + "react-intersection-observer": "^9.16.0", + "react-toastify": "^11.0.5", + "tostify": "^0.0.1", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -24,13 +33,15 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "autoprefixer": "^10.4.21", "eslint": "^9.22.0", "eslint-config-next": "^15.2.2", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.3", + "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^4.0.14", - "typescript": "^5" + "typescript": "^5.8.2" } }, "node_modules/@alloc/quick-lru": { @@ -47,9 +58,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", - "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.0.tgz", + "integrity": "sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==", "dev": true, "license": "MIT", "optional": true, @@ -59,9 +70,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz", + "integrity": "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==", "license": "MIT", "optional": true, "dependencies": { @@ -137,9 +148,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", - "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", + "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -160,9 +171,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -184,9 +195,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", - "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", + "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", "dev": true, "license": "MIT", "engines": { @@ -217,6 +228,59 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.5.tgz", + "integrity": "sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.9", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -658,15 +722,15 @@ } }, "node_modules/@next/env": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.2.tgz", - "integrity": "sha512-yWgopCfA9XDR8ZH3taB5nRKtKJ1Q5fYsTOuYkzIIoS8TJ0UAUKAGF73JnGszbjk2ufAQDj6mDdgsJAFx5CLtYQ==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", + "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.2.tgz", - "integrity": "sha512-1+BzokFuFQIfLaRxUKf2u5In4xhPV7tUgKcK53ywvFl6+LXHWHpFkcV7VNeKlyQKUotwiq4fy/aDNF9EiUp4RQ==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.4.tgz", + "integrity": "sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -674,9 +738,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz", - "integrity": "sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", + "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", "cpu": [ "arm64" ], @@ -690,9 +754,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz", - "integrity": "sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", + "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", "cpu": [ "x64" ], @@ -706,9 +770,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz", - "integrity": "sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", + "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", "cpu": [ "arm64" ], @@ -722,9 +786,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz", - "integrity": "sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", + "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", "cpu": [ "arm64" ], @@ -738,9 +802,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz", - "integrity": "sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", + "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", "cpu": [ "x64" ], @@ -754,9 +818,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz", - "integrity": "sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", + "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", "cpu": [ "x64" ], @@ -770,9 +834,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz", - "integrity": "sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", + "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", "cpu": [ "arm64" ], @@ -786,9 +850,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.2.tgz", - "integrity": "sha512-52nWy65S/R6/kejz3jpvHAjZDPKIbEQu4x9jDBzmB9jJfuOy5rspjKu4u77+fI4M/WzLXrrQd57hlFGzz1ubcQ==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", + "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", "cpu": [ "x64" ], @@ -849,167 +913,10 @@ "node": ">=12.4.0" } }, - "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-5.0.0.tgz", - "integrity": "sha512-zwHAf+owoxSWTDD4dFuwW+FkpaDzbaL30H5Ltocb+RmLyg4WKuteusRLKh5Y8b/cyu7UzhxM0haIqQjyqA1iuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-5.0.0.tgz", - "integrity": "sha512-1lS3aBNVjVQKBvZdHm13+8tSjvu2Tl1Cv4FnUyMYxqx6+rsom2YaOylS5LhDUwfZu0zAgpLMwK6kGpF/UPncNg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-5.0.0.tgz", - "integrity": "sha512-q9sRd68wC1/AJ0eu6ClhxlklVfe8gH4wrUkSyEbIYTZ8zY5yjsLY3fpqqsaCvWJUx65nW+XtnAxCGCi5AXr1Mw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-5.0.0.tgz", - "integrity": "sha512-catYavWsvqViYnCveQjhrK6yVYDEPFvIOgGLxnz5r2dcgrjpmquzREoyss0L2QG/J5HTTbwqwZ1kk+g56hE/1A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-5.0.0.tgz", - "integrity": "sha512-l/0pWoQM5kVmJLg4frQ1mKZOXgi0ex/hzvFt8E4WK2ifXr5JgKFUokxsb/oat7f5YzdJJh5r9p+qS/t3dA26Aw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-5.0.0.tgz", - "integrity": "sha512-bx0oz/oaAW4FGYqpIIxJCnmgb906YfMhTEWCJvYkxjpEI8VKLJEL3PQevYiqDq36SA0yRLJ/sQK2fqry8AFBfA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-5.0.0.tgz", - "integrity": "sha512-4PH++qbSIhlRsFYdN1P9neDov4OGhTGo5nbQ1D7AL6gWFLo3gdZTc00FM2y8JjeTcPWEXkViZuwpuc0w5i6qHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-5.0.0.tgz", - "integrity": "sha512-mLfQFpX3/5y9oWi0b+9FbWDkL2hM0Y29653beCHiHxAdGyVgb2DsJbK74WkMTwtSz9by8vyBh8jGPZcg1yLZbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-5.0.0.tgz", - "integrity": "sha512-uEhsAZSo65qsRi6+IfBTEUUFbjg7T2yruJeLYpFfEATpm3ory5Mgo5vx3L0c2/Cz1OUZXBgp3A8x6VMUB2jT2A==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-5.0.0.tgz", - "integrity": "sha512-8DbSso9Jp1ns8AYuZFXdRfAcdJrzZwkFm/RjPuvAPTENsm685dosBF8G6gTHQlHvULnk6o3sa9ygZaTGC/UoEw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-5.0.0.tgz", - "integrity": "sha512-ylppfPEg63NuRXOPNsXFlgyl37JrtRn0QMO26X3K3Ytp5HtLrMreQMGVtgr30e1l2YmAWqhvmKlCryOqzGPD/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", + "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", "dev": true, "license": "MIT", "engines": { @@ -1049,44 +956,44 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz", - "integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.17.tgz", + "integrity": "sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg==", "dev": true, "license": "MIT", "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", - "tailwindcss": "4.0.14" + "tailwindcss": "4.0.17" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz", - "integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.17.tgz", + "integrity": "sha512-B4OaUIRD2uVrULpAD1Yksx2+wNarQr2rQh65nXqaqbLY1jCd8fO+3KLh/+TH4Hzh2NTHQvgxVbPdUDOtLk7vAw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.0.14", - "@tailwindcss/oxide-darwin-arm64": "4.0.14", - "@tailwindcss/oxide-darwin-x64": "4.0.14", - "@tailwindcss/oxide-freebsd-x64": "4.0.14", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14", - "@tailwindcss/oxide-linux-arm64-gnu": "4.0.14", - "@tailwindcss/oxide-linux-arm64-musl": "4.0.14", - "@tailwindcss/oxide-linux-x64-gnu": "4.0.14", - "@tailwindcss/oxide-linux-x64-musl": "4.0.14", - "@tailwindcss/oxide-win32-arm64-msvc": "4.0.14", - "@tailwindcss/oxide-win32-x64-msvc": "4.0.14" + "@tailwindcss/oxide-android-arm64": "4.0.17", + "@tailwindcss/oxide-darwin-arm64": "4.0.17", + "@tailwindcss/oxide-darwin-x64": "4.0.17", + "@tailwindcss/oxide-freebsd-x64": "4.0.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.17", + "@tailwindcss/oxide-linux-x64-musl": "4.0.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.17" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz", - "integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.17.tgz", + "integrity": "sha512-3RfO0ZK64WAhop+EbHeyxGThyDr/fYhxPzDbEQjD2+v7ZhKTb2svTWy+KK+J1PHATus2/CQGAGp7pHY/8M8ugg==", "cpu": [ "arm64" ], @@ -1101,9 +1008,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz", - "integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.17.tgz", + "integrity": "sha512-e1uayxFQCCDuzTk9s8q7MC5jFN42IY7nzcr5n0Mw/AcUHwD6JaBkXnATkD924ZsHyPDvddnusIEvkgLd2CiREg==", "cpu": [ "arm64" ], @@ -1118,9 +1025,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz", - "integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.17.tgz", + "integrity": "sha512-d6z7HSdOKfXQ0HPlVx1jduUf/YtBuCCtEDIEFeBCzgRRtDsUuRtofPqxIVaSCUTOk5+OfRLonje6n9dF6AH8wQ==", "cpu": [ "x64" ], @@ -1135,9 +1042,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz", - "integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.17.tgz", + "integrity": "sha512-EjrVa6lx3wzXz3l5MsdOGtYIsRjgs5Mru6lDv4RuiXpguWeOb3UzGJ7vw7PEzcFadKNvNslEQqoAABeMezprxQ==", "cpu": [ "x64" ], @@ -1152,9 +1059,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz", - "integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.17.tgz", + "integrity": "sha512-65zXfCOdi8wuaY0Ye6qMR5LAXokHYtrGvo9t/NmxvSZtCCitXV/gzJ/WP5ksXPhff1SV5rov0S+ZIZU+/4eyCQ==", "cpu": [ "arm" ], @@ -1169,9 +1076,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz", - "integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.17.tgz", + "integrity": "sha512-+aaq6hJ8ioTdbJV5IA1WjWgLmun4T7eYLTvJIToiXLHy5JzUERRbIZjAcjgK9qXMwnvuu7rqpxzej+hGoEcG5g==", "cpu": [ "arm64" ], @@ -1186,9 +1093,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz", - "integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.17.tgz", + "integrity": "sha512-/FhWgZCdUGAeYHYnZKekiOC0aXFiBIoNCA0bwzkICiMYS5Rtx2KxFfMUXQVnl4uZRblG5ypt5vpPhVaXgGk80w==", "cpu": [ "arm64" ], @@ -1203,9 +1110,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz", - "integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.17.tgz", + "integrity": "sha512-gELJzOHK6GDoIpm/539Golvk+QWZjxQcbkKq9eB2kzNkOvrP0xc5UPgO9bIMNt1M48mO8ZeNenCMGt6tfkvVBg==", "cpu": [ "x64" ], @@ -1220,9 +1127,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz", - "integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.17.tgz", + "integrity": "sha512-68NwxcJrZn94IOW4TysMIbYv5AlM6So1luTlbYUDIGnKma1yTFGBRNEJ+SacJ3PZE2rgcTBNRHX1TB4EQ/XEHw==", "cpu": [ "x64" ], @@ -1237,9 +1144,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz", - "integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.17.tgz", + "integrity": "sha512-AkBO8efP2/7wkEXkNlXzRD4f/7WerqKHlc6PWb5v0jGbbm22DFBLbIM19IJQ3b+tNewQZa+WnPOaGm0SmwMNjw==", "cpu": [ "arm64" ], @@ -1254,9 +1161,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz", - "integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.17.tgz", + "integrity": "sha512-7/DTEvXcoWlqX0dAlcN0zlmcEu9xSermuo7VNGX9tJ3nYMdo735SHvbrHDln1+LYfF6NhJ3hjbpbjkMOAGmkDg==", "cpu": [ "x64" ], @@ -1271,24 +1178,24 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.0.14.tgz", - "integrity": "sha512-+uIR6KtKhla1XeIanF27KtrfYy+PX+R679v5LxbkmEZlhQe3g8rk+wKj7Xgt++rWGRuFLGMXY80Ek8JNn+kN/g==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.0.17.tgz", + "integrity": "sha512-qeJbRTB5FMZXmuJF+eePd235EGY6IyJZF0Bh0YM6uMcCI4L9Z7dy+lPuLAhxOJzxnajsbjPoDAKOuAqZRtf1PQ==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.0.14", - "@tailwindcss/oxide": "4.0.14", + "@tailwindcss/node": "4.0.17", + "@tailwindcss/oxide": "4.0.17", "lightningcss": "1.29.2", "postcss": "^8.4.41", - "tailwindcss": "4.0.14" + "tailwindcss": "4.0.17" } }, "node_modules/@tanstack/query-core": { - "version": "5.68.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.68.0.tgz", - "integrity": "sha512-r8rFYYo8/sY/LNaOqX84h12w7EQev4abFXDWy4UoDVUJzJ5d9Fbmb8ayTi7ScG+V0ap44SF3vNs/45mkzDGyGw==", + "version": "5.70.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.70.0.tgz", + "integrity": "sha512-ZkkjQAZjI6nS5OyAmaSQafQXK180Xvp0lZYk4BzrnskkTV8On3zSJUxOIXnh0h/8EgqRkCA9i879DiJovA1kGw==", "license": "MIT", "funding": { "type": "github", @@ -1296,12 +1203,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.68.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.68.0.tgz", - "integrity": "sha512-mMOdGDKlwTP/WV72QqSNf4PAMeoBp/DqBHQ222wBfb51Looi8QUqnCnb9O98ZgvNISmy6fzxRGBJdZ+9IBvX2Q==", + "version": "5.70.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.70.0.tgz", + "integrity": "sha512-z0tx1zz2CQ6nTm+fCaOp93FqsFjNgXtOy+4mC5ifQ4B+rJiMD0AGfJrYSGh/OuefhrzTYDAbkGUAGw6JzkWy8g==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.68.0" + "@tanstack/query-core": "5.70.0" }, "funding": { "type": "github", @@ -1323,9 +1230,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, @@ -1344,9 +1251,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", - "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", + "version": "20.17.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.28.tgz", + "integrity": "sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1354,10 +1261,10 @@ } }, "node_modules/@types/react": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", - "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", - "dev": true, + "version": "19.0.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", + "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1374,17 +1281,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", - "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/type-utils": "8.26.1", - "@typescript-eslint/utils": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1404,16 +1311,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", - "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/typescript-estree": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { @@ -1429,14 +1336,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", - "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1447,14 +1354,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", - "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.1", - "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1471,9 +1378,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", - "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "license": "MIT", "engines": { @@ -1485,14 +1392,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", - "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1568,16 +1475,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", - "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/typescript-estree": "8.26.1" + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1592,13 +1499,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", - "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/types": "8.28.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1609,6 +1516,219 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-ddnlXgRi0Fog5+7U5Q1qY62wl95Q1lB4tXQX1UIA9YHmRCHN2twaQW0/4tDVGCvTVEU3xEayU7VemEr7GcBYUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.3.2.tgz", + "integrity": "sha512-tnl9xoEeg503jis+LW5cuq4hyLGQyqaoBL8VdPSqcewo/FL1C8POHbzl+AL25TidWYJD+R6bGUTE381kA1sT9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.3.2.tgz", + "integrity": "sha512-zyPn9LFCCjhKPeCtECZaiMUgkYN/VpLb4a9Xv7QriJmTaQxsuDtXqOHifrzUXIhorJTyS+5MOKDuNL0X9I4EHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.3.2.tgz", + "integrity": "sha512-UWx56Wh59Ro69fe+Wfvld4E1n9KG0e3zeouWLn8eSasyi/yVH/7ZW3CLTVFQ81oMKSpXwr5u6RpzttDXZKiO4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.3.2.tgz", + "integrity": "sha512-VYGQXsOEJtfaoY2fOm8Z9ii5idFaHFYlrq3yMFZPaFKo8ufOXYm8hnfru7qetbM9MX116iWaPC0ZX5sK+1Dr+g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.3.2.tgz", + "integrity": "sha512-3zP420zxJfYPD1rGp2/OTIBxF8E3+/6VqCG+DEO6kkDgBiloa7Y8pw1o7N9BfgAC+VC8FPZsFXhV2lpx+lLRMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.3.2.tgz", + "integrity": "sha512-ZWjSleUgr88H4Kei7yT4PlPqySTuWN1OYDDcdbmMCtLWFly3ed+rkrcCb3gvqXdDbYrGOtzv3g2qPEN+WWNv5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.3.2.tgz", + "integrity": "sha512-p+5OvYJ2UOlpjes3WfBlxyvQok2u26hLyPxLFHkYlfzhZW0juhvBf/tvewz1LDFe30M7zL9cF4OOO5dcvtk+cw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.3.2.tgz", + "integrity": "sha512-yweY7I6SqNn3kvj6vE4PQRo7j8Oz6+NiUhmgciBNAUOuI3Jq0bnW29hbHJdxZRSN1kYkQnSkbbA1tT8VnK816w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.3.2.tgz", + "integrity": "sha512-fNIvtzJcGN9hzWTIayrTSk2+KHQrqKbbY+I88xMVMOFV9t4AXha4veJdKaIuuks+2JNr6GuuNdsL7+exywZ32w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.3.2.tgz", + "integrity": "sha512-OaFEw8WAjiwBGxutQgkWhoAGB5BQqZJ8Gjt/mW+m6DWNjimcxU22uWCuEtfw1CIwLlKPOzsgH0429fWmZcTGkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.3.2.tgz", + "integrity": "sha512-u+sumtO7M0AGQ9bNQrF4BHNpUyxo23FM/yXZfmVAicTQ+mXtG06O7pm5zQUw3Mr4jRs2I84uh4O0hd8bdouuvQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.3.2.tgz", + "integrity": "sha512-ZAJKy95vmDIHsRFuPNqPQRON8r2mSMf3p9DoX+OMOhvu2c8OXGg8MvhGRf3PNg45ozRrPdXDnngURKgaFfpGoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.3.2.tgz", + "integrity": "sha512-nQG4YFAS2BLoKVQFK/FrWJvFATI5DQUWQrcPcsWG9Ve5BLLHZuPOrJ2SpAJwLXQrRv6XHSFAYGI8wQpBg/CiFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.3.2.tgz", + "integrity": "sha512-XBWpUP0mHya6yGBwNefhyEa6V7HgYKCxEAY4qhTm/PcAQyBPNmjj97VZJOJkVdUsyuuii7xmq0pXWX/c2aToHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -1867,6 +1987,7 @@ "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -1927,9 +2048,9 @@ } }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1982,6 +2103,7 @@ "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2081,9 +2203,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001704", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz", - "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==", + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "funding": [ { "type": "opencollective", @@ -2123,6 +2245,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2206,7 +2337,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2270,6 +2401,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2378,9 +2519,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.119", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz", - "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==", + "version": "1.5.128", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", + "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -2578,6 +2720,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2597,19 +2740,19 @@ } }, "node_modules/eslint": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", - "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", + "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.1.0", + "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.22.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.23.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2658,13 +2801,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.2.tgz", - "integrity": "sha512-g34RI7RFS4HybYFwGa/okj+8WZM+/fy+pEM+aqRQoVvM4gQhKrd4wIEddKmlZfWD75j8LTwB5zwkmNv3DceH1A==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.4.tgz", + "integrity": "sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.2.2", + "@next/eslint-plugin-next": "15.2.4", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -2721,25 +2864,25 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.9.0.tgz", - "integrity": "sha512-EUcFmaz0zAa6P2C9jAb5XDymRld8S6TURvWcIW7y+czOW+K8hrjgQgbhBsNE0J/dDZ6HLfcn70LqnIil9W+ICw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.0.tgz", + "integrity": "sha512-aV3/dVsT0/H9BtpNwbaqvl+0xGMRGzncLyhm793NFGvbwGGvzyAykqWZ8oZlZuGwuHkwJjhWJkG1cM3ynvd2pQ==", "dev": true, "license": "ISC", "dependencies": { "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.3.7", + "debug": "^4.4.0", "get-tsconfig": "^4.10.0", - "is-bun-module": "^1.0.2", - "oxc-resolver": "^5.0.0", + "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.12" + "tinyglobby": "^0.2.12", + "unrs-resolver": "^1.3.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + "url": "https://opencollective.com/eslint-import-resolver-typescript" }, "peerDependencies": { "eslint": "*", @@ -2868,14 +3011,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", + "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.10.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2886,7 +3029,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -3253,6 +3396,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -3661,13 +3805,13 @@ } }, "node_modules/is-bun-module": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", - "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.6.3" + "semver": "^7.7.1" } }, "node_modules/is-callable": { @@ -4023,7 +4167,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4388,6 +4531,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4399,7 +4548,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4408,6 +4556,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "0.485.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.485.0.tgz", + "integrity": "sha512-NvyQJ0LKyyCxL23nPKESlr/jmz8r7fJO1bkuptSNYSy0s8VVj4ojhX0YAgmE1e0ewfxUZjIlZpvH+otfTnla8Q==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4485,6 +4642,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4493,9 +4659,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", - "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -4518,12 +4684,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.2.2.tgz", - "integrity": "sha512-dgp8Kcx5XZRjMw2KNwBtUzhngRaURPioxoNIVl5BOyJbhi9CUgEtKDO7fx5wh8Z8vOVX1nYZ9meawJoRrlASYA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", + "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", "license": "MIT", "dependencies": { - "@next/env": "15.2.2", + "@next/env": "15.2.4", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -4538,14 +4704,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.2.2", - "@next/swc-darwin-x64": "15.2.2", - "@next/swc-linux-arm64-gnu": "15.2.2", - "@next/swc-linux-arm64-musl": "15.2.2", - "@next/swc-linux-x64-gnu": "15.2.2", - "@next/swc-linux-x64-musl": "15.2.2", - "@next/swc-win32-arm64-msvc": "15.2.2", - "@next/swc-win32-x64-msvc": "15.2.2", + "@next/swc-darwin-arm64": "15.2.4", + "@next/swc-darwin-x64": "15.2.4", + "@next/swc-linux-arm64-gnu": "15.2.4", + "@next/swc-linux-arm64-musl": "15.2.4", + "@next/swc-linux-x64-gnu": "15.2.4", + "@next/swc-linux-x64-musl": "15.2.4", + "@next/swc-win32-arm64-msvc": "15.2.4", + "@next/swc-win32-x64-msvc": "15.2.4", "sharp": "^0.33.5" }, "peerDependencies": { @@ -4603,12 +4769,14 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, "license": "MIT" }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4618,7 +4786,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4773,29 +4940,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/oxc-resolver": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-5.0.0.tgz", - "integrity": "sha512-66fopyAqCN8Mx4tzNiBXWbk8asCSuxUWN62gwTc3yfRs7JfWhX/eVJCf+fUrfbNOdQVOWn+o8pAKllp76ysMXA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-resolver/binding-darwin-arm64": "5.0.0", - "@oxc-resolver/binding-darwin-x64": "5.0.0", - "@oxc-resolver/binding-freebsd-x64": "5.0.0", - "@oxc-resolver/binding-linux-arm-gnueabihf": "5.0.0", - "@oxc-resolver/binding-linux-arm64-gnu": "5.0.0", - "@oxc-resolver/binding-linux-arm64-musl": "5.0.0", - "@oxc-resolver/binding-linux-x64-gnu": "5.0.0", - "@oxc-resolver/binding-linux-x64-musl": "5.0.0", - "@oxc-resolver/binding-wasm32-wasi": "5.0.0", - "@oxc-resolver/binding-win32-arm64-msvc": "5.0.0", - "@oxc-resolver/binding-win32-x64-msvc": "5.0.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4901,6 +5045,7 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4929,6 +5074,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -5051,7 +5197,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -5097,30 +5242,58 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.2.1.tgz", + "integrity": "sha512-1pyALWM9mTZ7DG7tfcApwBy2kkld9Kz/EI++LhPnoXJAASbvuq6fdsDfkoB3q1JrxF7vhghVmQ759H/rOwUNNw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/react-datetime": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/react-datetime/-/react-datetime-3.3.1.tgz", + "integrity": "sha512-CMgQFLGidYu6CAlY6S2Om2UZiTfZsjC6j4foXcZ0kb4cSmPomdJ2S1PhK0v3fwflGGVuVARGxwkEUWtccHapJA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.7" + }, + "peerDependencies": { + "moment": "^2.16.0", + "react": "^16.5.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", "dependencies": { - "scheduler": "^0.25.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.0.0" + "react": "^19.1.0" } }, "node_modules/react-hook-form": { - "version": "7.54.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", - "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "version": "7.55.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz", + "integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -5133,13 +5306,40 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5316,9 +5516,9 @@ } }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, "node_modules/semver": { @@ -5742,14 +5942,14 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", + "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.0", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -5758,10 +5958,16 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz", - "integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.17.tgz", + "integrity": "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw==", "dev": true, "license": "MIT" }, @@ -5833,10 +6039,16 @@ "node": ">=8.0" } }, + "node_modules/tostify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tostify/-/tostify-0.0.1.tgz", + "integrity": "sha512-x/SckO051Chyh+94OIRz67Q6QTfRAXTxC2hCPqQLqoz+41eXZIxxFrLi5B4XdnU3Rn7LlMMQH+GpRnlLN7mP+g==", + "license": "MIT" + }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -5996,10 +6208,38 @@ "dev": true, "license": "MIT" }, + "node_modules/unrs-resolver": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.3.2.tgz", + "integrity": "sha512-ZKQBC351Ubw0PY8xWhneIfb6dygTQeUHtCcNGd0QB618zabD/WbFMYdRyJ7xeVT+6G82K5v/oyZO0QSHFtbIuw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/JounQin" + }, + "optionalDependencies": { + "@unrs/resolver-binding-darwin-arm64": "1.3.2", + "@unrs/resolver-binding-darwin-x64": "1.3.2", + "@unrs/resolver-binding-freebsd-x64": "1.3.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.3.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.3.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.3.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.3.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.3.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.3.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.3.2", + "@unrs/resolver-binding-linux-x64-musl": "1.3.2", + "@unrs/resolver-binding-wasm32-wasi": "1.3.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.3.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.3.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.3.2" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -6163,6 +6403,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 7fc7ab29..53255b5c 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,23 @@ }, "dependencies": { "@tanstack/react-query": "^5.68.0", - "autoprefixer": "^10.4.21", "axios": "^1.8.3", - "next": "15.2.2", - "postcss": "^8.5.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lodash": "^4.17.21", + "lucide-react": "^0.485.0", + "moment": "^2.30.1", + "next": "^15.2.4", "prettier-plugin-tailwindcss": "^0.6.11", "react": "^19.0.0", + "react-datepicker": "^8.2.1", + "react-datetime": "^3.3.1", "react-dom": "^19.0.0", - "react-hook-form": "^7.54.2" + "react-hook-form": "^7.54.2", + "react-intersection-observer": "^9.16.0", + "react-toastify": "^11.0.5", + "tostify": "^0.0.1", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -25,12 +34,14 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "autoprefixer": "^10.4.21", "eslint": "^9.22.0", "eslint-config-next": "^15.2.2", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.3", + "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^4.0.14", - "typescript": "^5" + "typescript": "^5.8.2" } } diff --git a/postcss.config.mjs b/postcss.config.mjs index 12a703d9..df2cc950 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,6 +1,6 @@ -module.exports = { +/** @type {import('tailwindcss').Config} */ +export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/public/images/dashboard.webp b/public/images/dashboard.webp deleted file mode 100644 index b02cdea8..00000000 Binary files a/public/images/dashboard.webp and /dev/null differ diff --git a/public/images/desktop.webp b/public/images/desktop.webp deleted file mode 100644 index 6caab525..00000000 Binary files a/public/images/desktop.webp and /dev/null differ diff --git a/public/images/invite.webp b/public/images/invite.webp deleted file mode 100644 index fdcc9986..00000000 Binary files a/public/images/invite.webp and /dev/null differ diff --git a/public/images/landing1.png b/public/images/landing1.png new file mode 100644 index 00000000..281bfe5b Binary files /dev/null and b/public/images/landing1.png differ diff --git a/public/images/landing2.png b/public/images/landing2.png new file mode 100644 index 00000000..f2ef67e1 Binary files /dev/null and b/public/images/landing2.png differ diff --git a/public/images/landing3.png b/public/images/landing3.png new file mode 100644 index 00000000..6bcfa8bb Binary files /dev/null and b/public/images/landing3.png differ diff --git a/public/images/landing4.png b/public/images/landing4.png new file mode 100644 index 00000000..952ec27b Binary files /dev/null and b/public/images/landing4.png differ diff --git a/public/images/landing5.png b/public/images/landing5.png new file mode 100644 index 00000000..3a9b163e Binary files /dev/null and b/public/images/landing5.png differ diff --git a/public/images/landing_hero.png b/public/images/landing_hero.png new file mode 100644 index 00000000..5c08a1b0 Binary files /dev/null and b/public/images/landing_hero.png differ diff --git a/public/images/members.webp b/public/images/members.webp deleted file mode 100644 index 325ca886..00000000 Binary files a/public/images/members.webp and /dev/null differ diff --git a/public/images/modal.webp b/public/images/modal.webp deleted file mode 100644 index 3d4e4533..00000000 Binary files a/public/images/modal.webp and /dev/null differ diff --git a/public/images/setting.webp b/public/images/setting.webp deleted file mode 100644 index 976b92b5..00000000 Binary files a/public/images/setting.webp and /dev/null differ diff --git a/public/svgs/Taskify-white.svg b/public/svgs/Taskify-white.svg new file mode 100644 index 00000000..81aca8dc --- /dev/null +++ b/public/svgs/Taskify-white.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/svgs/Taskify.svg b/public/svgs/Taskify.svg new file mode 100644 index 00000000..785c9464 --- /dev/null +++ b/public/svgs/Taskify.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/svgs/add-box copy.svg b/public/svgs/add-box copy.svg new file mode 100644 index 00000000..6871589d --- /dev/null +++ b/public/svgs/add-box copy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/add-box-white.svg b/public/svgs/add-box-white.svg new file mode 100644 index 00000000..601bcce1 --- /dev/null +++ b/public/svgs/add-box-white.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/add-box.svg b/public/svgs/add-box.svg index e7930fb6..d947b44b 100644 --- a/public/svgs/add-box.svg +++ b/public/svgs/add-box.svg @@ -1,3 +1,5 @@ - - + + + + \ No newline at end of file diff --git a/public/svgs/add_white_box.svg b/public/svgs/add_white_box.svg new file mode 100644 index 00000000..444bf059 --- /dev/null +++ b/public/svgs/add_white_box.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/svgs/arrow-backward-black.svg b/public/svgs/arrow-backward-black.svg new file mode 100644 index 00000000..3dfe4900 --- /dev/null +++ b/public/svgs/arrow-backward-black.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/arrow-backward-white.svg b/public/svgs/arrow-backward-white.svg new file mode 100644 index 00000000..e3c29a18 --- /dev/null +++ b/public/svgs/arrow-backward-white.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/arrow-down-black.svg b/public/svgs/arrow-down-black.svg new file mode 100644 index 00000000..fa57d9be --- /dev/null +++ b/public/svgs/arrow-down-black.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/svgs/arrow-forward-black.svg b/public/svgs/arrow-forward-black.svg new file mode 100644 index 00000000..e17089d5 --- /dev/null +++ b/public/svgs/arrow-forward-black.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/arrow-forward-white.svg b/public/svgs/arrow-forward-white.svg new file mode 100644 index 00000000..9d2a62cf --- /dev/null +++ b/public/svgs/arrow-forward-white.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/arrow_backward_white.svg b/public/svgs/arrow_backward_white.svg new file mode 100644 index 00000000..2007391c --- /dev/null +++ b/public/svgs/arrow_backward_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/svgs/arrow_forward_white.svg b/public/svgs/arrow_forward_white.svg new file mode 100644 index 00000000..d9aeb599 --- /dev/null +++ b/public/svgs/arrow_forward_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/svgs/calendar.svg b/public/svgs/calendar.svg index e4c342e8..76a6bcc2 100644 --- a/public/svgs/calendar.svg +++ b/public/svgs/calendar.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/public/svgs/check.svg b/public/svgs/check.svg index 82b48acd..6b0636e1 100644 --- a/public/svgs/check.svg +++ b/public/svgs/check.svg @@ -1,3 +1,5 @@ - - - \ No newline at end of file + + + + + diff --git a/public/svgs/close-white.svg b/public/svgs/close-white.svg new file mode 100644 index 00000000..4b837306 --- /dev/null +++ b/public/svgs/close-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/svgs/close.svg b/public/svgs/close.svg index 9669befe..9dc53ed4 100644 --- a/public/svgs/close.svg +++ b/public/svgs/close.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/public/svgs/cross-button.svg b/public/svgs/cross-button.svg new file mode 100644 index 00000000..b65c8009 --- /dev/null +++ b/public/svgs/cross-button.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/crown.svg b/public/svgs/crown.svg index 3fc4645f..50559d61 100644 --- a/public/svgs/crown.svg +++ b/public/svgs/crown.svg @@ -1,10 +1,10 @@ - - - + + + - - + + diff --git a/public/svgs/envelope.svg b/public/svgs/envelope.svg new file mode 100644 index 00000000..85949305 --- /dev/null +++ b/public/svgs/envelope.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/eye.svg b/public/svgs/eye.svg new file mode 100644 index 00000000..a03eff31 --- /dev/null +++ b/public/svgs/eye.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/facebook.svg b/public/svgs/facebook.svg index a4e77897..b2410d64 100644 --- a/public/svgs/facebook.svg +++ b/public/svgs/facebook.svg @@ -1,3 +1,5 @@ - + + + diff --git a/public/svgs/img.svg b/public/svgs/img.svg new file mode 100644 index 00000000..7d53a302 --- /dev/null +++ b/public/svgs/img.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/svgs/instagram.svg b/public/svgs/instagram.svg index 52b916e4..4165f763 100644 --- a/public/svgs/instagram.svg +++ b/public/svgs/instagram.svg @@ -1,11 +1,17 @@ - - - - + + + + + + + + + + - + diff --git a/public/svgs/kebab.svg b/public/svgs/kebab.svg index 78f1aa00..a921bb4d 100644 --- a/public/svgs/kebab.svg +++ b/public/svgs/kebab.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/public/svgs/logo-large-white.svg b/public/svgs/logo-large-white.svg new file mode 100644 index 00000000..28a66417 --- /dev/null +++ b/public/svgs/logo-large-white.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/logo-large.svg b/public/svgs/logo-large.svg index 0f0a8104..d980528c 100644 --- a/public/svgs/logo-large.svg +++ b/public/svgs/logo-large.svg @@ -1,11 +1,18 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/public/svgs/logo-small-white.svg b/public/svgs/logo-small-white.svg new file mode 100644 index 00000000..65e04c1e --- /dev/null +++ b/public/svgs/logo-small-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/svgs/logo-white.svg b/public/svgs/logo-white.svg new file mode 100644 index 00000000..cb3955d9 --- /dev/null +++ b/public/svgs/logo-white.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/svgs/logo.svg b/public/svgs/logo.svg new file mode 100644 index 00000000..0dda9cef --- /dev/null +++ b/public/svgs/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/svgs/large-logo.svg b/public/svgs/logo_taskify.svg similarity index 100% rename from public/svgs/large-logo.svg rename to public/svgs/logo_taskify.svg diff --git a/public/svgs/none-eye.svg b/public/svgs/none-eye.svg new file mode 100644 index 00000000..d69a99d5 --- /dev/null +++ b/public/svgs/none-eye.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/plus.svg b/public/svgs/plus.svg new file mode 100644 index 00000000..c12fac51 --- /dev/null +++ b/public/svgs/plus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/svgs/purple-circle.svg b/public/svgs/purple-circle.svg new file mode 100644 index 00000000..3fe33f47 --- /dev/null +++ b/public/svgs/purple-circle.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/svgs/search.svg b/public/svgs/search.svg index d67cbffa..80c14fd9 100644 --- a/public/svgs/search.svg +++ b/public/svgs/search.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/public/svgs/setting.svg b/public/svgs/setting.svg new file mode 100644 index 00000000..eae8bde6 --- /dev/null +++ b/public/svgs/setting.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/svgs/stroke.svg b/public/svgs/stroke.svg new file mode 100644 index 00000000..478fe600 --- /dev/null +++ b/public/svgs/stroke.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/svgs/unsubscribe.svg b/public/svgs/unsubscribe.svg index 6deed210..bfb3aed2 100644 --- a/public/svgs/unsubscribe.svg +++ b/public/svgs/unsubscribe.svg @@ -1,3 +1,5 @@ - + + + diff --git a/public/svgs/x-button.svg b/public/svgs/x-button.svg new file mode 100644 index 00000000..efafb3c5 --- /dev/null +++ b/public/svgs/x-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/x-button2.svg b/public/svgs/x-button2.svg new file mode 100644 index 00000000..5bc2a6c0 --- /dev/null +++ b/public/svgs/x-button2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/api/apiRoutes.ts b/src/api/apiRoutes.ts new file mode 100644 index 00000000..1d840fd6 --- /dev/null +++ b/src/api/apiRoutes.ts @@ -0,0 +1,48 @@ +import { TEAM_ID } from "@/constants/team"; + +export const apiRoutes = { + // 로그인 + login: () => `/${TEAM_ID}/auth/login`, //post + + // 비밀번호 변경 + password: () => `/${TEAM_ID}/auth/password`, //put + + // 카드 + cards: () => `/${TEAM_ID}/cards`, //post,get + cardDetail: (cardId: number) => `/${TEAM_ID}/cards/${cardId}`, //get,put,delete + + // 칼럼 + columns: () => `/${TEAM_ID}/columns`, //post,get + columnDetail: (columnId: number) => `/${TEAM_ID}/columns/${columnId}`, //put,delete + columnCardImage: (columnId: number) => + `/${TEAM_ID}/columns/${columnId}/card-image`, //post + + // 댓글 + comments: () => `/${TEAM_ID}/comments`, //post,get + commentsDetail: (commentId: number) => `/${TEAM_ID}/comments/${commentId}`, //put,delete + + // 대시보드 + dashboards: () => `/${TEAM_ID}/dashboards`, //post,get + dashboardDetail: (dashboardId: number) => + `/${TEAM_ID}/dashboards/${dashboardId}`, //get,put,delete + + // 대시보드 초대하기 + dashboardInvite: (dashboardId: number) => + `/${TEAM_ID}/dashboards/${dashboardId}/invitations`, //post,get + dashboardInviteDelete: (dashboardId: number, invitationId: number) => + `/${TEAM_ID}/dashboards/${dashboardId}/invitations/${invitationId}`, //delete + + // 초대받은 대시보드 + invitations: () => `/${TEAM_ID}/invitations`, //get + invitationDetail: (invitationId: number) => + `/${TEAM_ID}/invitations/${invitationId}`, //put + + // Members + members: () => `/${TEAM_ID}/members`, //get + memberDetail: (memberId: number) => `/${TEAM_ID}/members/${memberId}`, //delete + + // Users + users: () => `/${TEAM_ID}/users`, //post + usersMe: () => `/${TEAM_ID}/users/me`, //get,put + userMeImage: () => `/${TEAM_ID}/users/me/image`, //post +}; diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 00000000..e180c776 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,25 @@ +import axiosInstance from "./axiosInstance"; +import { UserType } from "@/types/users"; +import { TEAM_ID } from "@/constants/team"; + +interface AuthResponse extends UserType { + accessToken: string; +} + +export const postAuthData = async ({ + email, + password, +}: { + email: string; + password: string; +}) => { + const response = await axiosInstance.post( + `/${TEAM_ID}/auth/login`, + { + email, + password, + } + ); + + return response.data; +}; diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts new file mode 100644 index 00000000..af863383 --- /dev/null +++ b/src/api/axiosInstance.ts @@ -0,0 +1,27 @@ +// axiosInstance.ts + +import axios from "axios"; + +const axiosInstance = axios.create({ + baseURL: "https://sp-taskify-api.vercel.app", +}); + +// 👉 Authorization 헤더 자동 설정, 요청 보낼때 마다 localstorage에서 토큰 가져오기 +axiosInstance.interceptors.request.use((config) => { + const token = localStorage.getItem("accessToken"); // localStorage에서 토큰 가져오기 + if (token) { + config.headers.Authorization = `Bearer ${token}`; // 헤더에 Authorization 추가 + } + return config; +}); + +// 👉 요청 보낼 때마다 토큰 자동 추가 +axiosInstance.interceptors.request.use((config) => { + const token = localStorage.getItem("accessToken"); // localStorage에서 토큰 가져오기 + if (token) { + config.headers.Authorization = `Bearer ${token}`; // 헤더에 Authorization 추가 + } + return config; +}); + +export default axiosInstance; diff --git a/src/api/card.ts b/src/api/card.ts new file mode 100644 index 00000000..afc6bb9d --- /dev/null +++ b/src/api/card.ts @@ -0,0 +1,169 @@ +import axiosInstance from "./axiosInstance"; +import type { CardDetailType } from "@/types/cards"; // Dashboard 타입 import +import { apiRoutes } from "@/api/apiRoutes"; +import { TEAM_ID } from "@/constants/team"; + +/** 1. 카드 이미지 업로드 */ +export const uploadCardImage = async ({ + columnId, + imageFile, +}: { + columnId: number; + imageFile: File; +}): Promise => { + const formData = new FormData(); + formData.append("image", imageFile); + + const response = await axiosInstance.post( + `/${TEAM_ID}/columns/${columnId}/card-image`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + + return response.data.imageUrl; +}; + +/** 2. 카드 생성 */ +export const createCard = async ({ + assigneeUserId, + dashboardId, + columnId, + title, + description, + dueDate, + tags, + imageUrl, +}: { + assigneeUserId: number; + dashboardId: number; + columnId: number; + title: string; + description: string; + dueDate: string; + tags: string[]; + imageUrl?: string; +}) => { + const response = await axiosInstance.post(`/${TEAM_ID}/cards`, { + assigneeUserId, + dashboardId, + columnId, + title, + description, + dueDate, + tags, + imageUrl, + }); + + return response.data; +}; + +/** 3. 대시보드 멤버 조회 (담당자용) */ +export const getDashboardMembers = async ({ + dashboardId, + page = 1, + size = 20, +}: { + dashboardId: number; + page?: number; + size?: number; +}) => { + const res = await axiosInstance.get(apiRoutes.members(), { + params: { + page, + size, + dashboardId, + }, + }); + + return res.data.members; +}; + +/** 4. 카드 수정 */ +export const updateCard = async ( + id: number, + data: Partial, + { + cardId, + columnId, + assigneeUserId, + title, + description, + dueDate, + tags, + imageUrl, + }: { + cardId: number; + columnId: number; + assigneeUserId: number; + title: string; + description: string; + dueDate: string; + tags: string[]; + imageUrl?: string; + } +) => { + const response = await axiosInstance.put(apiRoutes.cardDetail(cardId), { + columnId, + assigneeUserId, + title, + description, + dueDate, + tags, + imageUrl, + }); + + return response.data; +}; + +// 카드 목록 조회 +export const getCardsByColumn = async ({ + columnId, + cursorId, + size = 10, +}: { + columnId: number; + cursorId?: number; + size?: number; +}) => { + const res = await axiosInstance.get(apiRoutes.cards(), { + params: { + columnId, + cursorId, + size, + }, + }); + + return res.data; +}; + +// 카드 상세 조회 +export async function getCardDetail(cardId: number): Promise { + try { + // apiRoutes를 사용하여 URL 동적 생성 + const url = apiRoutes.cardDetail(cardId); + const response = await axiosInstance.get(url); + return response.data as CardDetailType; + } catch (error) { + console.error("대시보드 데이터를 불러오는 데 실패했습니다.", error); + throw error; + } +} + +// 카드 삭제 +export const deleteCard = async (cardId: number) => { + const url = apiRoutes.cardDetail(cardId); + const response = await axiosInstance.delete(url); + return response.data; +}; +//카드 수정저장 +export const EditCard = async ( + cardId: number, + data: Partial +) => { + const response = await axiosInstance.put(apiRoutes.cardDetail(cardId), data); + return response.data; +}; diff --git a/src/api/changepassword.ts b/src/api/changepassword.ts new file mode 100644 index 00000000..a424071e --- /dev/null +++ b/src/api/changepassword.ts @@ -0,0 +1,32 @@ +import axios from "./axiosInstance"; +import { apiRoutes } from "./apiRoutes"; +import { isAxiosError } from "axios"; + +// 비밀번호 변경 +export const changePassword = async ({ + password, + newPassword, +}: { + password: string; + newPassword: string; +}) => { + try { + const response = await axios.put(apiRoutes.password(), { + password, + newPassword, + }); + return { success: true, data: response.data }; + } catch (error) { + if (isAxiosError(error)) { + return { + success: false, + status: error.response?.status, + message: error.response?.data?.message || "알 수 없는 오류", + }; + } + return { + success: false, + message: "에러가 발생했습니다.", + }; + } +}; diff --git a/src/api/columns.ts b/src/api/columns.ts new file mode 100644 index 00000000..dbfd4fcb --- /dev/null +++ b/src/api/columns.ts @@ -0,0 +1,65 @@ +import { ColumnType } from "@/types/task"; +import axiosInstance from "./axiosInstance"; +import { apiRoutes } from "./apiRoutes"; +import { TEAM_ID } from "@/constants/team"; + +// 칼럼 생성 +export const createColumn = async ({ + title, + dashboardId, +}: { + title: string; + dashboardId: number; +}): Promise => { + const res = await axiosInstance.post(`/${TEAM_ID}/columns`, { + title, + dashboardId, + }); + + return res.data; +}; + +// 칼럼 목록 조회 +export const getColumns = async ({ dashboardId }: { dashboardId: number }) => { + const res = await axiosInstance.get(apiRoutes.columns(), { + params: { + dashboardId, + }, + }); + + return res.data; +}; + +// 칼럼 수정 +export const updateColumn = async ({ + columnId, + title, +}: { + columnId: number; + title: string; +}) => { + const res = await axiosInstance.put(apiRoutes.columnDetail(columnId), { + title, + }); + return res.data; +}; + +// 칼럼 삭제 +export const deleteColumn = async ({ columnId }: { columnId: number }) => { + const res = await axiosInstance.delete(apiRoutes.columnDetail(columnId)); + return res; +}; + +export const getColumn = async ({ + dashboardId, +}: { + dashboardId: number; + columnId: number; +}) => { + const res = await axiosInstance.get(apiRoutes.columns(), { + params: { + dashboardId, + }, + }); + return res.data.data; +}; diff --git a/src/api/comment.ts b/src/api/comment.ts new file mode 100644 index 00000000..9b929531 --- /dev/null +++ b/src/api/comment.ts @@ -0,0 +1,54 @@ +import { apiRoutes } from "./apiRoutes"; +import axiosInstance from "./axiosInstance"; +import { + CreateCommentType, + UpdateCommenttype, + DeleteCommentParams, +} from "../types/comments"; +import { TEAM_ID } from "@/constants/team"; + +// 댓글 생성 +export const createComment = async (data: CreateCommentType) => { + const response = await axiosInstance.post(`/${TEAM_ID}/comments`, data); + return response.data; +}; + +// 댓글 목록 +export async function getComments({ + cardId, + pageParam, +}: { + cardId: number; + pageParam: number; +}) { + const response = await axiosInstance.get(apiRoutes.comments(), { + params: { + cardId, + page: pageParam, + }, + }); + + return { + comments: response.data.comments, + nextPage: response.data.nextPage, + }; +} + +// 댓글 수정 +export const updateComment = async ( + commentId: number, + data: UpdateCommenttype +) => { + const response = await axiosInstance.put( + apiRoutes.commentsDetail(commentId), + data + ); + return response.data; +}; +// 댓글 삭제 +export const deleteComment = async ({ commentId }: DeleteCommentParams) => { + const response = await axiosInstance.delete( + apiRoutes.commentsDetail(commentId) + ); + return response.data; +}; diff --git a/src/api/dashboards.ts b/src/api/dashboards.ts new file mode 100644 index 00000000..825f6e69 --- /dev/null +++ b/src/api/dashboards.ts @@ -0,0 +1,69 @@ +import axiosInstance from "./axiosInstance"; +import { apiRoutes } from "./apiRoutes"; +import { TEAM_ID } from "@/constants/team"; + +export interface Dashboard { + id: number; + title: string; + color: string; + userId: number; + createdAt: string; + updatedAt: string; + createdByMe: boolean; +} + +// 대시보드 생성 (POST) +export const createDashboard = async ({ + title, + color, +}: { + title: string; + color: string; +}) => { + const res = await axiosInstance.post(`/${TEAM_ID}/dashboards`, { + title, + color, + }); + return res.data; +}; + +// 대시보드 목록 조회 (GET) +export const getDashboards = async ({ + navigationMethod = "pagination", + page = 1, + size = 100, +}: { + navigationMethod?: "pagination"; + page?: number; + size?: number; +}): Promise<{ + dashboards: Dashboard[]; + totalCount: number; + cursorId: string | null; +}> => { + const res = await axiosInstance.get(apiRoutes.dashboards(), { + params: { navigationMethod, page, size }, + }); + + return res.data; +}; + +// 대시보드 상세 조회 (GET) +export const getDashboardById = async ({ + dashboardId, +}: { + dashboardId: number; +}) => { + const res = await axiosInstance.get(apiRoutes.dashboardDetail(dashboardId)); + return res.data; +}; + +// 대시보드 수정 (PUT) + +// 대시보드 삭제 (DELETE) + +// 대시보드 초대하기 (POST) + +// 대시보드 초대 불러오기 (GET) + +// 대시보드 초대 취소 (DELETE) diff --git a/src/api/members.ts b/src/api/members.ts new file mode 100644 index 00000000..b37f1e0d --- /dev/null +++ b/src/api/members.ts @@ -0,0 +1,22 @@ +import axiosInstance from "./axiosInstance"; +import { apiRoutes } from "./apiRoutes"; + +// 대시보드 멤버 목록 조회 +export const getMembers = async ({ dashboardId }: { dashboardId: number }) => { + if (!dashboardId) { + console.error("dashboardID가 없습니다."); + return []; + } + const response = await axiosInstance.get(apiRoutes.members(), { + params: { + dashboardId, + }, + }); + return response.data.members || []; +}; + +// 대시보드 멤버 삭제 +export const deleteMembers = async (memberId: number) => { + const response = await axiosInstance.delete(apiRoutes.memberDetail(memberId)); + return response.data; +}; diff --git a/src/api/statusOptions.ts b/src/api/statusOptions.ts new file mode 100644 index 00000000..7d35ff40 --- /dev/null +++ b/src/api/statusOptions.ts @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import axiosInstance from "./axiosInstance"; +import { apiRoutes } from "./apiRoutes"; + +export interface StatusOption { + label: string; + value: number; +} + +interface Column { + id: number; + title: string; +} + +export const useStatusOptions = () => { + const router = useRouter(); + const [statusOptions, setStatusOptions] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const teamId = router.query.teamId as string; + const dashboardId = Number(router.query.dashboardId); + + if (!teamId || isNaN(dashboardId)) { + console.warn("❗ teamId 또는 dashboardId가 유효하지 않습니다."); + return; + } + + const fetch = async () => { + try { + const res = await axiosInstance.get(apiRoutes.columns(), { + params: { dashboardId }, + }); + + const options = (res.data.data as Column[]).map((col) => ({ + label: col.title, + value: col.id, + })); + + setStatusOptions(options); + } catch (err) { + console.error("상태 목록 가져오기 실패:", err); + } finally { + setLoading(false); + } + }; + + fetch(); + }, [router.query.teamId, router.query.dashboardId]); + + return { statusOptions, loading }; +}; diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 00000000..934928c7 --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,52 @@ +import axiosInstance from "./axiosInstance"; +import { apiRoutes } from "./apiRoutes"; +import { UpdateUser, UserMeImage } from "@/types/users"; +import { UserType } from "@/types/users"; +import { TEAM_ID } from "@/constants/team"; + +interface SignUpRequest { + email: string; + nickname: string; + password: string; +} + +// 회원가입 (POST) +export const signUp = async ({ payload }: { payload: SignUpRequest }) => { + const response = await axiosInstance.post( + `/${TEAM_ID}/users`, + payload + ); + return response.data; +}; + +// 내 정보 조회 (GET) +export const getUserInfo = async () => { + const response = await axiosInstance.get(apiRoutes.usersMe()); + return response.data; +}; + +// 내 정보 수정 (PUT) +export const updateProfile = async (data: UpdateUser) => { + const res = await axiosInstance.put(apiRoutes.usersMe(), data, { + headers: { + "Content-Type": "application/json", + }, + }); + return res.data; +}; + +// 프로필 이미지 업로드 (POST) +export const uploadProfileImage = async ( + formData: FormData +): Promise => { + const response = await axiosInstance.post( + `/${TEAM_ID}/users/me/image`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + return response.data; +}; diff --git a/src/components/Layouts/DashboardLayout.tsx b/src/components/Layouts/DashboardLayout.tsx new file mode 100644 index 00000000..283eb82d --- /dev/null +++ b/src/components/Layouts/DashboardLayout.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import HeaderDashboard from "@/components/gnb/HeaderDashboard"; +import SideMenu from "@/components/sideMenu/SideMenu"; +import { DashboardType } from "@/types/dashboard"; +import { TEAM_ID } from "@/constants/team"; + +interface DashboardLayoutProps { + children: React.ReactNode; + dashboardList: DashboardType[]; + dashboardId?: string | string[]; +} + +const DashboardLayout = ({ + children, + dashboardList, + dashboardId, +}: DashboardLayoutProps) => { + return ( +
+ +
+ +
+ {children} +
+
+
+ ); +}; + +export default DashboardLayout; diff --git a/src/components/button/CardButton.tsx b/src/components/button/CardButton.tsx new file mode 100644 index 00000000..a60009b6 --- /dev/null +++ b/src/components/button/CardButton.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import { useRouter } from "next/router"; +import clsx from "clsx"; +import Image from "next/image"; + +interface CardButtonProps extends React.ButtonHTMLAttributes { + title?: string; + showCrown?: boolean; + color?: string; + isEditMode?: boolean; + dashboardId: number; + createdByMe?: boolean; + onDeleteClick?: (id: number) => void; + onLeaveClick?: (id: number) => void; +} + +const CardButton: React.FC = ({ + className, + title = "비브리지", + showCrown = true, + color = "#7ac555", // 기본 색상 + isEditMode = false, + dashboardId, + createdByMe, + onDeleteClick, + onLeaveClick, + ...props +}) => { + const router = useRouter(); + + const handleCardClick = (e: React.MouseEvent) => { + // 관리 상태에서 카드 클릭 이벤트 차단 + if (isEditMode) { + e.preventDefault(); + return; + } + // 카드 클릭 시 해당 대시보드로 이동 + router.push(`/dashboard/${dashboardId}`); + }; + + const handleEdit = (e: React.MouseEvent) => { + e.stopPropagation(); + router.push(`/dashboard/${dashboardId}/edit`); + }; + + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + if (createdByMe) { + // 실제 삭제 API 요청 + if (onDeleteClick) onDeleteClick(dashboardId); + } else { + // 나만 탈퇴 + if (onLeaveClick) onLeaveClick(dashboardId); + } + }; + + return ( +
+ {/* 왼쪽: 색상 도트 + 제목 + 왕관 */} +
+ {/* 색상 원 */} + + + + + {/* 제목 */} + + {title} + + + {/* 왕관 */} + {showCrown && ( + crown Icon + )} +
+ + {/* 오른쪽: 화살표 아이콘 or 수정/삭제 버튼 */} + {isEditMode ? ( +
+ {createdByMe && ( + + )} + +
+ ) : ( + arrow icon + )} +
+ ); +}; + +export default CardButton; diff --git a/src/components/button/ColumnsButton.tsx b/src/components/button/ColumnsButton.tsx new file mode 100644 index 00000000..12c3a2b6 --- /dev/null +++ b/src/components/button/ColumnsButton.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import clsx from "clsx"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + fullWidth?: boolean; +} + +const ColumnsButton: React.FC = ({ + fullWidth, + className, + children = "새로운 컬럼 추가하기", + ...props +}) => { + return ( + + ); +}; + +export default ColumnsButton; diff --git a/src/components/button/CustomButton.tsx b/src/components/button/CustomButton.tsx new file mode 100644 index 00000000..a59683aa --- /dev/null +++ b/src/components/button/CustomButton.tsx @@ -0,0 +1,61 @@ +import React from "react"; + +interface CustomBtnProps extends React.ButtonHTMLAttributes { + iAmOptional?: string; + children: React.ReactNode; + variant?: "primary" | "primaryDisabled" | "outline" | "outlineDisabled"; + size?: + | "large" + | "medium" + | "small" + | "tabletSmall" + | "mobileMedium" + | "mobileSmall"; + disabled?: boolean; +} + +export const CustomBtn: React.FC = ({ + children, + variant = "primary", + size = "medium", + disabled = false, + ...props +}) => { + const baseStyle = + "flex justify-center items-center rounded-lg cursor-pointer transition"; + + const sizeStyles: Record, string> = { + large: "w-[520px] h-[50px] font-18m", + medium: "w-[256px] h-[54px] font-16sb", + small: "w-[84px] h-[32px] font-14m rounded-sm", + tabletSmall: "w-[72px] h-[30px] font-14m rounded-sm", + mobileMedium: "w-[144px] h-[54px] font-16m rounded-lg", + mobileSmall: "w-[109px] h-[32px] font-12m rounded-sm", + }; + + const variantStyles: Record< + NonNullable, + string + > = { + primary: "bg-[var(--primary)] text-white", + primaryDisabled: "bg-[var(--color-gray2)] text-white", + outline: "border border-[var(--color-gray3)] text-[var(--primary)]", + outlineDisabled: "border border-[var(--color-gray3)] text-gray1", + }; + + const finalStyle = `${baseStyle} ${sizeStyles[size]} ${ + disabled + ? variant === "outlineDisabled" + ? variantStyles.outlineDisabled + : variant === "primaryDisabled" + ? variantStyles.primaryDisabled + : variantStyles.primaryDisabled + : variantStyles[variant] + }`; + + return ( + + ); +}; diff --git a/src/components/button/DashboardAddButton.tsx b/src/components/button/DashboardAddButton.tsx new file mode 100644 index 00000000..c0683c95 --- /dev/null +++ b/src/components/button/DashboardAddButton.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import clsx from "clsx"; +import Image from "next/image"; + +const DashboardAddButton: React.FC< + React.ButtonHTMLAttributes +> = ({ className, children = "새로운 대시보드", ...props }) => { + return ( + + ); +}; + +export default DashboardAddButton; diff --git a/src/components/button/DashboardDeleteButton.tsx b/src/components/button/DashboardDeleteButton.tsx new file mode 100644 index 00000000..3cf411b9 --- /dev/null +++ b/src/components/button/DashboardDeleteButton.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import clsx from "clsx"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + fullWidth?: boolean; + iconSrc?: string; +} + +const DashboardDeleteButton: React.FC = ({ + fullWidth = false, + className, + children = "대시보드 삭제하기", + ...props +}) => { + return ( + + ); +}; + +export default DashboardDeleteButton; diff --git a/src/components/button/PaginationButton.tsx b/src/components/button/PaginationButton.tsx new file mode 100644 index 00000000..2c22d539 --- /dev/null +++ b/src/components/button/PaginationButton.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +interface PaginationButtonProps + extends React.ButtonHTMLAttributes { + direction?: "left" | "right"; + disabled?: boolean; +} + +export const PaginationButton: React.FC = ({ + direction = "left", + disabled = false, + ...props +}) => { + const baseStyle = + "w-[40px] h-[40px] flex justify-center items-center border border-[var(--color-gray3)] rounded-md text-[16px] font-medium transition"; + + const enabledTextColor = + "text-[var(--color-gray1)] bg-white hover:bg-[var(--color-gray5)] cursor-pointer"; + const disabledTextColor = "text-[var(--color-gray3)] cursor-default"; + const finalStyle = `${baseStyle} ${disabled ? disabledTextColor : enabledTextColor}`; + + return ( + + ); +}; diff --git a/src/components/button/TodoButton.tsx b/src/components/button/TodoButton.tsx new file mode 100644 index 00000000..592c77b2 --- /dev/null +++ b/src/components/button/TodoButton.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import clsx from "clsx"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + fullWidth?: boolean; +} + +const TodoButton: React.FC = ({ + fullWidth = false, + className, + children, + ...props +}) => { + return ( + + ); +}; + +export default TodoButton; diff --git a/src/components/card/ChangePassword.tsx b/src/components/card/ChangePassword.tsx new file mode 100644 index 00000000..7cfff3bc --- /dev/null +++ b/src/components/card/ChangePassword.tsx @@ -0,0 +1,117 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { changePassword } from "@/api/changepassword"; +import Input from "@/components/input/Input"; +import { toast } from "react-toastify"; + +export default function ChangePassword() { + const router = useRouter(); + const [password, setPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [checkNewpassword, setCheckNewPassword] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const isPasswordMismatch = + !!checkNewpassword && checkNewpassword !== newPassword; + const isDisabled = + !password || + !newPassword || + !checkNewpassword || + isPasswordMismatch || + password.length < 8; + + const handleChangePassword = async () => { + if (isDisabled) return; + + setIsSubmitting(true); + + const result = await changePassword({ password, newPassword }); + + if (!result.success) { + const msg = + result.status === 400 + ? result.message || "현재 비밀번호가 올바르지 않습니다." + : "비밀번호 변경 중 오류가 발생했습니다."; + toast.error(msg); + setIsSubmitting(false); + + return; + } + toast.success("비밀번호가 변경되었습니다."); + setPassword(""); + setNewPassword(""); + setCheckNewPassword(""); + setIsSubmitting(false); + setTimeout(() => { + router.reload(); + }, 1500); + }; + + return ( +
+

+ 비밀번호 변경 +

+ +
+ + + setCheckNewPassword(value)} + forceInvalid={isPasswordMismatch} + invalidMessage="비밀번호가 일치하지 않습니다." + className="max-w-[624px]" + /> + {isPasswordMismatch && ( +

+ 비밀번호가 일치하지 않습니다. +

+ )} + + +
+
+ ); +} diff --git a/src/components/card/Profile.tsx b/src/components/card/Profile.tsx new file mode 100644 index 00000000..cc8395eb --- /dev/null +++ b/src/components/card/Profile.tsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import Image from "next/image"; +import { getUserInfo, updateProfile, uploadProfileImage } from "@/api/users"; +import Input from "@/components/input/Input"; +import { toast } from "react-toastify"; + +export default function ProfileCard() { + const router = useRouter(); + const [image, setImage] = useState(null); + const [nickname, setNickname] = useState(""); + const [email, setEmail] = useState(""); + const [preview, setPreview] = useState(null); + + const fetchUserData = async () => { + try { + const data = await getUserInfo(); + setImage(data.profileImageUrl); + setNickname(data.nickname); + setEmail(data.email); + } catch (err) { + console.error("유저 정보 불러오기 실패:", err); + } + }; + + const handleImageUpload = async ( + event: React.ChangeEvent + ) => { + const MAX_IMAGE_SIZE = 3.5 * 1024 * 1024; + + if (event.target.files && event.target.files[0]) { + const file = event.target.files[0]; + + if (file.size > MAX_IMAGE_SIZE) { + toast.error("3.5MB 이하만 등록 가능합니다."); + return; + } + + setPreview(URL.createObjectURL(file)); // 미리보기 + + try { + const formData = new FormData(); + formData.append("image", file); + + const response = await uploadProfileImage(formData); + setImage(response.profileImageUrl); // 서버에서 받은 URL 저장 + } catch (error) { + console.error("이미지 업로드 실패:", error); + toast.error("이미지 업로드에 실패하였습니다."); + } + } + }; + + const handleSave = async () => { + if (!nickname || !image) return; + + const userProfile = { + nickname, + profileImageUrl: image, + }; + + try { + await updateProfile(userProfile); + toast.success("프로필 변경이 완료되었습니다."); + setTimeout(() => { + router.reload(); + }, 1500); + } catch (error) { + console.error("프로필 변경 실패:", error); + toast.error("프로필 변경에 실패하였습니다."); + } + }; + + useEffect(() => { + fetchUserData(); + }, []); + + return ( +
+ {/* 프로필 제목 */} +

+ 프로필 +

+ {/* 프로필 이미지 및 입력 폼 영역 */} +
+ {/* 프로필 이미지 업로드 영역 */} +
+
+ +
+
+ + {/* 입력 폼 */} +
+ + setNickname(value)} + className="text-black4" + /> + +
+
+
+ ); +} diff --git a/src/components/columnCard/AddColumnModal.tsx b/src/components/columnCard/AddColumnModal.tsx new file mode 100644 index 00000000..c97c96a6 --- /dev/null +++ b/src/components/columnCard/AddColumnModal.tsx @@ -0,0 +1,64 @@ +// components/modal/AddColumnModal.tsx +import { Modal } from "@/components/modal/Modal"; +import Input from "@/components/input/Input"; +import { CustomBtn } from "@/components/button/CustomButton"; + +type AddColumnModalProps = { + isOpen: boolean; + onClose: () => void; + newColumnTitle: string; + setNewColumnTitle: (value: string) => void; + onSubmit: () => void; + isCreateDisabled: boolean; + invalidMessage: string; + pattern: string; +}; + +export default function AddColumnModal({ + isOpen, + onClose, + newColumnTitle, + setNewColumnTitle, + onSubmit, + isCreateDisabled, + invalidMessage, + pattern, +}: AddColumnModalProps) { + return ( + +
+

새 칼럼 생성

+ + + +
+ + 취소 + + + 생성 + +
+
+
+ ); +} diff --git a/src/components/columnCard/Card.tsx b/src/components/columnCard/Card.tsx new file mode 100644 index 00000000..a4da666f --- /dev/null +++ b/src/components/columnCard/Card.tsx @@ -0,0 +1,118 @@ +import { AssigneeType, CardType } from "@/types/task"; +import Image from "next/image"; + +type CardProps = CardType & { + imageUrl?: string | null; + assignee: AssigneeType; + onClick?: () => void; +}; + +export default function Card({ + title = "new Task", + dueDate, + tags, + assignee, + imageUrl, + onClick, +}: CardProps) { + return ( +
+ {/* 이미지 영역 */} + {imageUrl && ( +
+ Task Image +
+ )} + + {/* 텍스트 콘텐츠 영역 */} +
+ {/* 제목 */} +

+ {title} +

+ + {/* 태그 + 날짜 + 닉네임 */} +
+ {/* 태그들 */} +
+ {tags.map((tag, idx) => ( + + {tag} + + ))} +
+ + {/* 날짜 + 닉네임 */} +
+
+ calendar + {dueDate} +
+ {assignee.profileImageUrl ? ( + 프로필 이미지 + ) : ( +
+ {assignee.nickname[0]} +
+ )} +
+
+
+
+ ); +} diff --git a/src/components/columnCard/CardList.tsx b/src/components/columnCard/CardList.tsx new file mode 100644 index 00000000..0469486e --- /dev/null +++ b/src/components/columnCard/CardList.tsx @@ -0,0 +1,100 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { CardType } from "@/types/task"; +import Card from "./Card"; +import { getCardsByColumn } from "@/api/card"; + +type CardListProps = { + columnId: number; + teamId: string; + initialTasks: CardType[]; + onCardClick: (card: CardType) => void; +}; + +const ITEMS_PER_PAGE = 6; + +export default function CardList({ + columnId, + initialTasks, + onCardClick, +}: CardListProps) { + const [cards, setCards] = useState(initialTasks); + const [cursorId, setCursorId] = useState( + initialTasks.length > 0 ? initialTasks[initialTasks.length - 1].id : null + ); + const [hasMore, setHasMore] = useState(true); + const observerRef = useRef(null); + const isFetchingRef = useRef(false); + + /* cursorId 업데이트 방식 변경 */ + const fetchMoreCards = useCallback(async () => { + if (isFetchingRef.current || !hasMore) return; + + isFetchingRef.current = true; + + try { + const res = await getCardsByColumn({ + columnId, + size: ITEMS_PER_PAGE, + cursorId: cursorId ?? undefined, // 최신 cursorId 사용 + }); + + const newCards = res.cards as CardType[]; + + if (newCards.length > 0) { + setCards((prev) => { + const existingIds = new Set(prev.map((card) => card.id)); + const uniqueCards = newCards.filter( + (card) => !existingIds.has(card.id) + ); + return [...prev, ...uniqueCards]; + }); + + // cursorId 안전하게 업데이트 + setCursorId((prevCursorId) => { + const newCursor = newCards[newCards.length - 1]?.id ?? prevCursorId; + return newCursor; + }); + } + + if (newCards.length < ITEMS_PER_PAGE) { + setHasMore(false); + } + } catch (error) { + console.error("카드 로딩 실패:", error); + } finally { + isFetchingRef.current = false; + } + }, [columnId, cursorId, hasMore]); + + /* 무한 스크롤 */ + useEffect(() => { + if (!observerRef.current) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore) { + fetchMoreCards(); + } + }, + { threshold: 0.5 } + ); + + observer.observe(observerRef.current); + + return () => observer.disconnect(); + }, [fetchMoreCards, hasMore]); + + return ( +
+ {cards.map((task) => ( + onCardClick(task)} + /> + ))} + {hasMore &&
} +
+ ); +} diff --git a/src/components/columnCard/Column.tsx b/src/components/columnCard/Column.tsx new file mode 100644 index 00000000..3150e957 --- /dev/null +++ b/src/components/columnCard/Column.tsx @@ -0,0 +1,191 @@ +// Column.tsx +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { CardType } from "@/types/task"; +import TodoModal from "@/components/modalInput/ToDoModal"; +import TodoButton from "@/components/button/TodoButton"; +import ColumnManageModal from "@/components/columnCard/ColumnManageModal"; +import ColumnDeleteModal from "@/components/columnCard/ColumnDeleteModal"; +import { updateColumn, deleteColumn } from "@/api/columns"; +import { getDashboardMembers, getCardDetail } from "@/api/card"; +import { MemberType } from "@/types/users"; +import { TEAM_ID } from "@/constants/team"; +import CardList from "./CardList"; +import CardDetailModal from "@/components/modalDashboard/CardDetailModal"; +import { CardDetailType } from "@/types/cards"; +import { toast } from "react-toastify"; + +type ColumnProps = { + columnId: number; + title?: string; + tasks?: CardType[]; + dashboardId: number; +}; + +export default function Column({ + columnId, + title = "new Task", + tasks = [], + dashboardId, +}: ColumnProps) { + const [columnTitle, setColumnTitle] = useState(title); + const [isColumnModalOpen, setIsColumnModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isTodoModalOpen, setIsTodoModalOpen] = useState(false); + const [isCardDetailModalOpen, setIsCardDetailModalOpen] = useState(false); + const [selectedCard, setSelectedCard] = useState(null); + const [members, setMembers] = useState< + { id: number; userId: number; nickname: string }[] + >([]); + + // ✅ 멤버 불러오기 + useEffect(() => { + const fetchMembers = async () => { + try { + const result = await getDashboardMembers({ dashboardId }); + + const parsed = result.map((m: MemberType) => ({ + id: m.id, + userId: m.userId, + nickname: m.nickname || m.email, + })); + + setMembers(parsed); + } catch (error) { + console.error("멤버 불러오기 실패:", error); + } + }; + + fetchMembers(); + }, [dashboardId]); + + const handleEditColumn = async (newTitle: string) => { + if (!newTitle.trim()) { + toast.error("칼럼 이름을 입력해주세요."); + return; + } + + try { + const updated = await updateColumn({ columnId, title: newTitle }); + setColumnTitle(updated.title); + setIsColumnModalOpen(false); + toast.success("칼럼이 변경되었습니다."); + } catch (error) { + console.error("칼럼 이름 수정 실패:", error); + toast.error("칼럼 변경에 실패했습니다."); + } + }; + + const handleDeleteColumn = async () => { + try { + await deleteColumn({ columnId }); + setIsDeleteModalOpen(false); + toast.success("칼럼이 삭제되었습니다."); + } catch (error) { + console.error("칼럼 삭제 실패:", error); + toast.error("칼럼 삭제에 실패했습니다."); + } + }; + + const handleCardClick = async (cardId: number) => { + try { + const detail = await getCardDetail(cardId); + setSelectedCard(detail); + setIsCardDetailModalOpen(true); + } catch (e) { + console.error("카드 상세 불러오기 실패:", e); + } + }; + + return ( +
+ {/* 칼럼 헤더 */} +
+
+

+ {columnTitle} +

+ + {tasks.length} + +
+ setting icon setIsColumnModalOpen(true)} + /> +
+ + {/* 카드 영역 */} +
+
setIsTodoModalOpen(true)} className="mb-2"> + +
+ + {/* 무한스크롤 카드 리스트로 대체 */} +
+ handleCardClick(card.id)} + /> +
+
+ + {/* Todo 모달 */} + {isTodoModalOpen && ( + setIsTodoModalOpen(false)} + teamId={TEAM_ID} + dashboardId={dashboardId} + columnId={columnId} + members={members} + /> + )} + + {/* 칼럼 관리 모달 */} + setIsColumnModalOpen(false)} + onDeleteClick={() => { + setIsColumnModalOpen(false); + setIsDeleteModalOpen(true); + }} + columnTitle={columnTitle} + onEditSubmit={handleEditColumn} + /> + + {/* 칼럼 삭제 확인 모달 */} + setIsDeleteModalOpen(false)} + onDelete={handleDeleteColumn} + /> + + {/* 카드 상세 모달 */} + {isCardDetailModalOpen && selectedCard && ( + { + setIsCardDetailModalOpen(false); + setSelectedCard(null); + }} + /> + )} +
+ ); +} diff --git a/src/components/columnCard/ColumnDeleteModal.tsx b/src/components/columnCard/ColumnDeleteModal.tsx new file mode 100644 index 00000000..2e076ef9 --- /dev/null +++ b/src/components/columnCard/ColumnDeleteModal.tsx @@ -0,0 +1,36 @@ +// components/column/ColumnDeleteModal.tsx +import { Modal } from "../modal/Modal"; +import { CustomBtn } from "../button/CustomButton"; + +type ColumnDeleteModalProps = { + isOpen: boolean; + onClose: () => void; + onDelete: () => void; +}; + +export default function ColumnDeleteModal({ + isOpen, + onClose, + onDelete, +}: ColumnDeleteModalProps) { + return ( + +
+

칼럼의 모든 카드가 삭제됩니다.

+
+ + 취소 + + + 삭제 + +
+
+
+ ); +} diff --git a/src/components/columnCard/ColumnManageModal.tsx b/src/components/columnCard/ColumnManageModal.tsx new file mode 100644 index 00000000..46af35b5 --- /dev/null +++ b/src/components/columnCard/ColumnManageModal.tsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import { Modal } from "../modal/Modal"; +import { CustomBtn } from "../button/CustomButton"; +import Input from "../input/Input"; +import Image from "next/image"; + +type ColumnManageModalProps = { + isOpen: boolean; + onClose: () => void; + onDeleteClick: () => void; + columnTitle: string; + onEditSubmit: (newTitle: string) => void; +}; + +export default function ColumnManageModal({ + isOpen, + onClose, + onDeleteClick, + columnTitle, + onEditSubmit, +}: ColumnManageModalProps) { + const [newTitle, setNewTile] = useState(columnTitle); + + return ( + +
+ close icon +

칼럼 관리

+ +
+ + 삭제 + + onEditSubmit(newTitle)}>변경 +
+
+
+ ); +} diff --git a/src/components/common/CustomToastContainer.tsx b/src/components/common/CustomToastContainer.tsx new file mode 100644 index 00000000..f6714dba --- /dev/null +++ b/src/components/common/CustomToastContainer.tsx @@ -0,0 +1,19 @@ +"use client"; +import "react-toastify/dist/ReactToastify.css"; +import { ToastContainer, Slide } from "react-toastify"; + +const CustomToastContainer = () => { + return ( + + ); +}; + +export default CustomToastContainer; diff --git a/src/components/common/LoadingSpinner.tsx b/src/components/common/LoadingSpinner.tsx new file mode 100644 index 00000000..2daca5bc --- /dev/null +++ b/src/components/common/LoadingSpinner.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +const LoadingSpinner = () => { + return ( +
+
+
+ ); +}; + +export default LoadingSpinner; diff --git a/src/components/gnb/HeaderDashboard.tsx b/src/components/gnb/HeaderDashboard.tsx new file mode 100644 index 00000000..9944a903 --- /dev/null +++ b/src/components/gnb/HeaderDashboard.tsx @@ -0,0 +1,293 @@ +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import clsx from "clsx"; +import Image from "next/image"; +import SkeletonUser from "@/shared/skeletonUser"; +import { MemberType, UserType } from "@/types/users"; +import { getMembers } from "@/api/members"; +import { getUserInfo } from "@/api/users"; +import { getDashboardById } from "@/api/dashboards"; +import { UserProfileIcon } from "@/components/gnb/ProfileIcon"; +import MembersProfileIconList from "@/components/gnb/MembersProfileIconList"; +import UserMenu from "@/components/gnb/UserMenu"; +import MemberListMenu from "@/components/gnb/MemberListMenu"; +import InviteDashboard from "@/components/modal/InviteDashboard"; + +interface HeaderDashboardProps { + variant?: "mydashboard" | "dashboard" | "edit" | "mypage"; + dashboardTitle?: string; + dashboardId?: string | string[]; + isEditMode?: boolean; + onToggleEditMode?: () => void; +} + +const HeaderDashboard: React.FC = ({ + variant, + dashboardId, + isEditMode, + onToggleEditMode, +}) => { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [user, setUser] = useState(null); + const [members, setMembers] = useState([]); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isListOpen, setIsListOpen] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [dashboard, setDashboard] = useState<{ + title: string; + createdByMe: boolean; + } | null>(null); + + /*초대하기 모달 상태 관리*/ + const [isModalOpen, setIsModalOpen] = useState(false); + const openInviteModal = () => { + setIsModalOpen(true); + }; + const closeInviteModal = () => { + setIsModalOpen(false); + }; + + /*멤버 목록 api 호출*/ + useEffect(() => { + const fetchMembers = async () => { + try { + const members = await getMembers({ + dashboardId: Number(dashboardId), + }); + setMembers(members); + } catch (error) { + console.error("멤버 불러오기 실패:", error); + setErrorMessage("멤버 정보를 불러오지 못했습니다."); + } finally { + setIsLoading(false); + } + }; + if ( + (variant === "dashboard" || variant === "mypage" || variant === "edit") && + dashboardId + ) { + fetchMembers(); + } + }, [dashboardId, variant]); + + /*유저 정보 api 호출*/ + useEffect(() => { + const fetchUser = async () => { + try { + const user = await getUserInfo(); + setUser(user); + } catch (error) { + console.error("유저 정보 불러오기 실패", error); + setErrorMessage("유저 정보를 불러오지 못했습니다."); + } finally { + setIsLoading(false); + } + }; + const token = localStorage.getItem("accessToken"); + if (token) { + fetchUser(); + } + }, []); + + /*대시보드 api 호출*/ + useEffect(() => { + const fetchDashboard = async () => { + if (variant === "dashboard" && dashboardId) { + try { + const dashboardData = await getDashboardById({ + dashboardId: Number(dashboardId), + }); + setDashboard(dashboardData); + } catch (error) { + console.error("대시보드 정보 불러오기 실패", error); + setErrorMessage("대시보드를 불러오지 못했습니다."); + } finally { + setIsLoading(false); + } + } + }; + fetchDashboard(); + }, [variant, dashboardId]); + + /*헤더 종류에 따라 다른 제목 표시*/ + const title = (() => { + if (variant === "mydashboard") return "내 대시보드"; + if (variant === "edit") return "대시보드 수정"; + if (variant === "mypage") return "계정 관리"; + if (variant === "dashboard" && dashboard?.title) return dashboard.title; + })(); + + return ( +
+
+ {errorMessage && ( +

+ {errorMessage} +

+ )} + + {/*헤더 제목*/} +
+

+ {title} +

+ {dashboard?.createdByMe && ( + 왕관 아이콘 + )} +
+ +
+
+ {/*관리 버튼*/} + {(variant === "mydashboard" || dashboard?.createdByMe) && ( + + )} + + {/*초대하기 버튼*/} + {variant !== "mydashboard" && + variant !== "edit" && + dashboard?.createdByMe && ( + + )} + {isModalOpen && } +
+ + {/*멤버 목록*/} + {variant !== "mydashboard" && ( +
+ {isLoading ? ( + + ) : ( + members && ( +
setIsListOpen((prev) => !prev)} + className="flex items-center pl-[15px] md:pl-[25px] lg:pl-[30px] pr-[15px] md:pr-[25px] lg:pr-[30px] cursor-pointer" + > + +
+ ) + )} + +
+ )} + + {/*드롭다운 메뉴 너비 지정 목적의 유저 섹션 div*/} +
+ {/*구분선*/} +
+ {/*유저 드롭다운 메뉴*/} +
setIsMenuOpen((prev) => !prev)} + className="flex items-center gap-[12px] pl-[20px] md:pl-[30px] lg:pl-[35px] cursor-pointer overflow-hidden" + > + + {/*유저 프로필*/} + {isLoading ? ( + + ) : ( + user && ( + <> + + + {user.nickname} + + + ) + )} +
+
+
+
+
+ ); +}; + +export default HeaderDashboard; diff --git a/src/components/gnb/HeaderDefault.tsx b/src/components/gnb/HeaderDefault.tsx new file mode 100644 index 00000000..bf75252a --- /dev/null +++ b/src/components/gnb/HeaderDefault.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { useRouter } from "next/router"; +import Image from "next/image"; +import useUserStore from "@/store/useUserStore"; + +interface HeaderDefaultProps { + variant?: "white" | "black"; +} + +const HeaderDefault: React.FC = ({ variant = "white" }) => { + const router = useRouter(); + const user = useUserStore((state) => state.user); + const { clearUser } = useUserStore(); + + const isLoggedIn = !!user; + const isWhite = variant === "white"; + + const handleAuthClick = () => { + if (isLoggedIn) { + clearUser(); + localStorage.removeItem("accessToken"); + router.push("/"); + } else { + router.push("/login"); + } + }; + + return ( +
+
+
+ Taskify Logo router.push(`/`)} + width={121} + height={39} + className="hidden md:block" + /> + Taskify Mobile Logo router.push(`/`)} + width={24} + height={27} + className="block md:hidden" + /> +
+
+ + {!isLoggedIn && ( + + )} +
+
+
+ ); +}; + +export default HeaderDefault; diff --git a/src/components/gnb/MemberListMenu.tsx b/src/components/gnb/MemberListMenu.tsx new file mode 100644 index 00000000..a396424a --- /dev/null +++ b/src/components/gnb/MemberListMenu.tsx @@ -0,0 +1,51 @@ +import React, { useRef } from "react"; +import clsx from "clsx"; +import { useClosePopup } from "@/hooks/useClosePopup"; +import { MemberProfileIcon } from "@/components/gnb/ProfileIcon"; +import { MemberType } from "@/types/users"; + +interface MemberListMenuProps { + isListOpen: boolean; + setIsListOpen: React.Dispatch>; + members: MemberType[]; +} + +const MemberListMenu: React.FC = ({ + isListOpen, + setIsListOpen, + members, +}) => { + const ref = useRef(null); + + useClosePopup(ref, () => setIsListOpen(false)); + + return ( +
+
    + {members.map((member) => ( +
  • + + + {member.nickname} + +
  • + ))} +
+
+ ); +}; + +export default MemberListMenu; diff --git a/src/components/gnb/MembersProfileIconList.tsx b/src/components/gnb/MembersProfileIconList.tsx new file mode 100644 index 00000000..6b24e613 --- /dev/null +++ b/src/components/gnb/MembersProfileIconList.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from "react"; +import SkeletonUser from "@/shared/skeletonUser"; +import RandomProfile from "../table/member/RandomProfile"; +import Image from "next/image"; +import { MemberType } from "@/types/users"; + +/*멤버 프로필 아이콘*/ +interface MemberIconProps { + members: MemberType[]; + isLoading: boolean; +} + +export const MembersProfileIconList: React.FC = ({ + members, + isLoading, +}) => { + // 출력할 프로필 아이콘 최대 개수 + const [maxVisibleMembers, setMaxVisibleMembers] = useState(4); + + useEffect(() => { + const handleResize = () => { + // Tailwind 기준 sm 이하 (모바일) + if (window.innerWidth < 640) { + setMaxVisibleMembers(2); + } else { + setMaxVisibleMembers(4); + } + }; + + handleResize(); + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return ( +
+ {isLoading ? ( + + ) : ( + <> + {members.slice(0, maxVisibleMembers).map((member) => ( +
+ {member.profileImageUrl ? ( +
+ {member.nickname} +
+ ) : ( +
+ +
+ )} +
+ ))} + + {/* 출력되지 않은 나머지 멤버 수 */} + {members.length > maxVisibleMembers && ( +
+ +{members.length - maxVisibleMembers} +
+ )} + + )} +
+ ); +}; + +export default MembersProfileIconList; diff --git a/src/components/gnb/ProfileIcon.tsx b/src/components/gnb/ProfileIcon.tsx new file mode 100644 index 00000000..e1d08b82 --- /dev/null +++ b/src/components/gnb/ProfileIcon.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import RandomProfile from "../table/member/RandomProfile"; +import Image from "next/image"; +import { MemberType, UserType } from "@/types/users"; + +/*멤버 프로필 아이콘*/ +interface MemberIconProps { + members: MemberType; + variant?: "mydashboard" | "dashboard" | "edit" | "mypage"; +} + +export const MemberProfileIcon: React.FC = ({ members }) => ( +
+ {members.profileImageUrl ? ( + 멤버 프로필 아이콘 + ) : ( + + )} +
+); + +/*유저 프로필 아이콘*/ +interface UserIconProps { + user: UserType; +} + +export const UserProfileIcon: React.FC = ({ user }) => ( +
+ {user.profileImageUrl ? ( + 유저 프로필 아이콘 + ) : ( + + )} +
+); diff --git a/src/components/gnb/UserMenu.tsx b/src/components/gnb/UserMenu.tsx new file mode 100644 index 00000000..a87c58a6 --- /dev/null +++ b/src/components/gnb/UserMenu.tsx @@ -0,0 +1,70 @@ +import React, { useRef } from "react"; +import { useRouter } from "next/router"; +import clsx from "clsx"; +import { useClosePopup } from "@/hooks/useClosePopup"; +import { User, LogOut, FolderPen } from "lucide-react"; +import { UserType } from "@/types/users"; +import useUserStore from "@/store/useUserStore"; + +interface UserMenuProps { + user: UserType | null; + isMenuOpen: boolean; + setIsMenuOpen: React.Dispatch>; +} + +const dropdownButtonStyles = clsx( + "flex justify-center md:justify-start items-center", + "w-full px-3 py-3 gap-3", + "text-sm lg:text-base text-black3", + "hover:text-[var(--primary)] hover:bg-[#f9f9f9] cursor-pointer" +); + +const UserMenu: React.FC = ({ isMenuOpen, setIsMenuOpen }) => { + const { clearUser } = useUserStore(); + const router = useRouter(); + const ref = useRef(null); + + useClosePopup(ref, () => setIsMenuOpen(false)); + + const handleLogout = () => { + localStorage.setItem("isLoggingOut", "true"); + clearUser(); + localStorage.removeItem("accessToken"); + router.push("/"); + }; + + return ( +
+ + + +
+ ); +}; + +export default UserMenu; diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx new file mode 100644 index 00000000..9da618cf --- /dev/null +++ b/src/components/input/Input.tsx @@ -0,0 +1,156 @@ +import clsx from "clsx"; +import { HTMLInputTypeAttribute, useId, useRef, useState } from "react"; + +type GeneralInputType = "text" | "number" | "hidden" | "search" | "tel" | "url"; + +interface GeneralInputProps { + type: GeneralInputType; + label?: string; + placeholder?: string; + className?: string; + onChange?: (value: string) => void; + value?: string; + readOnly?: boolean; //입력방지 추가 + onKeyDown?: (e: React.KeyboardEvent) => void; + forceInvalid?: boolean; +} + +interface SignInputProps extends Omit { + type: Extract; + name: "email" | "nickname" | "password" | "passwordCheck"; + pattern?: string; + invalidMessage?: string; + labelClassName?: string; + wrapperClassName?: string; + forceInvalid?: boolean; +} + +type InputProps = GeneralInputProps | SignInputProps; + +export default function Input(props: InputProps) { + const { + type, + name, + label, + placeholder, + onChange, + pattern, + invalidMessage, + className, + labelClassName, + wrapperClassName, + forceInvalid, + ...rest + } = props as SignInputProps; + + const id = useId(); + const inputRef = useRef(null); + const [htmlType, setHtmlType] = useState(type); + const [isInvalid, setIsInvalid] = useState(false); + + const handleChange = (event: React.ChangeEvent) => { + const value = event.target.value; + + if (onChange) { + onChange(value); + } + + event.target.setCustomValidity(""); + + if (pattern) { + const regex = new RegExp(pattern); + setIsInvalid(!regex.test(value)); + } else { + setIsInvalid(false); + } + }; + + const handleBlur = (event: React.FocusEvent) => { + if (pattern) { + const input = event.target as HTMLInputElement; + if (!input.validity.valid) { + input.setCustomValidity(invalidMessage || "올바른 값을 입력하세요."); + setIsInvalid(true); + } else { + input.setCustomValidity(""); + setIsInvalid(false); + } + } + }; + + const togglePasswordTypeOnClick = () => { + setHtmlType((prev) => (prev === "password" ? "text" : "password")); + }; + + return ( +
+ {label && ( + + )} +
+ { + const input = e.target as HTMLInputElement; + input.setCustomValidity( + invalidMessage || "올바른 값을 입력하세요." + ); + setIsInvalid(true); + }} + onKeyDown={rest.onKeyDown} + className={clsx( + "peer flex h-[50px] w-full max-w-[520px] px-2 sm:px-4 py-2 rounded-lg transition-colors duration-200", + "border border-[var(--color-gray3)] focus:border-[var(--primary)] focus:ring-0 focus:outline-none", + isInvalid || forceInvalid + ? "border-[var(--color-red)] focus:border-[var(--color-red)]" + : "", + type === "password" + ? "text-[var(--color-black4)]" + : "text-[var(--color-black)]", + className + )} + {...rest} + /> + {type === "password" && ( + + )} +
+ + {isInvalid && invalidMessage && ( + + {invalidMessage} + + )} +
+ ); +} diff --git a/src/components/landing/Footer.tsx b/src/components/landing/Footer.tsx new file mode 100644 index 00000000..55220569 --- /dev/null +++ b/src/components/landing/Footer.tsx @@ -0,0 +1,42 @@ +import Image from "next/image"; +import Link from "next/link"; + +const Footer = () => { + return ( +
+ {/* 왼쪽 - 저작권 */} +
©codeit - 2025
+ + {/* 가운데 - 링크 */} +
+ Privacy Policy + FAQ +
+ + {/* 오른쪽 - SNS 아이콘 */} +
+ + 이메일 + + + 페이스북 + + + 인스타그램 + +
+
+ ); +}; + +export default Footer; diff --git a/src/components/landing/LandingMain.tsx b/src/components/landing/LandingMain.tsx new file mode 100644 index 00000000..d4b076ec --- /dev/null +++ b/src/components/landing/LandingMain.tsx @@ -0,0 +1,54 @@ +import Section1 from "./Section1"; +import Section2 from "./Section2"; +import Section3 from "./Section3"; + +export default function LandingMain() { + return ( +
+ {/* 히어로 섹션 */} + + + {/* Section2와 Section3 영역 */} +
+ {/* Section2: 우선순위 & 해야 할 일 등록 */} + + + {/* Section3 영역 시작 */} +
+ {/* 타이틀: 왼쪽 정렬 + 28px */} +

+ 생산성을 높이는 다양한 설정 ⚡ +

+ + {/* Section3 카드들 */} +
+ + + +
+
+
+
+ ); +} diff --git a/src/components/landing/Section1.tsx b/src/components/landing/Section1.tsx new file mode 100644 index 00000000..848f8d16 --- /dev/null +++ b/src/components/landing/Section1.tsx @@ -0,0 +1,58 @@ +import { useRouter } from "next/router"; +import useUserStore from "@/store/useUserStore"; +import Image from "next/image"; + +export default function Section1() { + const user = useUserStore((state) => state.user); + const isLoggedIn = !!user; + const router = useRouter(); + + const handleMainClick = () => { + if (isLoggedIn) { + router.push("/mydashboard"); + } else { + router.push("/login"); + } + }; + + return ( +
+ {/* 히어로 이미지 */} +
+
+ Taskify 히어로 이미지 +
+
+ + {/* 메인 타이틀 */} +
+ + 새로운 일정 관리{" "} + + + Taskify + +
+ + {/* 설명 문구 (비어있으면 지워도 됨) */} + + {/* 설명 문구 필요시 여기에 추가 */} + + + {/* CTA 버튼 */} + +
+ ); +} diff --git a/src/components/landing/Section2.tsx b/src/components/landing/Section2.tsx new file mode 100644 index 00000000..3d8e84f0 --- /dev/null +++ b/src/components/landing/Section2.tsx @@ -0,0 +1,57 @@ +import Image from "next/image"; + +export default function Section2() { + return ( +
+ {/* 카드 1 */} +
+ {/* 텍스트 왼쪽 */} +
+

+ Point 1 +

+
+

일의 우선순위를

+

관리하세요

+
+
+ + {/* 이미지 오른쪽 */} +
+ 우선순위 관리 예시 +
+
+ + {/* 카드 2 */} +
+ {/* 텍스트 */} +
+

+ Point 2 +

+
+

해야 할 일을

+

등록하세요

+
+
+ + {/* 이미지 */} +
+ 할 일 등록 예시 +
+
+
+ ); +} diff --git a/src/components/landing/Section3.tsx b/src/components/landing/Section3.tsx new file mode 100644 index 00000000..a787762b --- /dev/null +++ b/src/components/landing/Section3.tsx @@ -0,0 +1,46 @@ +import clsx from "clsx"; +import Image from "next/image"; + +interface Section3Props { + src: string; + alt: string; + padding: "sm" | "md" | "lg"; + height: number; + title: string; + description: string; +} + +export default function Section3({ + src, + alt, + padding, + height, + title, + description, +}: Section3Props) { + const paddingClasses = clsx({ + "py-[68px]": padding === "lg", + "py-[32px]": padding === "md", + "pb-[18px] pt-[11px]": padding === "sm", + }); + + return ( +
+ {/* 상단 이미지 영역 */} +
+ {alt} +
+ + {/* 하단 텍스트 영역 */} +
+

{title}

+

{description}

+
+
+ ); +} diff --git a/src/components/modal/ChangeBebridge.tsx b/src/components/modal/ChangeBebridge.tsx new file mode 100644 index 00000000..67f73dd8 --- /dev/null +++ b/src/components/modal/ChangeBebridge.tsx @@ -0,0 +1,120 @@ +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import Input from "../input/Input"; +import Image from "next/image"; +import axiosInstance from "@/api/axiosInstance"; +import { apiRoutes } from "@/api/apiRoutes"; +import { toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +const ChangeBebridge = () => { + const router = useRouter(); + const { dashboardId } = router.query; + const [dashboardDetail, setdashboardDetail] = useState<{ title?: string }>( + {} + ); + const [title, setTitle] = useState(""); + const [selected, setSelected] = useState(null); + const colors = ["#7ac555", "#760DDE", "#FF9800", "#76A5EA", "#E876EA"]; + + /* 대시보드 이름 데이터 */ + useEffect(() => { + const fetchDashboardTitle = async () => { + try { + const dashboardIdNumber = Number(dashboardId); + const res = await axiosInstance.get( + apiRoutes.dashboardDetail(dashboardIdNumber), + { + params: { + dashboardId, + }, + } + ); + if (res.data) { + const dashboardData = res.data; + setdashboardDetail(dashboardData); + } + } catch (error) { + console.error("대시보드 상세내용 불러오는데 오류 발생:", error); + } + }; + if (dashboardId) { + fetchDashboardTitle(); + } + }, [dashboardId]); + + /* 대시보드 이름 변경 버튼 */ + const handleUpdate = async () => { + const dashboardIdNumber = Number(dashboardId); + if (!dashboardId || selected === null) return; + + const payload = { + title, + color: colors[selected], + }; + + try { + await axiosInstance.put( + apiRoutes.dashboardDetail(dashboardIdNumber), + payload + ); + + toast.success("대시보드가 변경되었습니다!"); + setTimeout(() => { + router.reload(); + }, 1500); + } catch (error) { + console.error("대시보드 변경 실패:", error); + toast.error("대시보드 변경에 실패했습니다."); + } + }; + + return ( +
+

+ {dashboardDetail.title} +

+ + +
+ {colors.map((color, index) => ( +
+
+ ))} +
+
+ +
+
+ ); +}; + +export default ChangeBebridge; diff --git a/src/components/modal/DeleteDashboardModal.tsx b/src/components/modal/DeleteDashboardModal.tsx new file mode 100644 index 00000000..521d85d4 --- /dev/null +++ b/src/components/modal/DeleteDashboardModal.tsx @@ -0,0 +1,55 @@ +import { Modal } from "./Modal"; +import { CustomBtn } from "../button/CustomButton"; +import { useRouter } from "next/router"; +import axiosInstance from "@/api/axiosInstance"; +import { apiRoutes } from "@/api/apiRoutes"; +import { toast } from "react-toastify"; + +type DeleteDashboardProps = { + isOpen: boolean; + onClose: () => void; + dashboardid: string; +}; + +export default function DeleteDashboardModal({ + isOpen, + onClose, + dashboardid, +}: DeleteDashboardProps) { + const router = useRouter(); + + /* 대시보드 삭제 */ + const handleDelete = async () => { + const dashboardIdNumber = Number(dashboardid); + if (!dashboardid) return; + try { + await axiosInstance.delete(apiRoutes.dashboardDetail(dashboardIdNumber)); + router.push(`/mydashboard`); + } catch (error) { + console.error("대시보드 삭제 실패:", error); + toast.error("대시보드 삭제에 실패하였습니다 ."); + + window.location.reload(); + } + }; + return ( + +
+

대시보드를 삭제하시겠습니까?

+
+ + 취소 + + + 삭제 + +
+
+
+ ); +} diff --git a/src/components/modal/DeleteMemberModal.tsx b/src/components/modal/DeleteMemberModal.tsx new file mode 100644 index 00000000..29cad060 --- /dev/null +++ b/src/components/modal/DeleteMemberModal.tsx @@ -0,0 +1,51 @@ +import { Modal } from "./Modal"; +import { CustomBtn } from "../button/CustomButton"; +import axiosInstance from "@/api/axiosInstance"; +import { apiRoutes } from "@/api/apiRoutes"; +import { toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +type DeleteMemberdProps = { + isOpen: boolean; + onClose: () => void; + id: number; +}; + +export default function DeleteDashboardModal({ + isOpen, + onClose, + id, +}: DeleteMemberdProps) { + /* 멤버삭제 */ + const handleDelete = async (id: number) => { + try { + await axiosInstance.delete(apiRoutes.memberDetail(id)); + window.location.reload(); + } catch (error) { + toast.error("구성원 삭제에 실패했습니다."); + console.error("구성원 삭제 실패:", error); + } + }; + + return ( + +
+

멤버를 삭제하시겠습니까?

+
+ + 취소 + + handleDelete(id)}> + 삭제 + +
+
+
+ ); +} diff --git a/src/components/modal/DeleteModal.tsx b/src/components/modal/DeleteModal.tsx new file mode 100644 index 00000000..c91bf5bf --- /dev/null +++ b/src/components/modal/DeleteModal.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { Modal } from "@/components/modal/Modal"; +import { CustomBtn } from "../button/CustomButton"; +import { toast } from "react-toastify"; + +interface DeleteModalProps { + isDeleteModalOpen: boolean; + setIsDeleteModalOpen: (open: boolean) => void; + isConfirmDeleteModalOpen: boolean; + setIsConfirmDeleteModalOpen: (open: boolean) => void; + selectedTitle: string | null; + selectedCreatedByMe: boolean | null; + handleDelete: () => void; + handleLeave: () => void; +} + +export const DeleteModal: React.FC = ({ + isDeleteModalOpen, + setIsDeleteModalOpen, + isConfirmDeleteModalOpen, + setIsConfirmDeleteModalOpen, + selectedTitle, + selectedCreatedByMe, + handleDelete, + handleLeave, +}) => { + return ( + <> + setIsDeleteModalOpen(false)} + className="flex items-center justify-center text-center" + > +
+
{selectedTitle}
+
+ {selectedCreatedByMe + ? "대시보드를 삭제하시겠습니까?" + : "대시보드에서 나가시겠습니까?"} +
+
+ +
+ setIsDeleteModalOpen(false)} + className="cursor-pointer border px-3 py-1 rounded-md w-[84px] h-[32px] text-[var(--primary)] border-[var(--color-gray3)]" + > + 취소 + + { + if (selectedCreatedByMe) { + setIsDeleteModalOpen(false); + setIsConfirmDeleteModalOpen(true); // 재확인 모달 오픈 + } else { + handleLeave(); // 탈퇴일 때는 바로 닫힘 + toast.error("현재 탈퇴 기능이 준비 중입니다."); + } + }} + className="cursor-pointer bg-[var(--primary)] text-white px-3 py-1 rounded-md w-[84px] h-[32px]" + > + 확인 + +
+
+ + setIsConfirmDeleteModalOpen(false)} + className="flex items-center justify-center text-center" + > +
+
+ 삭제 시 복구할 수 없습니다. +
+
정말 삭제하시겠습니까?
+
+ +
+ setIsConfirmDeleteModalOpen(false)} + className="cursor-pointer border px-3 py-1 rounded-md w-[84px] h-[32px] text-[var(--primary)] border-[var(--color-gray3)]" + > + 취소 + + { + setIsConfirmDeleteModalOpen(false); + handleDelete(); // 진짜 삭제 실행 + toast.success("대시보드가 삭제되었습니다."); + }} + className="cursor-pointer bg-[var(--primary)] text-white px-3 py-1 rounded-md w-[84px] h-[32px]" + > + 확인 + +
+
+ + ); +}; diff --git a/src/components/modal/InviteDashboard.tsx b/src/components/modal/InviteDashboard.tsx new file mode 100644 index 00000000..f5ac634d --- /dev/null +++ b/src/components/modal/InviteDashboard.tsx @@ -0,0 +1,136 @@ +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import Input from "../input/Input"; +import Image from "next/image"; +import axiosInstance from "@/api/axiosInstance"; +import { apiRoutes } from "@/api/apiRoutes"; +import { AxiosError } from "axios"; +import { toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +export default function InviteDashboard({ onClose }: { onClose?: () => void }) { + const [email, setEmail] = useState(""); + const router = useRouter(); + const { dashboardId } = router.query; + + const [invitelist, setInviteList] = useState<{ email: string }[]>([]); + + const isValidEmail = (email: string) => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }; + + /* 초대내역 목록 api 호출*/ + useEffect(() => { + const fetchMembers = async () => { + try { + const dashboardIdNumber = Number(dashboardId); + const res = await axiosInstance.get( + apiRoutes.dashboardInvite(dashboardIdNumber), + { + params: { + dashboardId, + }, + } + ); + if (res.data && Array.isArray(res.data.invitations)) { + // 초대내역 리스트 + const inviteData = res.data.invitations.map( + (item: { invitee: { email: string } }) => ({ + email: item.invitee.email, + }) + ); + setInviteList(inviteData); + } + } catch (error) { + console.error("초대내역 불러오는데 오류 발생:", error); + } + }; + + if (dashboardId) { + fetchMembers(); + } + }, [dashboardId]); + + /* 초대하기 버튼 */ + const handleSubmit = async () => { + const dashboardIdNumber = Number(dashboardId); + if (!dashboardId || !email) return; + + if (invitelist?.some((invite) => invite.email === email)) { + toast.error("이미 초대한 멤버입니다."); + return; + } + + try { + await axiosInstance.post(apiRoutes.dashboardInvite(dashboardIdNumber), { + email, + }); + + toast.success("멤버 초대에 성공했습니다."); + onClose?.(); + } catch (error) { + if (error instanceof AxiosError) { + if (error.response?.status === 403) { + toast.error("초대 권한이 없습니다."); + return; + } else if (error.response?.status === 404) { + toast.error("대시보드 또는 유저가 존재하지 않습니다."); + return; + } else if (error.response?.status === 409) { + toast.error("이미 대시보드에 초대된 멤버입니다."); + return; + } else { + toast.error("오류가 발생했습니다."); + return; + } + } else { + toast.error("네트워크 오류가 발생했습니다."); + return; + } + } + }; + + return ( +
+
+
+

초대하기

+ 닫기 +
+ + +
+ + +
+
+
+ ); +} diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx new file mode 100644 index 00000000..d93c1d53 --- /dev/null +++ b/src/components/modal/Modal.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { createPortal } from "react-dom"; +import { CustomBtn } from "../button/CustomButton"; + +interface ButtonProps { + label: string; + onClick: () => void; + variant?: "primary" | "outline"; // 버튼 스타일 지정 가능 (기본값: primary) + className?: string; // 추가적인 버튼 스타일 적용 가능 +} + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; // 모달 상단 제목 (선택 사항) + children: React.ReactNode; + buttons?: ButtonProps[]; + className?: string; + contentClassName?: string; + buttonContainerClassName?: string; + width?: string; + height?: string; + backgroundClassName?: string; // 배경색 커스터마이징을 위한 클래스 추가 + backgroundStyle?: React.CSSProperties; // 배경 스타일을 직접 지정할 수 있는 스타일 추가 +} + +export function Modal({ + isOpen, + onClose, + title, + children, + buttons, + className = "", + contentClassName = "", + buttonContainerClassName = "", + width = "w-[568px]", // 기본 너비 설정, 필요 시 변경 가능 + height = "h-[266px]", // 기본 높이 설정, 필요 시 변경 가능 + backgroundClassName = "bg-black/35", + backgroundStyle = {}, +}: ModalProps) { + if (!isOpen) return null; + + return createPortal( +
+
e.stopPropagation()} + > + {/* 모달 제목 (선택 사항) */} + {title &&

{title}

} + + {/* 모달 컨텐츠 영역 */} +
+ {children} +
+ + {/* 버튼 영역 - 필요하면 원하는 개수만큼 버튼 추가 가능 */} + {buttons && buttons.length > 0 && ( +
+
+ {buttons.map((button, index) => ( + + {button.label} + + ))} +
+
+ )} +
+
, + document.body + ); +} diff --git a/src/components/modal/MypageModal.tsx b/src/components/modal/MypageModal.tsx new file mode 100644 index 00000000..ea8d21db --- /dev/null +++ b/src/components/modal/MypageModal.tsx @@ -0,0 +1,43 @@ +import { Modal } from "./Modal"; +import { CustomBtn } from "../button/CustomButton"; +import { ReactNode } from "react"; + +interface MypageModalProps { + isOpen: boolean; + onClose: () => void; + message: string; + confirmLabel?: string; + onConfirm?: () => void; + width?: string; + height?: string; + children?: ReactNode; +} + +export default function MypageModal({ + isOpen, + onClose, + message, + confirmLabel = "확인", + onConfirm, + width = "w-[357px]", + height = "h-[192px]", + children, +}: MypageModalProps) { + return ( + +
+

{message}

+ {children} +
+ + {confirmLabel} + +
+
+
+ ); +} diff --git a/src/components/modal/NewDashboard.tsx b/src/components/modal/NewDashboard.tsx new file mode 100644 index 00000000..3d940bd6 --- /dev/null +++ b/src/components/modal/NewDashboard.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import Input from "../input/Input"; +import Image from "next/image"; +import { createDashboard } from "@/api/dashboards"; +import { toast } from "react-toastify"; + +interface Dashboard { + id: number; + title: string; + color: string; + userId: number; + createdAt: string; + updatedAt: string; + createdByMe: boolean; +} + +interface NewDashboardProps { + teamId: string; + onClose?: () => void; + onCreate?: (newDashboard: Dashboard) => void; +} + +export default function NewDashboard({ onClose, onCreate }: NewDashboardProps) { + const [title, setTitle] = useState(""); + const [selected, setSelected] = useState(null); + const [loading, setLoading] = useState(false); + + const colors = ["#7ac555", "#760DDE", "#FF9800", "#76A5EA", "#E876EA"]; + + const handleSubmit = async () => { + const payload = { + title, + color: selected !== null ? colors[selected] : "", + }; + + try { + setLoading(true); + const response = await createDashboard(payload); + onCreate?.(response.data); + onClose?.(); + } catch (error) { + console.error("대시보드 생성 실패:", error); + toast.error("대시보드 생성에 실패했습니다."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

새로운 대시보드

+ + +
+ {colors.map((color, index) => ( +
+
+ ))} +
+ +
+ + +
+
+
+ ); +} diff --git a/src/components/modalDashboard/CardDetail.tsx b/src/components/modalDashboard/CardDetail.tsx new file mode 100644 index 00000000..a5b2685e --- /dev/null +++ b/src/components/modalDashboard/CardDetail.tsx @@ -0,0 +1,94 @@ +// CardDetail.tsx +import Image from "next/image"; +import { CardDetailType } from "@/types/cards"; +import { ProfileIcon } from "./profelicon"; +import ColorTagChip, { + getTagColor, +} from "@/components/modalInput/chips/ColorTagChip"; + +interface CardDetailProps { + card: CardDetailType; + columnName: string; +} + +export default function CardDetail({ card, columnName }: CardDetailProps) { + return ( +
+

{card.title}

+ {/* 작성자 정보 추가 */} +
+
+

담당자

+
+ + + + {card.assignee.nickname} + +
+ +
+

+ 마감일 +

+

+ {new Date(card.dueDate).toLocaleString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + })} +

+
+
+
+
+ + {columnName} + + | + {card.tags.map((tag, idx) => { + const { textColor, bgColor } = getTagColor(idx); + return ( + + {tag} + + ); + })} +
+

+ {card.description} +

+ {card.imageUrl && ( +
+ 카드 이미지 +
+ )} +
+ ); +} diff --git a/src/components/modalDashboard/CardDetailModal.tsx b/src/components/modalDashboard/CardDetailModal.tsx new file mode 100644 index 00000000..e1044aef --- /dev/null +++ b/src/components/modalDashboard/CardDetailModal.tsx @@ -0,0 +1,215 @@ +import { useMemo, useRef, useState } from "react"; +import { MoreVertical, X } from "lucide-react"; +import CardDetail from "./CardDetail"; +import CommentList from "./CommentList"; +import CardInput from "@/components/modalInput/CardInput"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createComment } from "@/api/comment"; +import { deleteCard, EditCard } from "@/api/card"; // EditCard API 추가 +import type { CardDetailType } from "@/types/cards"; +import TaskModal from "@/components/modalInput/TaskModal"; +import { useClosePopup } from "@/hooks/useClosePopup"; +import { getColumn } from "@/api/columns"; +import { useRouter } from "next/router"; +import { toast } from "react-toastify"; +interface CardDetailModalProps { + card: CardDetailType; + currentUserId: number; + dashboardId: number; + onClose: () => void; +} + +interface ColumnType { + id: number; + title: string; + status: string; +} + +export default function CardDetailPage({ + card, + currentUserId, + dashboardId, + onClose, +}: CardDetailModalProps) { + const [cardData, setCardData] = useState(card); + const [commentText, setCommentText] = useState(""); + const [showMenu, setShowMenu] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const queryClient = useQueryClient(); + const popupRef = useRef(null); + const router = useRouter(); + useClosePopup(popupRef, () => setShowMenu(false)); + + const { data: columns = [] } = useQuery({ + queryKey: ["columns", dashboardId], + queryFn: () => getColumn({ dashboardId, columnId: card.columnId }), + }); + + const columnName = useMemo(() => { + return ( + columns.find((col) => String(col.id) === String(cardData.columnId)) + ?.title || "알 수 없음" + ); + }, [columns, cardData.columnId]); + + const { mutate: createCommentMutate } = useMutation({ + mutationFn: createComment, + onSuccess: () => { + setCommentText(""); + queryClient.invalidateQueries({ queryKey: ["comments", card.id] }); + }, + }); + + const { mutate: deleteCardMutate } = useMutation({ + mutationFn: () => deleteCard(card.id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["cards"] }); + toast.success("카드가 삭제되었습니다."); + onClose(); + + setTimeout(() => { + router.reload(); + }, 1500); + }, + }); + + const handleClose = () => { + onClose(); + }; + + const handleCommentSubmit = () => { + if (!commentText.trim()) return; + createCommentMutate({ + content: commentText, + cardId: card.id, + columnId: card.columnId, + dashboardId, + }); + }; + + const { mutateAsync: updateCardMutate } = useMutation({ + mutationFn: (data: Partial) => EditCard(cardData.id, data), + onSuccess: (updatedCard) => { + setCardData(updatedCard); // ✅ 서버 응답을 최신 cardData로 설정 + queryClient.invalidateQueries({ queryKey: ["cards"] }); // 필요시 + }, + }); + + return ( + <> +
+
+
+ {/* 오른쪽 상단 메뉴 */} +
+ + {showMenu && ( +
+ + +
+ )} +
+ + +
+ + {/* 모달 내부 콘텐츠 */} +
+ +
+ + {/* 댓글 입력창 */} +
+

댓글

+
+ +
+
+ + {/* 댓글 목록 */} +
+
+ +
+
+
+
+ + {/* TaskModal 수정 모드 */} + {isEditModalOpen && ( + setIsEditModalOpen(false)} + onSubmit={async (data) => { + const matchedColumn = columns.find( + (col) => col.title === data.status + ); // title → id + + await updateCardMutate({ + columnId: matchedColumn?.id, // ✅ columnId로 넘기기! + assignee: { ...cardData.assignee, nickname: data.assignee }, + title: data.title, + description: data.description, + dueDate: data.deadline, + tags: data.tags, + imageUrl: data.image || undefined, + }); + + setIsEditModalOpen(false); + router.reload(); + }} + initialData={{ + status: columnName, + assignee: cardData.assignee.nickname, + title: cardData.title, + description: cardData.description, + deadline: cardData.dueDate, + tags: cardData.tags, + image: cardData.imageUrl ?? "", + }} + members={[{ nickname: cardData.assignee.nickname }]} + /> + )} + + ); +} diff --git a/src/components/modalDashboard/CommentList.tsx b/src/components/modalDashboard/CommentList.tsx new file mode 100644 index 00000000..87f84993 --- /dev/null +++ b/src/components/modalDashboard/CommentList.tsx @@ -0,0 +1,53 @@ +import { useEffect } from "react"; +import { useInView } from "react-intersection-observer"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { getComments } from "@/api/comment"; +import type { Comment as CommentType } from "@/types/comments"; +import UpdateComment from "./UpdateComment"; + +interface CommentListProps { + cardId: number; + currentUserId: number; + teamId: string; +} + +export default function CommentList({ + cardId, + currentUserId, +}: CommentListProps) { + const { ref, inView } = useInView(); + + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery({ + queryKey: ["comments", cardId], + queryFn: ({ pageParam = 1 }) => getComments({ cardId, pageParam }), + getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, + initialPageParam: 1, + enabled: !!cardId, + retry: false, + }); + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + const allComments: CommentType[] = + data?.pages.flatMap((page) => page.comments) ?? []; + + return ( +
+ {allComments.map((comment) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/src/components/modalDashboard/UpdateComment.tsx b/src/components/modalDashboard/UpdateComment.tsx new file mode 100644 index 00000000..09c88249 --- /dev/null +++ b/src/components/modalDashboard/UpdateComment.tsx @@ -0,0 +1,99 @@ +// ✅ UpdateComment.tsx +import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { deleteComment, updateComment } from "@/api/comment"; +import { Comment } from "@/types/comments"; +import { ProfileIcon } from "./profelicon"; +import formatDate from "./formatDate"; + +interface UpdateCommentProps { + comment: Comment; + currentUserId: number; + teamId: string; +} + +export default function UpdateComment({ + comment, + currentUserId, +}: UpdateCommentProps) { + const [isEditing, setIsEditing] = useState(false); + const [editedContent, setEditedContent] = useState(comment.content); + const queryClient = useQueryClient(); + + const handleEditToggle = () => { + setIsEditing(!isEditing); + setEditedContent(comment.content); + }; + + const handleDelete = async () => { + await deleteComment({ commentId: comment.id }); + queryClient.invalidateQueries({ queryKey: ["comments", comment.cardId] }); + }; + + const handleSave = async () => { + await updateComment(comment.id, { + ...comment, + content: editedContent, + }); + queryClient.invalidateQueries({ queryKey: ["comments", comment.cardId] }); + setIsEditing(false); + }; + + return ( +
+ {/* 프로필 */} + + + {/* 댓글 내용 */} +
+ {/* 작성자 + 시간 */} +
+ + {comment.author.nickname} + + {formatDate(comment.createdAt)} +
+ + {/* 본문 */} + {isEditing ? ( + <> +