diff --git a/README.md b/README.md index e69de29..0ee320f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,48 @@ +
+ +
+ +## ๐Ÿ–๏ธ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ + +> ํ˜ผ์ž ํ•ด๋ณด๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ์›น์„ ๋งŒ๋“ค๊ณ  ์‹ถ์—ˆ๊ณ , ๊ทธ๋ ‡๊ฒŒ ํƒ„์ƒํ•œ ๊ฒƒ์ด LocalCommu์ž…๋‹ˆ๋‹ค! + +### ๐Ÿ”’ ๋กœ๊ทธ์ธ +- ๊ธฐ๋ณธ์ ์ธ ๋กœ๊ทธ์ธ ๋ฐ ํšŒ์›๊ฐ€์ž…๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. +- ์†Œ์…œ ๋กœ๊ทธ์ธ(๊ตฌ๊ธ€, ๊นƒํ—ˆ๋ธŒ, ์นด์นด์˜ค)๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. +- ๋กœ๊ทธ์ธ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋ฆฌ๋””๋ ‰์…˜๋„ middleware๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + + +## ๐Ÿ”ง ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ + +### ๐Ÿ“ ํŽ˜์ด์ง€ +- / : ๋žœ๋”ฉ ํŽ˜์ด์ง€ => ์•„์ง ๋ฏธ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค. +- /map : ์ง€๋„ API ํ•™์Šต ๋ชฉ์ ์œผ๋กœ ๋งŒ๋“  ํ‚ค์›Œ๋“œ์— ๋”ฐ๋ฅธ ๋งˆ์ปค ์ƒ์„ฑ, ์ง€๋„ ์ด๋™ +- /post : ๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง€ => ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ๋ฐ ์กฐํšŒ ๊ธฐ๋Šฅ๊นŒ์ง€ ์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค. +- /login : ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ +- /signup : ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ +- /auth/error : ์ธ์ฆ ์˜ค๋ฅ˜ ๊ด€๋ จ ํŽ˜์ด์ง€ +- ๊ทธ ์™ธ์— ํŽ˜์ด์ง€๋Š” ํ˜„์žฌ ๊ฐœ๋ฐœ ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค. + +### ๐Ÿ–ฅ๏ธ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฐ์ฒด(DAO) +- ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ํ•จ์ˆ˜๋ฅผ ๋ถ„๋ฆฌ์‹œํ‚ค๊ธฐ ์œ„ํ•ด Dao ๊ฐ์ฒด๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- Dao ๊ฐ์ฒด์˜ ๋„ค์ด๋ฐ์€ ๋„๋ฉ”์ธ๋ณ„๋กœ ์ง€์—ˆ์Šต๋‹ˆ๋‹ค. +- ์ •์  ๋ฉ”์†Œ๋“œ๋กœ ๊ตฌํ˜„ํ•˜์˜€๊ธฐ์— ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### โ™ป๏ธ ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด (DTO) +- DAO๋ฅผ ํ†ตํ•ด DB์— ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•  ๋•Œ ํ•ด๋‹น DTO ๋ชจ๋ธ์„ ์ž‘์„ฑํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. +- ์ผ๋ถ€๋Š” DAO๋กœ ํ•ด๋†จ์ง€๋งŒ, ํ–ฅํ›„ ๊ฐœ์„ ํ•˜์—ฌ ์ด ํ”„๋กœ์ ํŠธ์—์„  DTO๋ฅผ ํ†ตํ•ด์„œ DAO๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์ž…๋‹ˆ๋‹ค. + +### ๐Ÿ’Ž ์ฃผ์š” ๊ธฐ์ˆ  ์Šคํƒ + +- Next.js(v15), React(v19), App Router +- Next-Auth V5 +- Prisma (NeonDB๋กœ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.) +- Tanstack Query +- TypeScript +- Style : Tailwind CSS +- UI Lib : Shadcn + + +
+ +
diff --git a/next.config.ts b/next.config.ts index 5e891cf..70326f4 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,26 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ + images: { + remotePatterns: [ + { + protocol: 'http', + hostname: 'k.kakaocdn.net', + }, + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, + { + protocol: 'https', + hostname: 'lh3.googleusercontent.com', + }, + { + protocol: 'https', + hostname: 'pstatic.net', + }, + ], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 8034af6..c60cb09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,37 @@ "name": "local-commu", "version": "0.1.0", "dependencies": { + "@auth/prisma-adapter": "^2.7.4", + "@hookform/resolvers": "^4.1.1", + "@prisma/client": "^6.4.1", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-query": "^5.66.9", + "@tanstack/react-query-devtools": "^5.66.9", + "axios": "^1.8.1", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "lucide-react": "^0.475.0", + "motion": "^12.4.7", "next": "15.1.7", + "next-auth": "^5.0.0-beta.25", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-icons": "^5.5.0", "tailwind-merge": "^3.0.2", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -29,6 +48,7 @@ "postcss": "^8", "prettier": "^3.5.2", "prettier-plugin-tailwindcss": "^0.6.11", + "prisma": "^6.4.1", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -44,6 +64,92 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", + "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "@types/cookie": "0.6.0", + "cookie": "0.7.1", + "jose": "^5.9.3", + "oauth4webapi": "^3.0.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.7.4.tgz", + "integrity": "sha512-3T/X94R9J1sxOLQtsD3ijIZ0JGHPXlZQxRr/8NpnZBJ3KGxun/mNsZ1MwMRhTxy0mmn9JWXk7u9+xCcVn0pu3A==", + "dependencies": { + "@auth/core": "0.37.4" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5" + } + }, + "node_modules/@auth/prisma-adapter/node_modules/@auth/core": { + "version": "0.37.4", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.4.tgz", + "integrity": "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw==", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^5.9.6", + "oauth4webapi": "^3.1.1", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter/node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@auth/prisma-adapter/node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -53,6 +159,406 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -172,6 +678,17 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.1.tgz", + "integrity": "sha512-S9YN1RgNWG+klUz5uQaV6rjE4pr6Py2tamj7ekshzLcMyg+/Pal1KZAYgGszV0+doiy41dUiQgXL3uRS9stndQ==", + "dependencies": { + "caniuse-lite": "^1.0.30001698" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -753,82 +1270,416 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz", - "integrity": "sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz", + "integrity": "sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.4.1.tgz", + "integrity": "sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.4.1.tgz", + "integrity": "sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.4.1.tgz", + "integrity": "sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "6.4.1", + "@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d", + "@prisma/fetch-engine": "6.4.1", + "@prisma/get-platform": "6.4.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d.tgz", + "integrity": "sha512-Xq54qw55vaCGrGgIJqyDwOq0TtjZPJEWsbQAHugk99hpDf2jcEeQhUcF+yzEsSqegBaDNLA4IC8Nn34sXmkiTQ==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.4.1.tgz", + "integrity": "sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "6.4.1", + "@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d", + "@prisma/get-platform": "6.4.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.4.1.tgz", + "integrity": "sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "6.4.1" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@radix-ui/react-use-layout-effect": "1.1.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "engines": { - "node": ">=12.4.0" + "node_modules/@radix-ui/react-label": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, @@ -849,6 +1700,68 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -874,6 +1787,66 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.66.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz", + "integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.65.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz", + "integrity": "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.66.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.9.tgz", + "integrity": "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A==", + "dependencies": { + "@tanstack/query-core": "5.66.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.66.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.9.tgz", + "integrity": "sha512-70G6AR35he53SYUcUK6EdqNR18zejCv1rM6900gjZP408EAex56YLwVSeijzk9lWeU2J42G9Fjh0i1WngUTsgw==", + "dependencies": { + "@tanstack/query-devtools": "5.65.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.66.9", + "react": "^18 || ^19" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -914,7 +1887,7 @@ "version": "19.0.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", - "dev": true, + "devOptional": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -1233,6 +2206,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -1406,6 +2390,11 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1430,6 +2419,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -1444,6 +2443,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1509,7 +2516,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -1683,6 +2689,17 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1697,6 +2714,14 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1784,11 +2809,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, + "devOptional": true, "dependencies": { "ms": "^2.1.3" }, @@ -1841,6 +2871,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -1850,6 +2888,11 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1876,7 +2919,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -1978,7 +3020,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -1987,7 +3028,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -2023,7 +3063,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -2035,7 +3074,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2075,6 +3113,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "devOptional": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2602,6 +3692,25 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -2632,6 +3741,46 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "12.4.7", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.7.tgz", + "integrity": "sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==", + "dependencies": { + "motion-dom": "^12.4.5", + "motion-utils": "^12.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2686,7 +3835,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -2706,11 +3854,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -2832,7 +3987,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2904,7 +4058,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2916,7 +4069,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -3435,6 +4587,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3604,7 +4764,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -3629,6 +4788,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3658,11 +4836,49 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion": { + "version": "12.4.7", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.4.7.tgz", + "integrity": "sha512-mhegHAbf1r80fr+ytC6OkjKvIUegRNXKLWNPrCN2+GnixlNSPwT03FtKqp9oDny1kNcLWZvwbmEr+JqVryFrcg==", + "dependencies": { + "framer-motion": "^12.4.7", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.4.5", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.5.tgz", + "integrity": "sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==", + "dependencies": { + "motion-utils": "^12.0.0" + } + }, + "node_modules/motion-utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz", + "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "devOptional": true }, "node_modules/mz": { "version": "2.7.0", @@ -3750,6 +4966,32 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.25", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz", + "integrity": "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==", + "dependencies": { + "@auth/core": "0.37.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0-0", + "nodemailer": "^6.6.5", + "react": "^18.2.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -3785,6 +5027,14 @@ "node": ">=0.10.0" } }, + "node_modules/oauth4webapi": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.0.tgz", + "integrity": "sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4201,6 +5451,26 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4303,6 +5573,40 @@ } } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, + "node_modules/prisma": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.4.1.tgz", + "integrity": "sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "6.4.1", + "esbuild": ">=0.12 <1", + "esbuild-register": "3.6.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4314,6 +5618,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4361,12 +5670,101 @@ "react": "^19.0.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==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "peerDependencies": { + "react": "*" + } + }, "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 }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5349,7 +6747,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5391,6 +6789,47 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5606,6 +7045,42 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "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==", + "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 3ddac1a..9b40d7c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "prisma generate && next build", "start": "next start", "lint": "next lint", "prepare": "husky" @@ -13,18 +13,37 @@ "*.{ts,tsx}": "eslint --fix" }, "dependencies": { + "@auth/prisma-adapter": "^2.7.4", + "@hookform/resolvers": "^4.1.1", + "@prisma/client": "^6.4.1", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-query": "^5.66.9", + "@tanstack/react-query-devtools": "^5.66.9", + "axios": "^1.8.1", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "lucide-react": "^0.475.0", + "motion": "^12.4.7", "next": "15.1.7", + "next-auth": "^5.0.0-beta.25", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-icons": "^5.5.0", "tailwind-merge": "^3.0.2", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -34,6 +53,7 @@ "postcss": "^8", "prettier": "^3.5.2", "prettier-plugin-tailwindcss": "^0.6.11", + "prisma": "^6.4.1", "tailwindcss": "^3.4.1", "typescript": "^5" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..3c8d928 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,115 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client{ + provider = "prisma-client-js" +} + +enum UserRole{ + ADMIN + USER +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? @map("email_verified") + image String? + password String? + role UserRole @default(USER) + accounts Account[] + + posts Post[] + comments Comment[] + postLikes PostLike[] + commentLikes CommentLike[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@map("users") +} + +model Account { + id String @id @default(cuid()) + userId String @map("user_id") + type String + provider String + providerAccountId String @map("provider_account_id") + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@map("accounts") +} + +model Post { + id String @id @default(cuid()) + title String + content String @db.Text + published Boolean @default(true) + viewCount Int @default(0) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String + + comments Comment[] + postLikes PostLike[] +} + +model Comment { + id String @id @default(cuid()) + content String @db.Text + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + postId String + + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String + + parentId String? + parent Comment? @relation("CommentToComment", fields: [parentId], references: [id], onDelete: SetNull) + replies Comment[] @relation("CommentToComment") + + commentLikes CommentLike[] +} + +model PostLike { + id String @id @default(cuid()) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + postId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + createdAt DateTime @default(now()) + + @@unique([postId, userId]) + @@index([userId]) + @@index([postId]) +} + +model CommentLike { + id String @id @default(cuid()) + comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) + commentId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + createdAt DateTime @default(now()) + + @@unique([commentId, userId]) + @@index([userId]) + @@index([commentId]) +} diff --git a/src/actions/login.ts b/src/actions/login.ts new file mode 100644 index 0000000..8529763 --- /dev/null +++ b/src/actions/login.ts @@ -0,0 +1,41 @@ +'use server'; + +import { z } from 'zod'; +import { loginSchema } from '@/schemas'; +import { signIn } from '@/auth'; +import { DEFAULT_LOGIN_REDIRECT } from '@/routes'; +import { AuthError } from 'next-auth'; +import UserDao from '@/dao/user'; + +export async function login(values: z.infer) { + const validatedFields = loginSchema.safeParse(values); + + if (!validatedFields.success) return { error: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ•„๋“œ์ž…๋‹ˆ๋‹ค.' }; + + const { email, password } = validatedFields.data; + + const existingUser = await UserDao.getUserByEmail(email); + if (!existingUser || !existingUser.email || !existingUser.password) { + return { error: 'ํšŒ์›๊ฐ€์ž…ํ•œ ์ด๋ ฅ์ด ์—†์Šต๋‹ˆ๋‹ค.' }; + } + + try { + await signIn('credentials', { + email, + password, + redirectTo: DEFAULT_LOGIN_REDIRECT, + }); + + return { success: '๋กœ๊ทธ์ธ์„ ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค!' }; + } catch (error) { + if (error instanceof AuthError) { + switch (error.type) { + case 'CredentialsSignin': + return { error: '๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.' }; + default: + return { error: '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' }; + } + } + throw error; + } +} diff --git a/src/actions/signup.ts b/src/actions/signup.ts new file mode 100644 index 0000000..1ba9363 --- /dev/null +++ b/src/actions/signup.ts @@ -0,0 +1,26 @@ +'use server'; + +import bcrypt from 'bcryptjs'; +import { z } from 'zod'; +import { signupSchema } from '@/schemas'; +import UserDao from '@/dao/user'; + +export async function signup(values: z.infer) { + const validatedFields = signupSchema.safeParse(values); + + if (!validatedFields.success) return { error: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ•„๋“œ์ž…๋‹ˆ๋‹ค.' }; + + const { email, password, name } = validatedFields.data; + const hashedPassword = await bcrypt.hash( + password, + Number(process.env.NEXT_PUBLIC_SOLT!), + ); + + const existingUser = await UserDao.getUserByEmail(email); + + if (existingUser) return { error: '์ด๋ฏธ ๊ฐ€์ž…ํ•œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.' }; + + const result = await UserDao.postUser(name, email, hashedPassword); + + return result; +} diff --git a/src/app/(auth)/auth/error/page.tsx b/src/app/(auth)/auth/error/page.tsx new file mode 100644 index 0000000..e99994e --- /dev/null +++ b/src/app/(auth)/auth/error/page.tsx @@ -0,0 +1,5 @@ +import ErrorCard from '@/components/auth/ErrorCard'; + +export default function AuthError() { + return ; +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..1d6815b --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,7 @@ +import { ReactNode } from 'react'; + +export default function AuthLayout({ children }: { children: ReactNode }) { + return ( +
{children}
+ ); +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..e5645eb --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,10 @@ +import LoginForm from '@/components/auth/LoginForm'; +import { Suspense } from 'react'; + +export default function Login() { + return ( + + + + ); +} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..7c0ac08 --- /dev/null +++ b/src/app/(auth)/signup/page.tsx @@ -0,0 +1,5 @@ +import SignupForm from '@/components/auth/SignupForm'; + +export default function Register() { + return ; +} diff --git a/src/app/(protected)/mypage/page.tsx b/src/app/(protected)/mypage/page.tsx new file mode 100644 index 0000000..fbb6432 --- /dev/null +++ b/src/app/(protected)/mypage/page.tsx @@ -0,0 +1,19 @@ +import { auth, signOut } from '@/auth'; + +export default async function MyPage() { + const session = await auth(); + + return ( +
+ {JSON.stringify(session)} +
{ + 'use server'; + await signOut({ redirectTo: '/login' }); + }} + > + +
+
+ ); +} diff --git a/src/app/(public)/map/page.tsx b/src/app/(public)/map/page.tsx new file mode 100644 index 0000000..afc7c31 --- /dev/null +++ b/src/app/(public)/map/page.tsx @@ -0,0 +1,12 @@ +import KakaoMap from '@/components/map/KakaoMap'; +import { KakaoProvider } from '@/context/KakaoContext'; + +export default function Map() { + return ( + <> + + + + + ); +} diff --git a/src/app/(public)/post/page.tsx b/src/app/(public)/post/page.tsx new file mode 100644 index 0000000..c962f67 --- /dev/null +++ b/src/app/(public)/post/page.tsx @@ -0,0 +1,20 @@ +import PostForm from '@/components/post/PostForm'; +import PostList from '@/components/post/PostList'; + +export default function Post() { + return ( + <> +
+
+

๊ฒŒ์‹œํŒ

+ +
+
+
+
+ +
+
+ + ); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..fae4907 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from '@/auth'; diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts new file mode 100644 index 0000000..7324d16 --- /dev/null +++ b/src/app/api/posts/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUser, unauthorized, badRequest } from '@/lib/apiUtils'; +import PostDao from '@/dao/post'; +import { postCreateSchema } from '@/schemas'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '10'); + const skip = (page - 1) * limit; + + const posts = await PostDao.getPosts({ + skip, + take: limit, + orderBy: { + createdAt: 'desc', + }, + include: { + author: { + select: { + id: true, + name: true, + image: true, + }, + }, + _count: { + select: { + comments: true, + postLikes: true, + }, + }, + }, + }); + + return NextResponse.json(posts); + } catch (error) { + console.error('๋ฐ์ดํผ ํŽ˜์นญ ์˜ค๋ฅ˜ :', error); + return NextResponse.json( + { error: '๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' }, + { status: 500 }, + ); + } +} + +export async function POST(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user || !user.id) { + return unauthorized(); + } + const body = await request.json(); + const validation = postCreateSchema.safeParse(body); + + if (!validation.success) { + return badRequest(validation.error.message); + } + + const { title, content } = validation.data; + + const post = await PostDao.createPost({ + data: { + title, + content, + author: { + connect: { id: user.id }, + }, + }, + include: { + author: { + select: { + id: true, + name: true, + image: true, + }, + }, + }, + }); + + return NextResponse.json(post, { status: 201 }); + } catch (error) { + console.error('๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ์˜ค๋ฅ˜ : ', error); + return NextResponse.json( + { error: '๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' }, + { status: 500 }, + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index 88ede11..8e5c091 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,6 +2,10 @@ @tailwind components; @tailwind utilities; +html, body, :root{ + height: 100%; +} + body, input::placeholder, textarea::placeholder { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cd6cbab..5d7eccc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,10 @@ import type { Metadata } from 'next'; import { Noto_Sans_KR } from 'next/font/google'; import './globals.css'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import QueryClientProvider from '@/queries/Provider'; +import ToastContainer from '@/components/ui/Toast'; +import SubmitSpinner from '@/components/ui/SubmitSpinner'; const notoSansKr = Noto_Sans_KR({ variable: '--font-noto-sans-kr', @@ -19,8 +23,15 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - {children} + + + + {children} + + + + + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 6e5d677..221e21a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,16 @@ +import Link from 'next/link'; + export default function Home() { - return
ํ™ˆํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค.
; + return ( +
+ ํ™ˆํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค. + + + ๊ฒŒ์‹œ๊ธ€ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ + +
+ ); } diff --git a/src/assets/images/empty-profile.jpg b/src/assets/images/empty-profile.jpg new file mode 100644 index 0000000..77721aa Binary files /dev/null and b/src/assets/images/empty-profile.jpg differ diff --git a/src/auth.config.ts b/src/auth.config.ts new file mode 100644 index 0000000..8933207 --- /dev/null +++ b/src/auth.config.ts @@ -0,0 +1,42 @@ +import type { NextAuthConfig } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import bcrypt from 'bcryptjs'; + +import { loginSchema } from '@/schemas'; +import UserDao from '@/dao/user'; + +import Github from 'next-auth/providers/github'; +import Google from 'next-auth/providers/google'; +import Kakao from 'next-auth/providers/kakao'; + +export default { + providers: [ + Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), + Github({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + Kakao({ + clientId: process.env.KAKAO_CLIENT_ID, + clientSecret: process.env.KAKAO_CLIENT_SECRET, + }), + Credentials({ + async authorize(credentials) { + const validatedFields = loginSchema.safeParse(credentials); + if (validatedFields.success) { + const { email, password } = validatedFields.data; + + const user = await UserDao.getUserByEmail(email); + if (!user || !user.password) return null; + + const passwordsMatch = await bcrypt.compare(password, user.password); + if (passwordsMatch) return user; + } + return null; + }, + }), + ], +} satisfies NextAuthConfig; diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..795f419 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,42 @@ +import NextAuth from 'next-auth'; +import authConfig from '@/auth.config'; +import { PrismaAdapter } from '@auth/prisma-adapter'; +import { db } from '@/lib/db'; +import UserDao from '@/dao/user'; +import { UserRole } from '@prisma/client'; + +export const { + auth, + signIn, + signOut, + handlers: { GET, POST }, +} = NextAuth({ + adapter: PrismaAdapter(db), + session: { strategy: 'jwt' }, + events: { + async linkAccount({ user }) { + UserDao.updateEmailVerified(user.id ?? ''); + }, + }, + pages: { + signIn: '/login', + error: '/auth/error', + }, + callbacks: { + async session({ token, session }) { + if (token.sub && session.user) session.user.id = token.sub; + + if (token.role && session.user) + session.user.role = token.role as UserRole; + return session; + }, + async jwt({ token }) { + if (!token.sub) return token; + const existingUser = await UserDao.getUserById(token.sub); + if (!existingUser) return token; + token.role = existingUser.role; + return token; + }, + }, + ...authConfig, +}); diff --git a/src/components/auth/BackButton.tsx b/src/components/auth/BackButton.tsx new file mode 100644 index 0000000..67eb58e --- /dev/null +++ b/src/components/auth/BackButton.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; + +interface BackButtonProps { + href: string; + label: string; +} + +export default function BackButton({ href, label }: BackButtonProps) { + return ( + + ); +} diff --git a/src/components/auth/CardWrapper.tsx b/src/components/auth/CardWrapper.tsx new file mode 100644 index 0000000..142bfb8 --- /dev/null +++ b/src/components/auth/CardWrapper.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { ReactNode } from 'react'; +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from '@/components/ui/card'; +import Header from '@/components/auth/Header'; +import Social from '@/components/auth/Social'; +import BackButton from '@/components/auth/BackButton'; + +interface CardWrapperProps { + children: ReactNode; + headerLabel: string; + backButtonLabel: string; + backButtonHref: string; + showSocial?: boolean; +} + +export default function CardWrapper({ + children, + headerLabel, + backButtonHref, + backButtonLabel, + showSocial, +}: CardWrapperProps) { + return ( + + +
+ + {children} + {showSocial && ( + + + + )} + + + + + ); +} diff --git a/src/components/auth/ErrorCard.tsx b/src/components/auth/ErrorCard.tsx new file mode 100644 index 0000000..7a6cd06 --- /dev/null +++ b/src/components/auth/ErrorCard.tsx @@ -0,0 +1,16 @@ +import CardWrapper from '@/components/auth/CardWrapper'; +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; + +export default function ErrorCard() { + return ( + +
+ +
+
+ ); +} diff --git a/src/components/auth/Header.tsx b/src/components/auth/Header.tsx new file mode 100644 index 0000000..cf73def --- /dev/null +++ b/src/components/auth/Header.tsx @@ -0,0 +1,12 @@ +interface HeaderProps { + label: string; +} + +export default function Header({ label }: HeaderProps) { + return ( +
+

LocalCommu

+

{label}

+
+ ); +} diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..6191459 --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import z from 'zod'; +import { loginSchema } from '@/schemas'; +import { useForm } from 'react-hook-form'; + +import CardWrapper from '@/components/auth/CardWrapper'; +import { + // + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { FormSuccess, FormError } from '@/components/auth/formStatus'; +import { login } from '@/actions/login'; +import { useState, useTransition } from 'react'; + +import { useSearchParams } from 'next/navigation'; + +export default function LoginForm() { + const searchParams = useSearchParams(); + const urlError = + searchParams.get('error') === 'OAuthAccountNotLinked' + ? '๋‹ค๋ฅธ ์†Œ์…œ ๊ณ„์ •์—์„œ ์‚ฌ์šฉ๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.' + : ''; + + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const form = useForm>({ + resolver: zodResolver(loginSchema), + defaultValues: { + email: '', + password: '', + }, + mode: 'onBlur', + }); + + const onSubmit = (value: z.infer) => { + setError(''); + setSuccess(''); + startTransition(() => { + login(value).then((data) => { + setError(data?.error ?? ''); + setSuccess(data?.success ?? ''); + }); + }); + }; + + return ( + +
+ +
+ { + return ( + + Email + + + + + + ); + }} + /> + { + return ( + + Password + + + + + + ); + }} + /> +
+ + + + + +
+ ); +} diff --git a/src/components/auth/SignupForm.tsx b/src/components/auth/SignupForm.tsx new file mode 100644 index 0000000..83439d5 --- /dev/null +++ b/src/components/auth/SignupForm.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import z from 'zod'; +import { signupSchema } from '@/schemas'; +import { useForm } from 'react-hook-form'; + +import CardWrapper from '@/components/auth/CardWrapper'; +import { + // + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { signup } from '@/actions/signup'; +import { useState, useTransition } from 'react'; +import { FormSuccess, FormError } from './formStatus'; + +export default function RegisterForm() { + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const form = useForm>({ + resolver: zodResolver(signupSchema), + defaultValues: { + email: '', + name: '', + password: '', + }, + mode: 'onBlur', + }); + + const onSubmit = (value: z.infer) => { + setError(''); + setSuccess(''); + startTransition(() => { + signup(value).then((data) => { + setError(data.error ?? ''); + setSuccess(data.success ?? ''); + }); + }); + }; + + return ( + +
+ +
+ { + return ( + + Email + + + + + + ); + }} + /> + { + return ( + + name + + + + + + ); + }} + /> + { + return ( + + Password + + + + + + ); + }} + /> +
+ + + + + +
+ ); +} diff --git a/src/components/auth/Social.tsx b/src/components/auth/Social.tsx new file mode 100644 index 0000000..7970f4a --- /dev/null +++ b/src/components/auth/Social.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { signIn } from 'next-auth/react'; + +import { FcGoogle } from 'react-icons/fc'; +import { FaGithub } from 'react-icons/fa'; +import { RiKakaoTalkFill } from 'react-icons/ri'; + +import { Button } from '@/components/ui/button'; +import { DEFAULT_LOGIN_REDIRECT } from '@/routes'; + +export default function Social() { + const onClick = (provider: 'google' | 'github' | 'kakao') => { + signIn(provider, { + callbackUrl: DEFAULT_LOGIN_REDIRECT, + }); + }; + + return ( +
+ + + +
+ ); +} diff --git a/src/components/auth/formStatus/FormError.tsx b/src/components/auth/formStatus/FormError.tsx new file mode 100644 index 0000000..3ebe4c2 --- /dev/null +++ b/src/components/auth/formStatus/FormError.tsx @@ -0,0 +1,16 @@ +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; + +interface FormErrorProps { + message?: string; +} + +export default function FormError({ message }: FormErrorProps) { + if (!message) return null; + + return ( +
+ +

{message}

+
+ ); +} diff --git a/src/components/auth/formStatus/FormSuccess.tsx b/src/components/auth/formStatus/FormSuccess.tsx new file mode 100644 index 0000000..b67faa9 --- /dev/null +++ b/src/components/auth/formStatus/FormSuccess.tsx @@ -0,0 +1,16 @@ +import { CheckCircledIcon } from '@radix-ui/react-icons'; + +interface FormSuccessProps { + message?: string; +} + +export default function FormSuccess({ message }: FormSuccessProps) { + if (!message) return null; + + return ( +
+ +

{message}

+
+ ); +} diff --git a/src/components/auth/formStatus/index.ts b/src/components/auth/formStatus/index.ts new file mode 100644 index 0000000..46d1808 --- /dev/null +++ b/src/components/auth/formStatus/index.ts @@ -0,0 +1,2 @@ +export { default as FormSuccess } from './FormSuccess'; +export { default as FormError } from './FormError'; diff --git a/src/components/map/KakaoMap.tsx b/src/components/map/KakaoMap.tsx new file mode 100644 index 0000000..18580e4 --- /dev/null +++ b/src/components/map/KakaoMap.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useKakao } from '@/context/KakaoContext'; +import { useEffect } from 'react'; +import SearchList from './SearchList'; + +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + kakao: any; + } +} + +/** + * ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋กœ๋“œ ๋  ๋•Œ KakaoAPI ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. + * @returns ์นด์นด์˜ค ์ง€๋„ ์ปดํฌ๋„ŒํŠธ + */ +export default function KakaoMap() { + const { setKakao } = useKakao(); + + useEffect(() => { + const kakaoMapScript = document.createElement('script'); + kakaoMapScript.async = false; + kakaoMapScript.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&autoload=false&libraries=services`; + document.head.appendChild(kakaoMapScript); + + const onLoadKakaoAPI = () => { + window.kakao.maps.load(() => { + const container = document.getElementById('map'); + const options = { + center: new window.kakao.maps.LatLng(33.450701, 126.570667), + level: 3, + }; + + setKakao({ + map: new window.kakao.maps.Map(container, options), + place: new window.kakao.maps.services.Places(), + infowindow: new window.kakao.maps.InfoWindow({ zIndex: 1 }), + }); + }); + }; + + kakaoMapScript.addEventListener('load', onLoadKakaoAPI); + + return () => { + kakaoMapScript.removeEventListener('load', onLoadKakaoAPI); + document.head.removeChild(kakaoMapScript); + }; + }, []); + + return ( + <> +
+ +
+ + ); +} diff --git a/src/components/map/SearchList.tsx b/src/components/map/SearchList.tsx new file mode 100644 index 0000000..201e197 --- /dev/null +++ b/src/components/map/SearchList.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import { useKakao } from '@/context/KakaoContext'; +import { placesSearchCB } from '@/lib/kakaoMap'; +import { useState } from 'react'; +import { Card, CardContent, CardHeader } from '../ui/card'; + +const searchKeywordSchema = z.object({ + keyword: z.string().min(1, { + message: 'ํ‚ค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”', + }), +}); + +interface Facility { + id: string; + address_name: string; + place_name: string; + place_url: string; + phone: string; +} + +export default function SearchList() { + const { kakao } = useKakao(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [list, setList] = useState(); + + const form = useForm>({ + resolver: zodResolver(searchKeywordSchema), + defaultValues: { + keyword: '', + }, + mode: 'onChange', + }); + + const onSubmit = (values: z.infer) => { + if (kakao) { + kakao.place.keywordSearch( + values.keyword, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (data: any, status: any, pagination: any) => { + placesSearchCB(data, status, pagination, kakao, setList); + }, + ); + } + }; + + const prevClick = () => { + list?.pagination.prevPage(); + }; + + const nextClick = () => { + list?.pagination.nextPage(); + }; + + return ( +
+
+ + { + return ( + + ํ‚ค์›Œ๋“œ + + + + + + ); + }} + /> + + + +
+
+ + +
+
+ {list?.data && + list.data.map((facility: Facility) => ( + + ))} +
+
+
+ ); +} + +function FacilityCard({ data }: { data: Facility }) { + return ( + + + {data.place_name} + +

์ฃผ์†Œ : {data.address_name}

+

์ „ํ™”๋ฒˆํ˜ธ : {data.phone}

+
+
+
+ ); +} diff --git a/src/components/post/PostCard.tsx b/src/components/post/PostCard.tsx new file mode 100644 index 0000000..7066d5e --- /dev/null +++ b/src/components/post/PostCard.tsx @@ -0,0 +1,83 @@ +import { Post } from '@/types'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '../ui/card'; +import { FaRegThumbsUp } from 'react-icons/fa'; +import { FaRegComment } from 'react-icons/fa'; +import Image from 'next/image'; +import emptyProfile from '@/assets/images/empty-profile.jpg'; +import { convertDateFormat } from '@/lib/convertDateFormat'; +import { Skeleton } from '@/components/ui/skeleton'; + +export default function PostCard({ post }: { post: Post }) { + return ( + + + +

{post.title}

+
+ + {post.likeCount} +
+
+
+ {`${post.author.name}๋‹˜์˜ + {post.author.name} + {convertDateFormat(post.createdAt)} +
+
+ +

{post.content}

+
+ +
+ + {post.commentCount} + ๋Œ“๊ธ€ +
+
+
+ ); +} + +export function PostCardSkeleton() { + return ( + + +
+ +
+ + +
+
+ +
+ + + +
+
+ + + + +
+ + + +
+
+
+ ); +} diff --git a/src/components/post/PostForm.tsx b/src/components/post/PostForm.tsx new file mode 100644 index 0000000..51e41fc --- /dev/null +++ b/src/components/post/PostForm.tsx @@ -0,0 +1,140 @@ +'use client'; + +import z from 'zod'; +import { postCreateSchema } from '@/schemas'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { CiCirclePlus } from 'react-icons/ci'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { useCreatePost } from '@/queries/posts'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { getErrorMessage } from '@/lib/errorUtils'; +import { useToastStore } from '@/stores/ToastStore'; +import { useSubmitSpinnerStore } from '@/stores/SubmitSpinnerStore'; + +export default function PostForm() { + const form = useForm>({ + resolver: zodResolver(postCreateSchema), + defaultValues: { + title: '', + content: '', + }, + mode: 'onChange', + }); + + const { mutateAsync: createPost } = useCreatePost(); + + const [open, setOpen] = useState(false); + const router = useRouter(); + const { addToast } = useToastStore(); + const { setVisible } = useSubmitSpinnerStore(); + + const onSubmit = async (values: z.infer) => { + try { + setVisible(true); + await createPost(values); + setOpen(false); + form.reset(); + setVisible(false); + addToast({ + message: '๊ฒŒ์‹œ๊ธ€์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!', + type: 'success', + }); + } catch (error) { + setOpen(false); + form.reset(); + setVisible(false); + addToast({ + message: getErrorMessage(error), + type: 'error', + }); + router.push('/login'); + } + }; + + return ( + { + setOpen(isOpen); + if (!isOpen) form.reset(); + }} + > + + + + + + ์ƒˆ ๊ธ€ ์ž‘์„ฑ + + +
+ + ( + + ์ œ๋ชฉ + + + + + + )} + /> + ( + + ๋‚ด์šฉ + +