diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..a4c6f0dd --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,64 @@ +## Purpose +Provide concise, actionable instructions for an AI coding agent to work productively in this repository: a Vite + React frontend with Prisma + Supabase integrations and project-specific conventions. + +## Quick start (commands) +- Start dev server: `npm run dev` (Vite) — opens on http://localhost:5173 by default. +- Build static assets: `npm run build`. +- Preview production build: `npm run preview`. +- Lint code: `npm run lint` (ESLint; see `eslint.config.js`). + +Reference: `package.json` contains the scripts above. + +## Big-picture architecture +- Frontend: React + Vite (source in `src/`). Entry: `src/main.jsx` and `src/App.jsx`. +- UI is organized by features/pages under `src/pages/` and reusable pieces under `src/components/` (each feature folder commonly has an `index.jsx` and `style.jsx`). +- Styling: `styled-components` and some `@emotion` are used. Files that define styles are often named `style.jsx` next to the component. +- Data & APIs: `src/api/` contains `api.jsx`, `fetch.jsx`, and `supabase.jsx` — this is the surface for network calls and service integration. React Query (`@tanstack/react-query`) is used in hooks for data fetching. +- Backend/DB client: Prisma schema is in `prisma/schema.prisma` and a generated client exists under `prisma/generated/prisma/client.ts` — do not edit generated files. + +## Important directories and files (examples) +- `src/components/` — common components (e.g., `MovieCard.jsx`, `MovieGrid.jsx`, `SearchInput.jsx`). +- `src/pages/` — page-level components grouped by route (e.g., `DetailPage`, `MainPage`, `SearchPage`). +- `src/constants/` — app constants. Example: `src/constants/tabs.jsx` defines tab mapping used by the Detail page: + - To add a tab, update `src/constants/tabs.jsx` with a new key and component mapping (e.g., `{ help: { label: '도움', component: HelpComponent } }`). +- `src/context/AuthContext.jsx` — authentication context used across the app. +- `src/hooks/` — custom hooks (naming: `useSomething.jsx`, e.g., `useFetchData.jsx`, `useInfiniteMovies.jsx`). Follow existing function shapes. +- `public/data/` — static JSON samples used by the app (e.g., `movieListData.json`, `movieDetailData.json`). Useful for offline testing. + +## Data flow & integration notes +- Primary fetch layer: `src/api/fetch.jsx` and `src/api/api.jsx`. Many components use React Query hooks that call utilities in `src/api/`. +- Supabase is integrated in `src/api/supabase.jsx` and used where auth or DB features are needed. Check `src/context/AuthContext.jsx` and `src/pages/LoginPage` for concrete usage. +- Prisma client exists in `prisma/generated/prisma`. If you need DB schema changes, edit `prisma/schema.prisma` and run the Prisma CLI locally (e.g., `npx prisma migrate dev`), but be cautious: migrations are environment-sensitive. Generated client files should not be edited manually. +- Environment variables: `dotenv` is present in dependencies — local dev likely requires a `.env` for Supabase/Prisma credentials. The repo does not include secrets. + +## Conventions and patterns to follow +- File naming: components and pages use `.jsx` (this is a JS project, not TypeScript). Keep exports default where existing files do. +- Style files: component-specific styles live in `style.jsx` next to the component; prefer the existing styled-components pattern. +- Component exports: many folders have `index.jsx` that re-exports inner components — use that pattern when adding new feature folders. +- Hooks: use the `useX` prefix and put shared logic into `src/hooks/`. +- State & data fetching: prefer React Query for server data and local hooks for derived or UI-only state (`useDebounce`, `useIntersectionObserver`, etc.). See `src/hooks/useInfiniteMovies.jsx` for an example. + +## Editing and codegen rules +- Do not edit files under `prisma/generated/` — regenerate them from Prisma schema instead. +- Keep ESLint happy: `npm run lint` is available; preserve current code style and patterns. + +## Debugging tips +- Use `npm run dev` and open the browser console for client errors. +- Vite Hot Module Reloading (HMR) is enabled — iterative edits to components/style files update quickly. +- For data-related bugs, compare live API responses vs `public/data/*.json` which contain sample payloads used in the UI. + +## When adding features (practical checklist) +1. Add components under `src/components/` or a page under `src/pages/` following existing folder structure. +2. Add styles in `style.jsx` and export the component from the folder `index.jsx`. +3. If new network calls are needed, add them to `src/api/` and use React Query hooks in `src/hooks/` when appropriate. +4. Update `src/constants/` for central mappings (e.g., `tabs.jsx`) rather than sprinkling strings across components. + +## Where to look for examples +- Tab pattern: `src/constants/tabs.jsx` (maps keys to components used by DetailPage). +- Auth flow: `src/context/AuthContext.jsx` + `src/pages/LoginPage`. +- Data fetch + infinite scroll example: `src/hooks/useInfiniteMovies.jsx` and `src/components/Carousel.jsx`. + +## Final note +Keep instructions concrete and limited to discoverable patterns above. If something requires environment secrets or backend migration, request those details before attempting changes. + +Please review and tell me if you'd like more examples (small code snippets) or added run/debug workflows (Prisma commands, Supabase seed steps) — I can expand those when you confirm environment details. diff --git a/jsconfig.json b/jsconfig.json index ef37cad9..dac60676 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -5,7 +5,7 @@ "@/*": ["src/*"], "@components/*": ["src/components/*"], "@api/*": ["src/api/*"], - "@hooks/*": ["src/hook/*"], + "@hooks/*": ["src/hooks/*"], "@pages/*": ["src/pages/*"] } }, diff --git a/package-lock.json b/package-lock.json index 2bc7e7b9..bf44e0f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "oz_react_mini_14", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", @@ -15,6 +17,7 @@ "@fortawesome/react-fontawesome": "^3.1.0", "@prisma/client": "^6.18.0", "@supabase/supabase-js": "^2.78.0", + "@tanstack/react-query": "^5.90.7", "axios": "^1.13.1", "dotenv": "^17.2.3", "lodash": "^4.17.21", @@ -43,7 +46,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -99,7 +101,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", @@ -133,7 +134,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -143,7 +143,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -185,7 +184,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -195,7 +193,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -229,7 +226,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" @@ -273,11 +269,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -292,7 +296,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -311,7 +314,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -321,6 +323,74 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", @@ -336,12 +406,126 @@ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", "license": "MIT" }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/styled/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", "license": "MIT" }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1067,7 +1251,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1089,7 +1272,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1099,14 +1281,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1599,6 +1779,32 @@ "@supabase/storage-js": "2.78.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.7.tgz", + "integrity": "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.7.tgz", + "integrity": "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1667,6 +1873,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/phoenix": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", @@ -1809,6 +2021,21 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1930,7 +2157,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2081,6 +2307,31 @@ "node": ">=18" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2126,7 +2377,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2234,6 +2484,15 @@ "node": ">=14" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2335,7 +2594,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2603,6 +2861,12 @@ "node": ">=16.0.0" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2853,6 +3117,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2867,7 +3140,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2890,6 +3162,27 @@ "node": ">=0.8.19" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2934,7 +3227,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2954,7 +3246,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2970,6 +3261,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3021,6 +3318,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3113,7 +3416,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3236,7 +3538,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3245,6 +3546,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3265,6 +3584,21 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3445,6 +3779,12 @@ "react": "^19.2.0" } }, + "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==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3507,11 +3847,30 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3610,6 +3969,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3690,6 +4058,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/swiper": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz", @@ -3979,6 +4359,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 1df00248..11696b33 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", @@ -17,6 +19,7 @@ "@fortawesome/react-fontawesome": "^3.1.0", "@prisma/client": "^6.18.0", "@supabase/supabase-js": "^2.78.0", + "@tanstack/react-query": "^5.90.7", "axios": "^1.13.1", "dotenv": "^17.2.3", "lodash": "^4.17.21", diff --git a/public/generated/prisma/browser.ts b/public/generated/prisma/browser.ts new file mode 100644 index 00000000..92bfaa51 --- /dev/null +++ b/public/generated/prisma/browser.ts @@ -0,0 +1,28 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * This file should be your main import to use Prisma-related types and utilities in a browser. + * Use it to get access to models, enums, and input types. + * + * This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only. + * See `client.ts` for the standard, server-side entry point. + * + * 🟢 You can import this file directly. + */ + +import * as Prisma from './internal/prismaNamespaceBrowser.ts' +export { Prisma } +export * as $Enums from './enums.ts' +export * from './enums.ts'; +/** + * Model User + * + */ +export type User = Prisma.UserModel +/** + * Model Profile + * + */ +export type Profile = Prisma.ProfileModel diff --git a/public/generated/prisma/client.ts b/public/generated/prisma/client.ts new file mode 100644 index 00000000..9a8de95f --- /dev/null +++ b/public/generated/prisma/client.ts @@ -0,0 +1,55 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types. + * If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead. + * + * 🟢 You can import this file directly. + */ + +import * as process from 'node:process' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' +globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url)) + +import * as runtime from "@prisma/client/runtime/library" +import * as $Enums from "./enums.ts" +import * as $Class from "./internal/class.ts" +import * as Prisma from "./internal/prismaNamespace.ts" + +export * as $Enums from './enums.ts' +export * from "./enums.ts" +/** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient() + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client). + */ +export const PrismaClient = $Class.getPrismaClientClass(__dirname) +export type PrismaClient = $Class.PrismaClient +export { Prisma } + + +// file annotations for bundling tools to include these files +path.join(__dirname, "libquery_engine-darwin-arm64.dylib.node") +path.join(process.cwd(), "src/generated/prisma/libquery_engine-darwin-arm64.dylib.node") + +/** + * Model User + * + */ +export type User = Prisma.UserModel +/** + * Model Profile + * + */ +export type Profile = Prisma.ProfileModel diff --git a/public/generated/prisma/commonInputTypes.ts b/public/generated/prisma/commonInputTypes.ts new file mode 100644 index 00000000..cfe086c4 --- /dev/null +++ b/public/generated/prisma/commonInputTypes.ts @@ -0,0 +1,145 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * This file exports various common sort, input & filter types that are not directly linked to a particular model. + * + * 🟢 You can import this file directly. + */ + +import type * as runtime from "@prisma/client/runtime/library" +import * as $Enums from "./enums.ts" +import type * as Prisma from "./internal/prismaNamespace.ts" + + +export type IntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntFilter<$PrismaModel> | number +} + +export type StringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringFilter<$PrismaModel> | string +} + +export type IntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number + _count?: Prisma.NestedIntFilter<$PrismaModel> + _avg?: Prisma.NestedFloatFilter<$PrismaModel> + _sum?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedIntFilter<$PrismaModel> + _max?: Prisma.NestedIntFilter<$PrismaModel> +} + +export type StringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedStringFilter<$PrismaModel> + _max?: Prisma.NestedStringFilter<$PrismaModel> +} + +export type NestedIntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntFilter<$PrismaModel> | number +} + +export type NestedStringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringFilter<$PrismaModel> | string +} + +export type NestedIntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number + _count?: Prisma.NestedIntFilter<$PrismaModel> + _avg?: Prisma.NestedFloatFilter<$PrismaModel> + _sum?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedIntFilter<$PrismaModel> + _max?: Prisma.NestedIntFilter<$PrismaModel> +} + +export type NestedFloatFilter<$PrismaModel = never> = { + equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> + lt?: number | Prisma.FloatFieldRefInput<$PrismaModel> + lte?: number | Prisma.FloatFieldRefInput<$PrismaModel> + gt?: number | Prisma.FloatFieldRefInput<$PrismaModel> + gte?: number | Prisma.FloatFieldRefInput<$PrismaModel> + not?: Prisma.NestedFloatFilter<$PrismaModel> | number +} + +export type NestedStringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedStringFilter<$PrismaModel> + _max?: Prisma.NestedStringFilter<$PrismaModel> +} + + diff --git a/public/generated/prisma/enums.ts b/public/generated/prisma/enums.ts new file mode 100644 index 00000000..0189a344 --- /dev/null +++ b/public/generated/prisma/enums.ts @@ -0,0 +1,14 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* +* This file exports all enum related types from the schema. +* +* 🟢 You can import this file directly. +*/ + + + +// This file is empty because there are no enums in the schema. +export {} diff --git a/public/generated/prisma/internal/class.ts b/public/generated/prisma/internal/class.ts new file mode 100644 index 00000000..617b5c48 --- /dev/null +++ b/public/generated/prisma/internal/class.ts @@ -0,0 +1,228 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * Please import the `PrismaClient` class from the `client.ts` file instead. + */ + +import * as runtime from "@prisma/client/runtime/library" +import type * as Prisma from "./prismaNamespace.ts" + + +const config: runtime.GetPrismaClientConfig = { + "generator": { + "name": "client", + "provider": { + "fromEnvVar": null, + "value": "prisma-client" + }, + "output": { + "value": "/Users/admin/Desktop/oz_react_mini_14/src/generated/prisma", + "fromEnvVar": null + }, + "config": { + "engineType": "library" + }, + "binaryTargets": [ + { + "fromEnvVar": null, + "value": "darwin-arm64", + "native": true + } + ], + "previewFeatures": [], + "sourceFilePath": "/Users/admin/Desktop/oz_react_mini_14/prisma/schema.prisma", + "isCustomOutput": true + }, + "relativePath": "../../../prisma", + "clientVersion": "6.18.0", + "engineVersion": "34b5a692b7bd79939a9a2c3ef97d816e749cda2f", + "datasourceNames": [ + "db" + ], + "activeProvider": "postgresql", + "postinstall": true, + "inlineDatasources": { + "db": { + "url": { + "fromEnvVar": "DATABASE_URL", + "value": null + } + } + }, + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n email String @unique\n name String\n}\n\nmodel Profile {\n id Int @id @default(autoincrement())\n bio String\n userId Int @unique\n}\n", + "inlineSchemaHash": "36c993f3fb49d83980b8571b5bf3a16caec262c9552c96728ba7c11ab8676f5b", + "copyEngine": true, + "runtimeDataModel": { + "models": {}, + "enums": {}, + "types": {} + }, + "dirname": "" +} + +config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"dbName\":null,\"schema\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"nativeType\":null,\"default\":{\"name\":\"autoincrement\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"email\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":true,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"nativeType\":null,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"nativeType\":null,\"isGenerated\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Profile\":{\"dbName\":null,\"schema\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"nativeType\":null,\"default\":{\"name\":\"autoincrement\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"bio\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"nativeType\":null,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"userId\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":true,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Int\",\"nativeType\":null,\"isGenerated\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}") +config.engineWasm = undefined +config.compilerWasm = undefined + + + + +export type LogOptions = + 'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array ? Prisma.GetEvents : never : never + +export interface PrismaClientConstructor { + /** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient() + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client). + */ + + new < + Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions, + LogOpts extends LogOptions = LogOptions, + OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'], + ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs + >(options?: Prisma.Subset ): PrismaClient +} + +/** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient() + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client). + */ + +export interface PrismaClient< + in LogOpts extends Prisma.LogLevel = never, + in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'], + in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs +> { + [K: symbol]: { types: Prisma.TypeMap['other'] } + + $on(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient; + + /** + * Connect with the database + */ + $connect(): runtime.Types.Utils.JsPromise; + + /** + * Disconnect from the database + */ + $disconnect(): runtime.Types.Utils.JsPromise; + +/** + * Executes a prepared raw query and returns the number of affected rows. + * @example + * ``` + * const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};` + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). + */ + $executeRaw(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise; + + /** + * Executes a raw query and returns the number of affected rows. + * Susceptible to SQL injections, see documentation. + * @example + * ``` + * const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com') + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). + */ + $executeRawUnsafe(query: string, ...values: any[]): Prisma.PrismaPromise; + + /** + * Performs a prepared raw query and returns the `SELECT` data. + * @example + * ``` + * const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};` + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). + */ + $queryRaw(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise; + + /** + * Performs a raw query and returns the `SELECT` data. + * Susceptible to SQL injections, see documentation. + * @example + * ``` + * const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com') + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). + */ + $queryRawUnsafe(query: string, ...values: any[]): Prisma.PrismaPromise; + + + /** + * Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole. + * @example + * ``` + * const [george, bob, alice] = await prisma.$transaction([ + * prisma.user.create({ data: { name: 'George' } }), + * prisma.user.create({ data: { name: 'Bob' } }), + * prisma.user.create({ data: { name: 'Alice' } }), + * ]) + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions). + */ + $transaction

[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise> + + $transaction(fn: (prisma: Omit) => runtime.Types.Utils.JsPromise, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise + + + $extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb, ExtArgs, runtime.Types.Utils.Call, { + extArgs: ExtArgs + }>> + + /** + * `prisma.user`: Exposes CRUD operations for the **User** model. + * Example usage: + * ```ts + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + */ + get user(): Prisma.UserDelegate; + + /** + * `prisma.profile`: Exposes CRUD operations for the **Profile** model. + * Example usage: + * ```ts + * // Fetch zero or more Profiles + * const profiles = await prisma.profile.findMany() + * ``` + */ + get profile(): Prisma.ProfileDelegate; +} + +export function getPrismaClientClass(dirname: string): PrismaClientConstructor { + config.dirname = dirname + return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor +} diff --git a/public/generated/prisma/internal/prismaNamespace.ts b/public/generated/prisma/internal/prismaNamespace.ts new file mode 100644 index 00000000..2604e29c --- /dev/null +++ b/public/generated/prisma/internal/prismaNamespace.ts @@ -0,0 +1,832 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file. + * While this enables partial backward compatibility, it is not part of the stable public API. + * + * If you are looking for your Models, Enums, and Input Types, please import them from the respective + * model files in the `model` directory! + */ + +import * as runtime from "@prisma/client/runtime/library" +import type * as Prisma from "../models.ts" +import { type PrismaClient } from "./class.ts" + +export type * from '../models.ts' + +export type DMMF = typeof runtime.DMMF + +export type PrismaPromise = runtime.Types.Public.PrismaPromise + +/** + * Prisma Errors + */ + +export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError +export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError + +export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError +export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError + +export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError +export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError + +export const PrismaClientInitializationError = runtime.PrismaClientInitializationError +export type PrismaClientInitializationError = runtime.PrismaClientInitializationError + +export const PrismaClientValidationError = runtime.PrismaClientValidationError +export type PrismaClientValidationError = runtime.PrismaClientValidationError + +/** + * Re-export of sql-template-tag + */ +export const sql = runtime.sqltag +export const empty = runtime.empty +export const join = runtime.join +export const raw = runtime.raw +export const Sql = runtime.Sql +export type Sql = runtime.Sql + + + +/** + * Decimal.js + */ +export const Decimal = runtime.Decimal +export type Decimal = runtime.Decimal + +export type DecimalJsLike = runtime.DecimalJsLike + +/** + * Metrics + */ +export type Metrics = runtime.Metrics +export type Metric = runtime.Metric +export type MetricHistogram = runtime.MetricHistogram +export type MetricHistogramBucket = runtime.MetricHistogramBucket + +/** +* Extensions +*/ +export type Extension = runtime.Types.Extensions.UserArgs +export const getExtensionContext = runtime.Extensions.getExtensionContext +export type Args = runtime.Types.Public.Args +export type Payload = runtime.Types.Public.Payload +export type Result = runtime.Types.Public.Result +export type Exact = runtime.Types.Public.Exact + +export type PrismaVersion = { + client: string + engine: string +} + +/** + * Prisma Client JS version: 6.18.0 + * Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f + */ +export const prismaVersion: PrismaVersion = { + client: "6.18.0", + engine: "34b5a692b7bd79939a9a2c3ef97d816e749cda2f" +} + +/** + * Utility Types + */ + +export type Bytes = runtime.Bytes +export type JsonObject = runtime.JsonObject +export type JsonArray = runtime.JsonArray +export type JsonValue = runtime.JsonValue +export type InputJsonObject = runtime.InputJsonObject +export type InputJsonArray = runtime.InputJsonArray +export type InputJsonValue = runtime.InputJsonValue + + +export const NullTypes = { + DbNull: runtime.objectEnumValues.classes.DbNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.DbNull), + JsonNull: runtime.objectEnumValues.classes.JsonNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.JsonNull), + AnyNull: runtime.objectEnumValues.classes.AnyNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.AnyNull), +} +/** + * Helper for filtering JSON entries that have `null` on the database (empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const DbNull = runtime.objectEnumValues.instances.DbNull +/** + * Helper for filtering JSON entries that have JSON `null` values (not empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const JsonNull = runtime.objectEnumValues.instances.JsonNull +/** + * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull` + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const AnyNull = runtime.objectEnumValues.instances.AnyNull + + +type SelectAndInclude = { + select: any + include: any +} + +type SelectAndOmit = { + select: any + omit: any +} + +/** + * From T, pick a set of properties whose keys are in the union K + */ +type Prisma__Pick = { + [P in K]: T[P]; +}; + +export type Enumerable = T | Array; + +/** + * Subset + * @desc From `T` pick properties that exist in `U`. Simple version of Intersection + */ +export type Subset = { + [key in keyof T]: key extends keyof U ? T[key] : never; +}; + +/** + * SelectSubset + * @desc From `T` pick properties that exist in `U`. Simple version of Intersection. + * Additionally, it validates, if both select and include are present. If the case, it errors. + */ +export type SelectSubset = { + [key in keyof T]: key extends keyof U ? T[key] : never +} & + (T extends SelectAndInclude + ? 'Please either choose `select` or `include`.' + : T extends SelectAndOmit + ? 'Please either choose `select` or `omit`.' + : {}) + +/** + * Subset + Intersection + * @desc From `T` pick properties that exist in `U` and intersect `K` + */ +export type SubsetIntersection = { + [key in keyof T]: key extends keyof U ? T[key] : never +} & + K + +type Without = { [P in Exclude]?: never }; + +/** + * XOR is needed to have a real mutually exclusive union type + * https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types + */ +export type XOR = + T extends object ? + U extends object ? + (Without & U) | (Without & T) + : U : T + + +/** + * Is T a Record? + */ +type IsObject = T extends Array +? False +: T extends Date +? False +: T extends Uint8Array +? False +: T extends BigInt +? False +: T extends object +? True +: False + + +/** + * If it's T[], return T + */ +export type UnEnumerate = T extends Array ? U : T + +/** + * From ts-toolbelt + */ + +type __Either = Omit & + { + // Merge all but K + [P in K]: Prisma__Pick // With K possibilities + }[K] + +type EitherStrict = Strict<__Either> + +type EitherLoose = ComputeRaw<__Either> + +type _Either< + O extends object, + K extends Key, + strict extends Boolean +> = { + 1: EitherStrict + 0: EitherLoose +}[strict] + +export type Either< + O extends object, + K extends Key, + strict extends Boolean = 1 +> = O extends unknown ? _Either : never + +export type Union = any + +export type PatchUndefined = { + [K in keyof O]: O[K] extends undefined ? At : O[K] +} & {} + +/** Helper Types for "Merge" **/ +export type IntersectOf = ( + U extends unknown ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never + +export type Overwrite = { + [K in keyof O]: K extends keyof O1 ? O1[K] : O[K]; +} & {}; + +type _Merge = IntersectOf; +}>>; + +type Key = string | number | symbol; +type AtStrict = O[K & keyof O]; +type AtLoose = O extends unknown ? AtStrict : never; +export type At = { + 1: AtStrict; + 0: AtLoose; +}[strict]; + +export type ComputeRaw = A extends Function ? A : { + [K in keyof A]: A[K]; +} & {}; + +export type OptionalFlat = { + [K in keyof O]?: O[K]; +} & {}; + +type _Record = { + [P in K]: T; +}; + +// cause typescript not to expand types and preserve names +type NoExpand = T extends unknown ? T : never; + +// this type assumes the passed object is entirely optional +export type AtLeast = NoExpand< + O extends unknown + ? | (K extends keyof O ? { [P in K]: O[P] } & O : O) + | {[P in keyof O as P extends K ? P : never]-?: O[P]} & O + : never>; + +type _Strict = U extends unknown ? U & OptionalFlat<_Record, keyof U>, never>> : never; + +export type Strict = ComputeRaw<_Strict>; +/** End Helper Types for "Merge" **/ + +export type Merge = ComputeRaw<_Merge>>; + +export type Boolean = True | False + +export type True = 1 + +export type False = 0 + +export type Not = { + 0: 1 + 1: 0 +}[B] + +export type Extends = [A1] extends [never] + ? 0 // anything `never` is false + : A1 extends A2 + ? 1 + : 0 + +export type Has = Not< + Extends, U1> +> + +export type Or = { + 0: { + 0: 0 + 1: 1 + } + 1: { + 0: 1 + 1: 1 + } +}[B1][B2] + +export type Keys = U extends unknown ? keyof U : never + +export type GetScalarType = O extends object ? { + [P in keyof T]: P extends keyof O + ? O[P] + : never +} : never + +type FieldPaths< + T, + U = Omit +> = IsObject extends True ? U : T + +export type GetHavingFields = { + [K in keyof T]: Or< + Or, Extends<'AND', K>>, + Extends<'NOT', K> + > extends True + ? // infer is only needed to not hit TS limit + // based on the brilliant idea of Pierre-Antoine Mills + // https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437 + T[K] extends infer TK + ? GetHavingFields extends object ? Merge> : never> + : never + : {} extends FieldPaths + ? never + : K +}[keyof T] + +/** + * Convert tuple to union + */ +type _TupleToUnion = T extends (infer E)[] ? E : never +type TupleToUnion = _TupleToUnion +export type MaybeTupleToUnion = T extends any[] ? TupleToUnion : T + +/** + * Like `Pick`, but additionally can also accept an array of keys + */ +export type PickEnumerable | keyof T> = Prisma__Pick> + +/** + * Exclude all keys with underscores + */ +export type ExcludeUnderscoreKeys = T extends `_${string}` ? never : T + + +export type FieldRef = runtime.FieldRef + +type FieldRefInputType = Model extends never ? never : FieldRef + + +export const ModelName = { + User: 'User', + Profile: 'Profile' +} as const + +export type ModelName = (typeof ModelName)[keyof typeof ModelName] + + + +export interface TypeMapCb extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record> { + returns: TypeMap +} + +export type TypeMap = { + globalOmitOptions: { + omit: GlobalOmitOptions + } + meta: { + modelProps: "user" | "profile" + txIsolationLevel: TransactionIsolationLevel + } + model: { + User: { + payload: Prisma.$UserPayload + fields: Prisma.UserFieldRefs + operations: { + findUnique: { + args: Prisma.UserFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.UserFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.UserFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.UserFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.UserFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.UserCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.UserCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.UserCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.UserDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.UserUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.UserDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.UserUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.UserUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.UserUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.UserAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.UserGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.UserCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + Profile: { + payload: Prisma.$ProfilePayload + fields: Prisma.ProfileFieldRefs + operations: { + findUnique: { + args: Prisma.ProfileFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.ProfileFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.ProfileFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.ProfileFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.ProfileFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.ProfileCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.ProfileCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.ProfileCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.ProfileDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.ProfileUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.ProfileDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.ProfileUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.ProfileUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.ProfileUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.ProfileAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.ProfileGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.ProfileCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + } +} & { + other: { + payload: any + operations: { + $executeRaw: { + args: [query: TemplateStringsArray | Sql, ...values: any[]], + result: any + } + $executeRawUnsafe: { + args: [query: string, ...values: any[]], + result: any + } + $queryRaw: { + args: [query: TemplateStringsArray | Sql, ...values: any[]], + result: any + } + $queryRawUnsafe: { + args: [query: string, ...values: any[]], + result: any + } + } + } +} + +/** + * Enums + */ + +export const TransactionIsolationLevel = runtime.makeStrictEnum({ + ReadUncommitted: 'ReadUncommitted', + ReadCommitted: 'ReadCommitted', + RepeatableRead: 'RepeatableRead', + Serializable: 'Serializable' +} as const) + +export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + + +export const UserScalarFieldEnum = { + id: 'id', + email: 'email', + name: 'name' +} as const + +export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum] + + +export const ProfileScalarFieldEnum = { + id: 'id', + bio: 'bio', + userId: 'userId' +} as const + +export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum] + + +export const SortOrder = { + asc: 'asc', + desc: 'desc' +} as const + +export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + + +export const QueryMode = { + default: 'default', + insensitive: 'insensitive' +} as const + +export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + + + +/** + * Field references + */ + + +/** + * Reference to a field of type 'Int' + */ +export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'> + + + +/** + * Reference to a field of type 'Int[]' + */ +export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'> + + + +/** + * Reference to a field of type 'String' + */ +export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'> + + + +/** + * Reference to a field of type 'String[]' + */ +export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'> + + + +/** + * Reference to a field of type 'Float' + */ +export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'> + + + +/** + * Reference to a field of type 'Float[]' + */ +export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'> + + +/** + * Batch Payload for updateMany & deleteMany & createMany + */ +export type BatchPayload = { + count: number +} + + +export type Datasource = { + url?: string +} +export type Datasources = { + db?: Datasource +} + +export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs> +export type DefaultPrismaClient = PrismaClient +export type ErrorFormat = 'pretty' | 'colorless' | 'minimal' +export interface PrismaClientOptions { + /** + * Overwrites the datasource url from your schema.prisma file + */ + datasources?: Datasources + /** + * Overwrites the datasource url from your schema.prisma file + */ + datasourceUrl?: string + /** + * @default "colorless" + */ + errorFormat?: ErrorFormat + /** + * @example + * ``` + * // Shorthand for `emit: 'stdout'` + * log: ['query', 'info', 'warn', 'error'] + * + * // Emit as events only + * log: [ + * { emit: 'event', level: 'query' }, + * { emit: 'event', level: 'info' }, + * { emit: 'event', level: 'warn' } + * { emit: 'event', level: 'error' } + * ] + * + * / Emit as events and log to stdout + * og: [ + * { emit: 'stdout', level: 'query' }, + * { emit: 'stdout', level: 'info' }, + * { emit: 'stdout', level: 'warn' } + * { emit: 'stdout', level: 'error' } + * + * ``` + * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/logging#the-log-option). + */ + log?: (LogLevel | LogDefinition)[] + /** + * The default values for transactionOptions + * maxWait ?= 2000 + * timeout ?= 5000 + */ + transactionOptions?: { + maxWait?: number + timeout?: number + isolationLevel?: TransactionIsolationLevel + } + /** + * Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-planetscale` + */ + adapter?: runtime.SqlDriverAdapterFactory | null + /** + * Global configuration for omitting model fields by default. + * + * @example + * ``` + * const prisma = new PrismaClient({ + * omit: { + * user: { + * password: true + * } + * } + * }) + * ``` + */ + omit?: GlobalOmitConfig +} +export type GlobalOmitConfig = { + user?: Prisma.UserOmit + profile?: Prisma.ProfileOmit +} + +/* Types for Logging */ +export type LogLevel = 'info' | 'query' | 'warn' | 'error' +export type LogDefinition = { + level: LogLevel + emit: 'stdout' | 'event' +} + +export type CheckIsLogLevel = T extends LogLevel ? T : never; + +export type GetLogType = CheckIsLogLevel< + T extends LogDefinition ? T['level'] : T +>; + +export type GetEvents = T extends Array + ? GetLogType + : never; + +export type QueryEvent = { + timestamp: Date + query: string + params: string + duration: number + target: string +} + +export type LogEvent = { + timestamp: Date + message: string + target: string +} +/* End Types for Logging */ + + +export type PrismaAction = + | 'findUnique' + | 'findUniqueOrThrow' + | 'findMany' + | 'findFirst' + | 'findFirstOrThrow' + | 'create' + | 'createMany' + | 'createManyAndReturn' + | 'update' + | 'updateMany' + | 'updateManyAndReturn' + | 'upsert' + | 'delete' + | 'deleteMany' + | 'executeRaw' + | 'queryRaw' + | 'aggregate' + | 'count' + | 'runCommandRaw' + | 'findRaw' + | 'groupBy' + +/** + * `PrismaClient` proxy available in interactive transactions. + */ +export type TransactionClient = Omit + diff --git a/public/generated/prisma/internal/prismaNamespaceBrowser.ts b/public/generated/prisma/internal/prismaNamespaceBrowser.ts new file mode 100644 index 00000000..966ae6bc --- /dev/null +++ b/public/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -0,0 +1,103 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file. + * While this enables partial backward compatibility, it is not part of the stable public API. + * + * If you are looking for your Models, Enums, and Input Types, please import them from the respective + * model files in the `model` directory! + */ + +import * as runtime from "@prisma/client/runtime/index-browser" + +export type * from '../models.ts' +export type * from './prismaNamespace.ts' + +export const Decimal = runtime.Decimal + + +export const NullTypes = { + DbNull: runtime.objectEnumValues.classes.DbNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.DbNull), + JsonNull: runtime.objectEnumValues.classes.JsonNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.JsonNull), + AnyNull: runtime.objectEnumValues.classes.AnyNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.AnyNull), +} +/** + * Helper for filtering JSON entries that have `null` on the database (empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const DbNull = runtime.objectEnumValues.instances.DbNull +/** + * Helper for filtering JSON entries that have JSON `null` values (not empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const JsonNull = runtime.objectEnumValues.instances.JsonNull +/** + * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull` + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const AnyNull = runtime.objectEnumValues.instances.AnyNull + + +export const ModelName = { + User: 'User', + Profile: 'Profile' +} as const + +export type ModelName = (typeof ModelName)[keyof typeof ModelName] + +/* + * Enums + */ + +export const TransactionIsolationLevel = runtime.makeStrictEnum({ + ReadUncommitted: 'ReadUncommitted', + ReadCommitted: 'ReadCommitted', + RepeatableRead: 'RepeatableRead', + Serializable: 'Serializable' +} as const) + +export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + + +export const UserScalarFieldEnum = { + id: 'id', + email: 'email', + name: 'name' +} as const + +export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum] + + +export const ProfileScalarFieldEnum = { + id: 'id', + bio: 'bio', + userId: 'userId' +} as const + +export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum] + + +export const SortOrder = { + asc: 'asc', + desc: 'desc' +} as const + +export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + + +export const QueryMode = { + default: 'default', + insensitive: 'insensitive' +} as const + +export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + diff --git a/public/generated/prisma/libquery_engine-darwin-arm64.dylib.node b/public/generated/prisma/libquery_engine-darwin-arm64.dylib.node new file mode 100755 index 00000000..eeba421c Binary files /dev/null and b/public/generated/prisma/libquery_engine-darwin-arm64.dylib.node differ diff --git a/public/generated/prisma/models.ts b/public/generated/prisma/models.ts new file mode 100644 index 00000000..91a3b15d --- /dev/null +++ b/public/generated/prisma/models.ts @@ -0,0 +1,12 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * This is a barrel export file for all models and their related types. + * + * 🟢 You can import this file directly. + */ +export type * from './models/User.ts' +export type * from './models/Profile.ts' +export type * from './commonInputTypes.ts' \ No newline at end of file diff --git a/public/generated/prisma/models/Profile.ts b/public/generated/prisma/models/Profile.ts new file mode 100644 index 00000000..8033aca8 --- /dev/null +++ b/public/generated/prisma/models/Profile.ts @@ -0,0 +1,1134 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * This file exports the `Profile` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/library" +import type * as $Enums from "../enums.ts" +import type * as Prisma from "../internal/prismaNamespace.ts" + +/** + * Model Profile + * + */ +export type ProfileModel = runtime.Types.Result.DefaultSelection + +export type AggregateProfile = { + _count: ProfileCountAggregateOutputType | null + _avg: ProfileAvgAggregateOutputType | null + _sum: ProfileSumAggregateOutputType | null + _min: ProfileMinAggregateOutputType | null + _max: ProfileMaxAggregateOutputType | null +} + +export type ProfileAvgAggregateOutputType = { + id: number | null + userId: number | null +} + +export type ProfileSumAggregateOutputType = { + id: number | null + userId: number | null +} + +export type ProfileMinAggregateOutputType = { + id: number | null + bio: string | null + userId: number | null +} + +export type ProfileMaxAggregateOutputType = { + id: number | null + bio: string | null + userId: number | null +} + +export type ProfileCountAggregateOutputType = { + id: number + bio: number + userId: number + _all: number +} + + +export type ProfileAvgAggregateInputType = { + id?: true + userId?: true +} + +export type ProfileSumAggregateInputType = { + id?: true + userId?: true +} + +export type ProfileMinAggregateInputType = { + id?: true + bio?: true + userId?: true +} + +export type ProfileMaxAggregateInputType = { + id?: true + bio?: true + userId?: true +} + +export type ProfileCountAggregateInputType = { + id?: true + bio?: true + userId?: true + _all?: true +} + +export type ProfileAggregateArgs = { + /** + * Filter which Profile to aggregate. + */ + where?: Prisma.ProfileWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Profiles to fetch. + */ + orderBy?: Prisma.ProfileOrderByWithRelationInput | Prisma.ProfileOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.ProfileWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Profiles from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Profiles. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Profiles + **/ + _count?: true | ProfileCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: ProfileAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: ProfileSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: ProfileMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: ProfileMaxAggregateInputType +} + +export type GetProfileAggregateType = { + [P in keyof T & keyof AggregateProfile]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type ProfileGroupByArgs = { + where?: Prisma.ProfileWhereInput + orderBy?: Prisma.ProfileOrderByWithAggregationInput | Prisma.ProfileOrderByWithAggregationInput[] + by: Prisma.ProfileScalarFieldEnum[] | Prisma.ProfileScalarFieldEnum + having?: Prisma.ProfileScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: ProfileCountAggregateInputType | true + _avg?: ProfileAvgAggregateInputType + _sum?: ProfileSumAggregateInputType + _min?: ProfileMinAggregateInputType + _max?: ProfileMaxAggregateInputType +} + +export type ProfileGroupByOutputType = { + id: number + bio: string + userId: number + _count: ProfileCountAggregateOutputType | null + _avg: ProfileAvgAggregateOutputType | null + _sum: ProfileSumAggregateOutputType | null + _min: ProfileMinAggregateOutputType | null + _max: ProfileMaxAggregateOutputType | null +} + +type GetProfileGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof ProfileGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type ProfileWhereInput = { + AND?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[] + OR?: Prisma.ProfileWhereInput[] + NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[] + id?: Prisma.IntFilter<"Profile"> | number + bio?: Prisma.StringFilter<"Profile"> | string + userId?: Prisma.IntFilter<"Profile"> | number +} + +export type ProfileOrderByWithRelationInput = { + id?: Prisma.SortOrder + bio?: Prisma.SortOrder + userId?: Prisma.SortOrder +} + +export type ProfileWhereUniqueInput = Prisma.AtLeast<{ + id?: number + userId?: number + AND?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[] + OR?: Prisma.ProfileWhereInput[] + NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[] + bio?: Prisma.StringFilter<"Profile"> | string +}, "id" | "userId"> + +export type ProfileOrderByWithAggregationInput = { + id?: Prisma.SortOrder + bio?: Prisma.SortOrder + userId?: Prisma.SortOrder + _count?: Prisma.ProfileCountOrderByAggregateInput + _avg?: Prisma.ProfileAvgOrderByAggregateInput + _max?: Prisma.ProfileMaxOrderByAggregateInput + _min?: Prisma.ProfileMinOrderByAggregateInput + _sum?: Prisma.ProfileSumOrderByAggregateInput +} + +export type ProfileScalarWhereWithAggregatesInput = { + AND?: Prisma.ProfileScalarWhereWithAggregatesInput | Prisma.ProfileScalarWhereWithAggregatesInput[] + OR?: Prisma.ProfileScalarWhereWithAggregatesInput[] + NOT?: Prisma.ProfileScalarWhereWithAggregatesInput | Prisma.ProfileScalarWhereWithAggregatesInput[] + id?: Prisma.IntWithAggregatesFilter<"Profile"> | number + bio?: Prisma.StringWithAggregatesFilter<"Profile"> | string + userId?: Prisma.IntWithAggregatesFilter<"Profile"> | number +} + +export type ProfileCreateInput = { + bio: string + userId: number +} + +export type ProfileUncheckedCreateInput = { + id?: number + bio: string + userId: number +} + +export type ProfileUpdateInput = { + bio?: Prisma.StringFieldUpdateOperationsInput | string + userId?: Prisma.IntFieldUpdateOperationsInput | number +} + +export type ProfileUncheckedUpdateInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + bio?: Prisma.StringFieldUpdateOperationsInput | string + userId?: Prisma.IntFieldUpdateOperationsInput | number +} + +export type ProfileCreateManyInput = { + id?: number + bio: string + userId: number +} + +export type ProfileUpdateManyMutationInput = { + bio?: Prisma.StringFieldUpdateOperationsInput | string + userId?: Prisma.IntFieldUpdateOperationsInput | number +} + +export type ProfileUncheckedUpdateManyInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + bio?: Prisma.StringFieldUpdateOperationsInput | string + userId?: Prisma.IntFieldUpdateOperationsInput | number +} + +export type ProfileCountOrderByAggregateInput = { + id?: Prisma.SortOrder + bio?: Prisma.SortOrder + userId?: Prisma.SortOrder +} + +export type ProfileAvgOrderByAggregateInput = { + id?: Prisma.SortOrder + userId?: Prisma.SortOrder +} + +export type ProfileMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + bio?: Prisma.SortOrder + userId?: Prisma.SortOrder +} + +export type ProfileMinOrderByAggregateInput = { + id?: Prisma.SortOrder + bio?: Prisma.SortOrder + userId?: Prisma.SortOrder +} + +export type ProfileSumOrderByAggregateInput = { + id?: Prisma.SortOrder + userId?: Prisma.SortOrder +} + + + +export type ProfileSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + bio?: boolean + userId?: boolean +}, ExtArgs["result"]["profile"]> + +export type ProfileSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + bio?: boolean + userId?: boolean +}, ExtArgs["result"]["profile"]> + +export type ProfileSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + bio?: boolean + userId?: boolean +}, ExtArgs["result"]["profile"]> + +export type ProfileSelectScalar = { + id?: boolean + bio?: boolean + userId?: boolean +} + +export type ProfileOmit = runtime.Types.Extensions.GetOmit<"id" | "bio" | "userId", ExtArgs["result"]["profile"]> + +export type $ProfilePayload = { + name: "Profile" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: number + bio: string + userId: number + }, ExtArgs["result"]["profile"]> + composites: {} +} + +export type ProfileGetPayload = runtime.Types.Result.GetResult + +export type ProfileCountArgs = + Omit & { + select?: ProfileCountAggregateInputType | true + } + +export interface ProfileDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['Profile'], meta: { name: 'Profile' } } + /** + * Find zero or one Profile that matches the filter. + * @param {ProfileFindUniqueArgs} args - Arguments to find a Profile + * @example + * // Get one Profile + * const profile = await prisma.profile.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one Profile that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {ProfileFindUniqueOrThrowArgs} args - Arguments to find a Profile + * @example + * // Get one Profile + * const profile = await prisma.profile.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Profile that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ProfileFindFirstArgs} args - Arguments to find a Profile + * @example + * // Get one Profile + * const profile = await prisma.profile.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Profile that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ProfileFindFirstOrThrowArgs} args - Arguments to find a Profile + * @example + * // Get one Profile + * const profile = await prisma.profile.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Profiles that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ProfileFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Profiles + * const profiles = await prisma.profile.findMany() + * + * // Get first 10 Profiles + * const profiles = await prisma.profile.findMany({ take: 10 }) + * + * // Only select the `id` + * const profileWithIdOnly = await prisma.profile.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a Profile. + * @param {ProfileCreateArgs} args - Arguments to create a Profile. + * @example + * // Create one Profile + * const Profile = await prisma.profile.create({ + * data: { + * // ... data to create a Profile + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Profiles. + * @param {ProfileCreateManyArgs} args - Arguments to create many Profiles. + * @example + * // Create many Profiles + * const profile = await prisma.profile.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Profiles and returns the data saved in the database. + * @param {ProfileCreateManyAndReturnArgs} args - Arguments to create many Profiles. + * @example + * // Create many Profiles + * const profile = await prisma.profile.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Profiles and only return the `id` + * const profileWithIdOnly = await prisma.profile.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a Profile. + * @param {ProfileDeleteArgs} args - Arguments to delete one Profile. + * @example + * // Delete one Profile + * const Profile = await prisma.profile.delete({ + * where: { + * // ... filter to delete one Profile + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one Profile. + * @param {ProfileUpdateArgs} args - Arguments to update one Profile. + * @example + * // Update one Profile + * const profile = await prisma.profile.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Profiles. + * @param {ProfileDeleteManyArgs} args - Arguments to filter Profiles to delete. + * @example + * // Delete a few Profiles + * const { count } = await prisma.profile.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Profiles. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ProfileUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Profiles + * const profile = await prisma.profile.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Profiles and returns the data updated in the database. + * @param {ProfileUpdateManyAndReturnArgs} args - Arguments to update many Profiles. + * @example + * // Update many Profiles + * const profile = await prisma.profile.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Profiles and only return the `id` + * const profileWithIdOnly = await prisma.profile.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one Profile. + * @param {ProfileUpsertArgs} args - Arguments to update or create a Profile. + * @example + * // Update or create a Profile + * const profile = await prisma.profile.upsert({ + * create: { + * // ... data to create a Profile + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the Profile we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__ProfileClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Profiles. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ProfileCountArgs} args - Arguments to filter Profiles to count. + * @example + * // Count the number of Profiles + * const count = await prisma.profile.count({ + * where: { + * // ... the filter for the Profiles we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a Profile. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ProfileAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by Profile. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ProfileGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends ProfileGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: ProfileGroupByArgs['orderBy'] } + : { orderBy?: ProfileGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetProfileGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the Profile model + */ +readonly fields: ProfileFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for Profile. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__ProfileClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the Profile model + */ +export interface ProfileFieldRefs { + readonly id: Prisma.FieldRef<"Profile", 'Int'> + readonly bio: Prisma.FieldRef<"Profile", 'String'> + readonly userId: Prisma.FieldRef<"Profile", 'Int'> +} + + +// Custom InputTypes +/** + * Profile findUnique + */ +export type ProfileFindUniqueArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * Filter, which Profile to fetch. + */ + where: Prisma.ProfileWhereUniqueInput +} + +/** + * Profile findUniqueOrThrow + */ +export type ProfileFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * Filter, which Profile to fetch. + */ + where: Prisma.ProfileWhereUniqueInput +} + +/** + * Profile findFirst + */ +export type ProfileFindFirstArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * Filter, which Profile to fetch. + */ + where?: Prisma.ProfileWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Profiles to fetch. + */ + orderBy?: Prisma.ProfileOrderByWithRelationInput | Prisma.ProfileOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Profiles. + */ + cursor?: Prisma.ProfileWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Profiles from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Profiles. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Profiles. + */ + distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[] +} + +/** + * Profile findFirstOrThrow + */ +export type ProfileFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * Filter, which Profile to fetch. + */ + where?: Prisma.ProfileWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Profiles to fetch. + */ + orderBy?: Prisma.ProfileOrderByWithRelationInput | Prisma.ProfileOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Profiles. + */ + cursor?: Prisma.ProfileWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Profiles from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Profiles. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Profiles. + */ + distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[] +} + +/** + * Profile findMany + */ +export type ProfileFindManyArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * Filter, which Profiles to fetch. + */ + where?: Prisma.ProfileWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Profiles to fetch. + */ + orderBy?: Prisma.ProfileOrderByWithRelationInput | Prisma.ProfileOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Profiles. + */ + cursor?: Prisma.ProfileWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Profiles from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Profiles. + */ + skip?: number + distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[] +} + +/** + * Profile create + */ +export type ProfileCreateArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * The data needed to create a Profile. + */ + data: Prisma.XOR +} + +/** + * Profile createMany + */ +export type ProfileCreateManyArgs = { + /** + * The data used to create many Profiles. + */ + data: Prisma.ProfileCreateManyInput | Prisma.ProfileCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Profile createManyAndReturn + */ +export type ProfileCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelectCreateManyAndReturn | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * The data used to create many Profiles. + */ + data: Prisma.ProfileCreateManyInput | Prisma.ProfileCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Profile update + */ +export type ProfileUpdateArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * The data needed to update a Profile. + */ + data: Prisma.XOR + /** + * Choose, which Profile to update. + */ + where: Prisma.ProfileWhereUniqueInput +} + +/** + * Profile updateMany + */ +export type ProfileUpdateManyArgs = { + /** + * The data used to update Profiles. + */ + data: Prisma.XOR + /** + * Filter which Profiles to update + */ + where?: Prisma.ProfileWhereInput + /** + * Limit how many Profiles to update. + */ + limit?: number +} + +/** + * Profile updateManyAndReturn + */ +export type ProfileUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * The data used to update Profiles. + */ + data: Prisma.XOR + /** + * Filter which Profiles to update + */ + where?: Prisma.ProfileWhereInput + /** + * Limit how many Profiles to update. + */ + limit?: number +} + +/** + * Profile upsert + */ +export type ProfileUpsertArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * The filter to search for the Profile to update in case it exists. + */ + where: Prisma.ProfileWhereUniqueInput + /** + * In case the Profile found by the `where` argument doesn't exist, create a new Profile with this data. + */ + create: Prisma.XOR + /** + * In case the Profile was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * Profile delete + */ +export type ProfileDeleteArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null + /** + * Filter which Profile to delete. + */ + where: Prisma.ProfileWhereUniqueInput +} + +/** + * Profile deleteMany + */ +export type ProfileDeleteManyArgs = { + /** + * Filter which Profiles to delete + */ + where?: Prisma.ProfileWhereInput + /** + * Limit how many Profiles to delete. + */ + limit?: number +} + +/** + * Profile without action + */ +export type ProfileDefaultArgs = { + /** + * Select specific fields to fetch from the Profile + */ + select?: Prisma.ProfileSelect | null + /** + * Omit specific fields from the Profile + */ + omit?: Prisma.ProfileOmit | null +} diff --git a/public/generated/prisma/models/User.ts b/public/generated/prisma/models/User.ts new file mode 100644 index 00000000..fdab78b6 --- /dev/null +++ b/public/generated/prisma/models/User.ts @@ -0,0 +1,1140 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// @ts-nocheck +/* + * This file exports the `User` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/library" +import type * as $Enums from "../enums.ts" +import type * as Prisma from "../internal/prismaNamespace.ts" + +/** + * Model User + * + */ +export type UserModel = runtime.Types.Result.DefaultSelection + +export type AggregateUser = { + _count: UserCountAggregateOutputType | null + _avg: UserAvgAggregateOutputType | null + _sum: UserSumAggregateOutputType | null + _min: UserMinAggregateOutputType | null + _max: UserMaxAggregateOutputType | null +} + +export type UserAvgAggregateOutputType = { + id: number | null +} + +export type UserSumAggregateOutputType = { + id: number | null +} + +export type UserMinAggregateOutputType = { + id: number | null + email: string | null + name: string | null +} + +export type UserMaxAggregateOutputType = { + id: number | null + email: string | null + name: string | null +} + +export type UserCountAggregateOutputType = { + id: number + email: number + name: number + _all: number +} + + +export type UserAvgAggregateInputType = { + id?: true +} + +export type UserSumAggregateInputType = { + id?: true +} + +export type UserMinAggregateInputType = { + id?: true + email?: true + name?: true +} + +export type UserMaxAggregateInputType = { + id?: true + email?: true + name?: true +} + +export type UserCountAggregateInputType = { + id?: true + email?: true + name?: true + _all?: true +} + +export type UserAggregateArgs = { + /** + * Filter which User to aggregate. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Users + **/ + _count?: true | UserCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: UserAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: UserSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: UserMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: UserMaxAggregateInputType +} + +export type GetUserAggregateType = { + [P in keyof T & keyof AggregateUser]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type UserGroupByArgs = { + where?: Prisma.UserWhereInput + orderBy?: Prisma.UserOrderByWithAggregationInput | Prisma.UserOrderByWithAggregationInput[] + by: Prisma.UserScalarFieldEnum[] | Prisma.UserScalarFieldEnum + having?: Prisma.UserScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: UserCountAggregateInputType | true + _avg?: UserAvgAggregateInputType + _sum?: UserSumAggregateInputType + _min?: UserMinAggregateInputType + _max?: UserMaxAggregateInputType +} + +export type UserGroupByOutputType = { + id: number + email: string + name: string + _count: UserCountAggregateOutputType | null + _avg: UserAvgAggregateOutputType | null + _sum: UserSumAggregateOutputType | null + _min: UserMinAggregateOutputType | null + _max: UserMaxAggregateOutputType | null +} + +type GetUserGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof UserGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type UserWhereInput = { + AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + OR?: Prisma.UserWhereInput[] + NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + id?: Prisma.IntFilter<"User"> | number + email?: Prisma.StringFilter<"User"> | string + name?: Prisma.StringFilter<"User"> | string +} + +export type UserOrderByWithRelationInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder +} + +export type UserWhereUniqueInput = Prisma.AtLeast<{ + id?: number + email?: string + AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + OR?: Prisma.UserWhereInput[] + NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + name?: Prisma.StringFilter<"User"> | string +}, "id" | "email"> + +export type UserOrderByWithAggregationInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder + _count?: Prisma.UserCountOrderByAggregateInput + _avg?: Prisma.UserAvgOrderByAggregateInput + _max?: Prisma.UserMaxOrderByAggregateInput + _min?: Prisma.UserMinOrderByAggregateInput + _sum?: Prisma.UserSumOrderByAggregateInput +} + +export type UserScalarWhereWithAggregatesInput = { + AND?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[] + OR?: Prisma.UserScalarWhereWithAggregatesInput[] + NOT?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[] + id?: Prisma.IntWithAggregatesFilter<"User"> | number + email?: Prisma.StringWithAggregatesFilter<"User"> | string + name?: Prisma.StringWithAggregatesFilter<"User"> | string +} + +export type UserCreateInput = { + email: string + name: string +} + +export type UserUncheckedCreateInput = { + id?: number + email: string + name: string +} + +export type UserUpdateInput = { + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type UserUncheckedUpdateInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type UserCreateManyInput = { + id?: number + email: string + name: string +} + +export type UserUpdateManyMutationInput = { + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type UserUncheckedUpdateManyInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type UserCountOrderByAggregateInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder +} + +export type UserAvgOrderByAggregateInput = { + id?: Prisma.SortOrder +} + +export type UserMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder +} + +export type UserMinOrderByAggregateInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder +} + +export type UserSumOrderByAggregateInput = { + id?: Prisma.SortOrder +} + +export type StringFieldUpdateOperationsInput = { + set?: string +} + +export type IntFieldUpdateOperationsInput = { + set?: number + increment?: number + decrement?: number + multiply?: number + divide?: number +} + + + +export type UserSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + email?: boolean + name?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + email?: boolean + name?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + email?: boolean + name?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectScalar = { + id?: boolean + email?: boolean + name?: boolean +} + +export type UserOmit = runtime.Types.Extensions.GetOmit<"id" | "email" | "name", ExtArgs["result"]["user"]> + +export type $UserPayload = { + name: "User" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: number + email: string + name: string + }, ExtArgs["result"]["user"]> + composites: {} +} + +export type UserGetPayload = runtime.Types.Result.GetResult + +export type UserCountArgs = + Omit & { + select?: UserCountAggregateInputType | true + } + +export interface UserDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['User'], meta: { name: 'User' } } + /** + * Find zero or one User that matches the filter. + * @param {UserFindUniqueArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one User that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {UserFindUniqueOrThrowArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first User that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindFirstArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first User that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindFirstOrThrowArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Users that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Users + * const users = await prisma.user.findMany() + * + * // Get first 10 Users + * const users = await prisma.user.findMany({ take: 10 }) + * + * // Only select the `id` + * const userWithIdOnly = await prisma.user.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a User. + * @param {UserCreateArgs} args - Arguments to create a User. + * @example + * // Create one User + * const User = await prisma.user.create({ + * data: { + * // ... data to create a User + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Users. + * @param {UserCreateManyArgs} args - Arguments to create many Users. + * @example + * // Create many Users + * const user = await prisma.user.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Users and returns the data saved in the database. + * @param {UserCreateManyAndReturnArgs} args - Arguments to create many Users. + * @example + * // Create many Users + * const user = await prisma.user.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Users and only return the `id` + * const userWithIdOnly = await prisma.user.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a User. + * @param {UserDeleteArgs} args - Arguments to delete one User. + * @example + * // Delete one User + * const User = await prisma.user.delete({ + * where: { + * // ... filter to delete one User + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one User. + * @param {UserUpdateArgs} args - Arguments to update one User. + * @example + * // Update one User + * const user = await prisma.user.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Users. + * @param {UserDeleteManyArgs} args - Arguments to filter Users to delete. + * @example + * // Delete a few Users + * const { count } = await prisma.user.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Users. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Users + * const user = await prisma.user.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Users and returns the data updated in the database. + * @param {UserUpdateManyAndReturnArgs} args - Arguments to update many Users. + * @example + * // Update many Users + * const user = await prisma.user.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Users and only return the `id` + * const userWithIdOnly = await prisma.user.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one User. + * @param {UserUpsertArgs} args - Arguments to update or create a User. + * @example + * // Update or create a User + * const user = await prisma.user.upsert({ + * create: { + * // ... data to create a User + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the User we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Users. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserCountArgs} args - Arguments to filter Users to count. + * @example + * // Count the number of Users + * const count = await prisma.user.count({ + * where: { + * // ... the filter for the Users we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a User. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by User. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends UserGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: UserGroupByArgs['orderBy'] } + : { orderBy?: UserGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetUserGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the User model + */ +readonly fields: UserFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for User. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__UserClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the User model + */ +export interface UserFieldRefs { + readonly id: Prisma.FieldRef<"User", 'Int'> + readonly email: Prisma.FieldRef<"User", 'String'> + readonly name: Prisma.FieldRef<"User", 'String'> +} + + +// Custom InputTypes +/** + * User findUnique + */ +export type UserFindUniqueArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User findUniqueOrThrow + */ +export type UserFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User findFirst + */ +export type UserFindFirstArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Users. + */ + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User findFirstOrThrow + */ +export type UserFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Users. + */ + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User findMany + */ +export type UserFindManyArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which Users to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User create + */ +export type UserCreateArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data needed to create a User. + */ + data: Prisma.XOR +} + +/** + * User createMany + */ +export type UserCreateManyArgs = { + /** + * The data used to create many Users. + */ + data: Prisma.UserCreateManyInput | Prisma.UserCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * User createManyAndReturn + */ +export type UserCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelectCreateManyAndReturn | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data used to create many Users. + */ + data: Prisma.UserCreateManyInput | Prisma.UserCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * User update + */ +export type UserUpdateArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data needed to update a User. + */ + data: Prisma.XOR + /** + * Choose, which User to update. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User updateMany + */ +export type UserUpdateManyArgs = { + /** + * The data used to update Users. + */ + data: Prisma.XOR + /** + * Filter which Users to update + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to update. + */ + limit?: number +} + +/** + * User updateManyAndReturn + */ +export type UserUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data used to update Users. + */ + data: Prisma.XOR + /** + * Filter which Users to update + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to update. + */ + limit?: number +} + +/** + * User upsert + */ +export type UserUpsertArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The filter to search for the User to update in case it exists. + */ + where: Prisma.UserWhereUniqueInput + /** + * In case the User found by the `where` argument doesn't exist, create a new User with this data. + */ + create: Prisma.XOR + /** + * In case the User was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * User delete + */ +export type UserDeleteArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter which User to delete. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User deleteMany + */ +export type UserDeleteManyArgs = { + /** + * Filter which Users to delete + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to delete. + */ + limit?: number +} + +/** + * User without action + */ +export type UserDefaultArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null +} diff --git a/src/App.jsx b/src/App.jsx index ce4710eb..93829dd5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,29 +1,43 @@ -import "./App.css"; import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { AuthProvider } from "@context/AuthContext"; import { Routes, Route } from "react-router-dom"; -import Layout from "@pages/Layout"; -import LoginPage from "@pages/LoginPage"; -import MainPage from "@pages/MainPage"; -import DetailPage from "@pages/DetailPage"; -import SearchPage from "@pages/SearchPage"; -import SignupPage from "@pages/SignupPage"; +import { + Layout, + LoginPage, + MainPage, + DetailPage, + SearchPage, + SignupPage, +} from "@/pages"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + staleTime: 100 * 60 * 5, + }, + }, +}); function App() { return ( - - - - }> - } /> - } /> - } /> - } /> - } /> - - - - + + + + + }> + } /> + } /> + } /> + } /> + } /> + + + + + ); } diff --git a/src/api/api.jsx b/src/api/api.jsx index bc5cefd2..80b6c9ca 100644 --- a/src/api/api.jsx +++ b/src/api/api.jsx @@ -14,18 +14,4 @@ const instance = axios.create({ language: "ko-KR", }, }); - -// fetchData -// api data 불러오기 (tmdb data 전부) - -export const fetchData = async (endpoint, extraParams = {}) => { - try { - const response = await instance.get(endpoint, { params: extraParams }); - return response.data; - } catch (error) { - console.error("API 요청 중 오류 발생:", error); - throw error; - } -}; - export default instance; diff --git a/src/api/fetch.jsx b/src/api/fetch.jsx new file mode 100644 index 00000000..455f6ab0 --- /dev/null +++ b/src/api/fetch.jsx @@ -0,0 +1,74 @@ +// fetchData +// api data 불러오기 (tmdb data 전부) + +import instance from "./api"; + +export const fetchData = async (endpoint, extraParams = {}) => { + try { + const response = await instance.get(endpoint, { params: extraParams }); + return response.data; + } catch (error) { + console.error("API 요청 중 오류 발생:", error); + throw error; + } +}; + +// 무한스크롤 - TopRated Movie +export const fetchTopRagedMovies = async ({ pageParam = 1 }) => { + try { + const response = await instance.get(`/movie/top_rated`, { + params: { page: pageParam }, + }); + + const data = response.data; + + return { + results: data.results, + nextPage: data.page + 1, + totalPages: data.total_pages, + }; + } catch (error) { + console.error("Top Raged 요청 중 오류발생", error); + throw error; + } +}; + +// 무한스크롤 - Similar Movies +export const fetchSimilarMovies = async ({ movieId, pageParam }) => { + try { + const response = await instance.get(`/movie/${movieId}/similar`, { + params: { page: pageParam }, + }); + + const data = response.data; + + return { + results: data.results, + nextPage: data.page + 1, + totalPages: data.total_pages, + }; + } catch (error) { + console.error("Similar 영화 요청 중 오류 발생", error); + throw error; + } +}; + +// 무한스크롤 - Popular Movie +export const fetchPopularMovies = async ({ pageParam = 1 }) => { + try { + const response = await instance.get("/movie/popular", { + params: { page: pageParam }, + }); + + const data = response.data; + + return { + results: data.results, + nextPage: data.page + 1, + totalPages: data.total_pages, + }; + } catch (error) { + console.error("Similar 영화 요청 중 오류 발생", error); + throw error; + } +}; diff --git a/src/api/index.jsx b/src/api/index.jsx new file mode 100644 index 00000000..6767bb81 --- /dev/null +++ b/src/api/index.jsx @@ -0,0 +1,8 @@ +export { default as axiosInstance } from "./api"; +export { + fetchData, + fetchTopRagedMovies, + fetchSimilarMovies, + fetchPopularMovies, +} from "./fetch"; +export { supabase } from "./supabase"; diff --git a/src/api/supabase.jsx b/src/api/supabase.jsx index 3e06b2fc..05f30876 100644 --- a/src/api/supabase.jsx +++ b/src/api/supabase.jsx @@ -3,4 +3,19 @@ import { createClient } from "@supabase/supabase-js"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; -export const supabase = createClient(supabaseUrl, supabaseAnonKey); +export const supabase = createClient(supabaseUrl, supabaseAnonKey, { + db: { + schema: "public", + }, + auth: { + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true, + }, + global: { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + }, +}); diff --git a/public/image/keynema_logo.png b/src/assets/image/keynema_logo.png similarity index 100% rename from public/image/keynema_logo.png rename to src/assets/image/keynema_logo.png diff --git a/src/components/Banner/Banner.jsx b/src/components/Banner/Banner.jsx deleted file mode 100644 index d4f1ac77..00000000 --- a/src/components/Banner/Banner.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Swiper, SwiperSlide } from "swiper/react"; -import { Navigation, Pagination } from "swiper/modules"; -import "swiper/css"; -import "./BannerSwiper.css"; -import "swiper/css/navigation"; -import "swiper/css/pagination"; -import { Container, Wrap, Overlay, Title, Description } from "./BannerStyle"; -import { useNavigate } from "react-router-dom"; -import { useFetchData } from "@hook/useFetchData"; - -const Banner = () => { - const { data, loading, error } = useFetchData("/movie/now_playing"); - const [swiperHeight, setSwiperHeight] = useState("644px"); - const navigate = useNavigate(); - - useEffect(() => { - const handleResize = () => { - const width = window.innerWidth; - if (width <= 768) { - setSwiperHeight("344px"); // 모바일 - } else if (width <= 960) { - setSwiperHeight("444px"); // 태블릿 작은 사이즈 - } else if (width <= 1240) { - setSwiperHeight("544px"); // 태블릿 큰 사이즈 - } else { - setSwiperHeight("644px"); // 데스크톱 - } - }; - handleResize(); //초기실행 - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }); - - if (loading) return Loading...; - if (error) return Error : {error.message}; - if (!data?.results) return null; - - const movies = data.results; - - //100자 초과 시 '...'으로 표시되게 - const truncate = (str, n) => { - return str?.length > n ? str.substring(0, n) + "..." : str; - }; - - return ( - - = 3} - navigation={true} - pagination={{ - clickable: true, - dynamicBullets: true, - }} - breakpoints={{ - 1240: { slidesPerView: 3 }, - 960: { slidesPerView: 2 }, - 768: { slidesPerView: 1.6 }, - 0: { slidesPerView: 1.6 }, - }} - > - {movies.map((movie) => ( - - navigate(`/movie/${movie.id}`)}> - {movie.title - - - {movie.title} - {truncate(movie.overview, 20)} - - - ))} - ; - - - ); -}; - -export default Banner; diff --git a/src/components/Banner/BannerStyle.jsx b/src/components/Banner/BannerStyle.jsx deleted file mode 100644 index 39e70c84..00000000 --- a/src/components/Banner/BannerStyle.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// BannerStyle.js -import styled from "styled-components"; - -export const Container = styled.section` - margin-top: 0px; - margin-bottom: 88px; - - @media (max-width: 1240px) { - margin-top: 100px; - } - - @media (max-width: 9860px) { - margin-top: 140px; - } - - @media (max-width: 768px) { - margin-top: 200px; - } -`; - -export const Wrap = styled.div` - width: 100%; - height: 100%; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } -`; - -export const Overlay = styled.div` - position: absolute; - bottom: 76px; - left: 40px; - right: 20px; - color: #fff; - z-index: 5; /* 이미지 위에 표시 */ - display: flex; - flex-direction: column; - gap: 8px; - text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7); - - @media (max-width: 960) { - bottom: 20px; - left: 16px; - right: 16px; - text-shadow: none; - } -`; - -export const Title = styled.h2` - font-size: 48px; - font-weight: 700; - margin: 0; - - @media (max-width: 768px) { - font-size: 24px; - } -`; - -export const Description = styled.p` - font-size: 20px; - margin: 0; - line-height: 1.3; - @media (max-width: 768px) { - font-size: 14px; - } -`; diff --git a/src/components/Banner/BannerSwiper.css b/src/components/Banner/BannerSwiper.css deleted file mode 100644 index d53c4cca..00000000 --- a/src/components/Banner/BannerSwiper.css +++ /dev/null @@ -1,144 +0,0 @@ -.banner-container .swiper { - position: relative; - z-index: 1; - width: 100%; - - @media (max-width: 1240px) { - .banner-container .swiper { - height: 544px !important; - } - } - - @media (max-width: 960px) { - .banner-container .swiper { - padding-top: 88; - } - } - - @media (max-width: 768px) { - .banner-container .swiper { - padding-top: 100px !important; - } - } -} - -.banner-container .swiper-slide { - width: 300%; - height: 100%; - border-radius: 20px; - transform: scale(0.85); - opacity: 0.5; - - display: flex; - justify-content: center; - align-items: center; - - overflow: hidden; - border-radius: 20px; - background: #000; - - transition: transform 0.3s ease, opacity 0.3s ease; - - img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - cursor: pointer; - } - /* 밑에서 위로 fade되는 그라디언트 */ - &::after { - content: ""; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 50%; - background: linear-gradient(to top, rgba(0, 0, 0, 0.63), transparent); - pointer-events: none; - } -} - -@media (max-width: 960px) { - .banner-container .swiper-slide { - transform: scale(1); - opacity: 1; - box-shadow: none; - } - - .banner-container .swiper-slide-active { - box-shadow: none; - } - - .banner-container .swiper-slide:not(.swiper-slide-active) img { - filter: none; - transform: scale(1); - } -} - -.banner-container .swiper-slide-active { - transform: scale(1); - opacity: 1; - box-shadow: 0px 0px 45.34px rgba(0, 0, 0, 0.69); -} - -/* 블러 효과 양 옆 슬라이드 */ -.banner-container .swiper-slide:not(.swiper-slide-active) img { - filter: blur(10px); - transform: scale(0.95); - transition: filter 0.3s ease, transform 0.3s ease; -} - -/* 네비게이션 버튼 */ -.banner-container .swiper-button-prev, -.banner-container .swiper-button-next { - width: 80px !important; - height: 80px !important; - top: 50% !important; - transform: translateY(-50%) !important; - color: white !important; -} - -.banner-container .swiper-button-next { - right: 152px !important; -} - -.banner-container .swiper-button-prev { - left: 152px !important; -} - -@media (max-width: 960px) { - .banner-container .swiper-button-prev, - .banner-container .swiper-button-next { - display: none !important; - } -} - -/* 페이지네이션 */ -.banner-container.swiper-pagination { - position: absolute !important; - bottom: 0px !important; - left: 0; - width: 100%; - text-align: center; - z-index: 20 !important; -} - -.banner-container .swiper-pagination-bullet { - background: #d9d9d9 !important; - width: 14px; - height: 14px; - border-radius: 50%; -} - -.banner-container .swiper-pagination-bullet-active { - background: #ff1a86 !important; -} - -@media (max-width: 960px) { - .banner-container .swiper-pagination { - display: none !important; - } -} diff --git a/src/components/Banner/bannerConfig.js b/src/components/Banner/bannerConfig.js new file mode 100644 index 00000000..e656f9c5 --- /dev/null +++ b/src/components/Banner/bannerConfig.js @@ -0,0 +1,36 @@ +export const SWIPER_CONFIG = { + spaceBetween: 40, + slidesPerView: 1.6, + centeredSlides: true, + navigation: true, + pagination: { + clickable: true, + dynamicBullets: true, + dynamicMainBullets: 3, + }, + autoplay: { + delay: 5000, + disableOnInteraction: false, + }, + loop: true, + effect: "slide", + speed: 800, + breakpoints: { + 1240: { + slidesPerView: 1.6, + spaceBetween: 40, + }, + 960: { + slidesPerView: 1.2, + spaceBetween: 16, + }, + 768: { + slidesPerView: 1.2, + spaceBetween: 12, + }, + 0: { + slidesPerView: 1.2, + spaceBetween: 4, + }, + }, +}; diff --git a/src/components/Banner/index.jsx b/src/components/Banner/index.jsx new file mode 100644 index 00000000..8c022182 --- /dev/null +++ b/src/components/Banner/index.jsx @@ -0,0 +1,58 @@ +import { Swiper, SwiperSlide } from "swiper/react"; +import { useNavigate } from "react-router-dom"; +import { Navigation, Pagination, Autoplay } from "swiper/modules"; +import { useFetchData } from "@/hooks"; +import { SWIPER_CONFIG } from "./bannerConfig"; +import { truncateText } from "@/utils/text"; +import { getImageUrl } from "@/constants/images"; +import { Typography } from "@/components"; + +import "swiper/css"; +import "swiper/css/navigation"; +import "swiper/css/pagination"; + +import { BannerContainer, SlideContent, ImgWrapper, Overlay } from "./style"; + +const Banner = () => { + const { data, loading, error } = useFetchData("/movie/now_playing"); + const navigate = useNavigate(); + + const movies = data?.results || []; + + if (loading) return

; + if (error) return
Error : {error.message}
; + if (movies.length === 0) return null; + + return ( + + + {movies.map((movie) => ( + + {({ isActive }) => ( + + navigate(`/movie/${movie.id}`)}> + {movie.title} + + {isActive && ( + + + {movie.title} + + + {truncateText(movie.overview, 20)} + + + )} + + )} + + ))} + + + ); +}; + +export default Banner; diff --git a/src/components/Banner/style.jsx b/src/components/Banner/style.jsx new file mode 100644 index 00000000..63d0e0be --- /dev/null +++ b/src/components/Banner/style.jsx @@ -0,0 +1,206 @@ +import styled from "@emotion/styled"; + +export const BannerContainer = styled.section` + position: relative; + margin-top: 100px; + margin-bottom: 40px; + width: 100%; + height: auto; + overflow: hidden; + + .swiper { + width: 100%; + height: 644px; + overflow: hidden; + } + + .swiper-wrapper { + align-items: center; + } + + .swiper-slide { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } + + /* 네비게이션 버튼 */ + .swiper-button-next, + .swiper-button-prev { + color: #fff; + width: 80px; + height: 80px; + border-radius: 50%; + + &::after { + font-size: 24px; + font-weight: bold; + } + } + + .swiper-button-prev { + left: 100px; + } + + .swiper-button-next { + right: 100px; + } + + /* 페이지네이션 */ + .swiper-pagination-bullet { + background: #fff; + width: 10px; + height: 10px; + opacity: 0.5; + } + + .swiper-pagination-bullet-active { + opacity: 1; + background: #ff1a66; + width: 12px; + height: 12px; + } + + @media (max-width: 1240px) { + margin-top: 100px; + } + + @media (max-width: 960px) { + margin-top: 140px; + + .swiper { + height: 450px; + } + + .swiper-button-next, + .swiper-button-prev { + display: none; + } + } + + @media (max-width: 768px) { + margin-top: 200px; + margin-bottom: 50px; + + .swiper { + height: 350px; + } + + .swiper-pagination-bullet { + width: 8px; + height: 8px; + } + + .swiper-pagination-bullet-active { + width: 10px; + height: 10px; + } + } +`; + +export const SlideContent = styled.div` + position: relative; + width: ${(props) => (props.isActive ? "1254px" : "1024px")}; + max-width: ${(props) => (props.isActive ? "1254px" : "1024px")}; + height: 605px; + border-radius: 16px; + overflow: hidden; + transition: all 0.5s ease; + opacity: ${(props) => (props.isActive ? "1" : "0.4")}; + filter: ${(props) => (props.isActive ? "blur(0)" : "blur(8px)")}; + transform: ${(props) => (props.isActive ? "scale(1)" : "scale(0.9)")}; + + @media (max-width: 1240px) { + width: ${(props) => (props.isActive ? "95%" : "85%")}; + max-width: ${(props) => (props.isActive ? "95%" : "85%")}; + } + + @media (max-width: 960px) { + width: 100%; + max-width: 100%; + height: 450px; + opacity: ${(props) => (props.isActive ? "1" : "0.6")}; + filter: ${(props) => (props.isActive ? "blur(0)" : "blur(4px)")}; + } + + @media (max-width: 768px) { + height: 350px; + } +`; + +export const ImgWrapper = styled.div` + position: relative; + width: 100%; + height: 100%; + cursor: pointer; + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + + &::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 360px; + background: linear-gradient( + to top, + rgba(0, 0, 0, 1) 0%, + rgba(0, 0, 0, 0.45) 45%, + rgba(0, 0, 0, 0) 100% + ); + pointer-events: none; + z-index: 1; + } + + @media (max-width: 960px) { + &::after { + height: 200px; + } + } +`; + +export const Overlay = styled.div` + position: absolute; + bottom: 76px; + left: 40px; + right: 40px; + z-index: 10; + display: flex; + flex-direction: column; + gap: 8px; + color: #fff; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7); + animation: fadeInUp 0.5s ease forwards; + + @keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @media (max-width: 960px) { + bottom: 40px; + left: 20px; + right: 20px; + text-shadow: none; + } + + @media (max-width: 768px) { + bottom: 30px; + left: 16px; + right: 16px; + gap: 4px; + } +`; diff --git a/src/components/Categories.jsx b/src/components/Categories/index.jsx similarity index 96% rename from src/components/Categories.jsx rename to src/components/Categories/index.jsx index 91ab9c61..b4e385cd 100644 --- a/src/components/Categories.jsx +++ b/src/components/Categories/index.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { Link } from "react-router-dom"; import styled from "styled-components"; diff --git a/src/components/CommonStyle/Icon.jsx b/src/components/CommonStyle/Icon.jsx deleted file mode 100644 index 7805c44f..00000000 --- a/src/components/CommonStyle/Icon.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import styled from "styled-components"; - -const StyledFontAwesomeIcon = styled(FontAwesomeIcon)` - font-size: ${(props) => props.$size || "24px"}; - cursor: ${(props) => (props.onClick ? "pointer" : "default")}; - color: ${(props) => props.$color || "#fff"}; - transition: transform 0.3s ease; - - &.user { - @media (max-width: 900px) { - display: none; - } - } - - &.bell { - display: none; - @media (max-width: 900px) { - display: block; - } - } -`; - -const Icon = ({ icon, className, onClick, size, color, label }) => { - return ( - - ); -}; - -export default Icon; diff --git a/src/components/CommonStyle/TitleStyle.jsx b/src/components/CommonStyle/TitleStyle.jsx deleted file mode 100644 index c4c95617..00000000 --- a/src/components/CommonStyle/TitleStyle.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import styled from "styled-components"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -export const TitleWrap = styled.div` - height: 100%; - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding-left: 30px; - padding-right: 36px; - - @media (max-width: 1240px) { - padding-left: 24px; - padding-right: 24px; - } - - @media (max-width: 768px) { - padding-left: 16px; - padding-right: 16px; - margin-bottom: 12px; - } -`; - -export const Title = styled.h1` - font-size: 24px; - color: #fff; - - @media (max-width: 768px) { - font-size: 20px; - } -`; - -export const PlusBtn = styled.div` - display: flex; - align-items: center; - gap: 4px; - cursor: pointer; - - @media (max-width: 768px) { - font-size: 12px; - line-height: 12px; - } -`; - -export const P = styled.span` - font-size: 14px; - line-height: 14px; - color: #fff; -`; - -export const StyledIcon = styled(FontAwesomeIcon)` - color: #fff; - font-size: 12px; - vertical-align: middle; - - @media (max-width: 768px) { - font-size: 10px; - } -`; diff --git a/src/components/Header/UserSection.jsx b/src/components/Header/UserSection.jsx new file mode 100644 index 00000000..c0bd02e2 --- /dev/null +++ b/src/components/Header/UserSection.jsx @@ -0,0 +1,107 @@ +import { Typography, Icon } from "@/components"; +import { Link } from "react-router-dom"; +import styled from "@emotion/styled"; + +const UserSection = ({ loading, user, onLogout }) => { + if (loading) { + return ...; + } + + if (user) { + return ( + <> + + +
+ + 내 프로필 + + +
+
+ + + + + ); + } + + return ( + + + + ); +}; +export default UserSection; + +const UserMenu = styled.div` + position: relative; + + .user-icon { + color: ${(props) => props.theme.colors.accent}; + transition: transform 0.2s ease; + cursor: pointer; + + @media (max-width: 900px) { + display: none; + } + } + + &:hover .user-icon { + transform: scale(1.1); + } + + .dropdown { + display: none; + position: absolute; + top: 100%; + right: 0; + background: rgba(26, 28, 32, 0.95); + backdrop-filter: blur(20px); + border-radius: 8px; + padding: 8px; + min-width: 150px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 1000; + + a, + button { + display: block; + padding: 12px 16px; + color: ${(props) => props.theme.colors.text}; + text-decoration: none; + background: transparent; + border: none; + width: 100%; + text-align: left; + cursor: pointer; + border-radius: 4px; + transition: background 0.2s; + + &:hover { + background: rgba(26, 28, 32, 0.76); + color: ${(props) => props.theme.colors.accent}; + } + } + } + + &:hover .dropdown { + display: block; + } +`; + +const BellIcon = styled.div` + display: none; + cursor: pointer; + + @media (max-width: 900px) { + display: block; + } + + &:hover { + transform: scale(1.1); + transition: transform 0.3s ease; + } +`; diff --git a/src/components/Header/Header.jsx b/src/components/Header/index.jsx similarity index 57% rename from src/components/Header/Header.jsx rename to src/components/Header/index.jsx index 43870685..45ce7c2d 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/index.jsx @@ -1,27 +1,25 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useNavigate, Link } from "react-router-dom"; +import { debounce } from "lodash"; +import { useAuth } from "@/hooks"; +import { + Icon, + Typography, + SearchInput, + Categories, + SideMenu, +} from "@/components"; + import { HeaderArea, - StyledLogo, - StyledTopRow, - StyledRightSection, + TopRow, + Logo, + RightSection, IconGroup, HamburgerBtn, - SearchBar, UserMenu, -} from "./HeaderStyle"; - -import { - faUser, - faBell, - faBars, - faXmark, -} from "@fortawesome/free-solid-svg-icons"; -import Icon from "@components/CommonStyle/Icon"; -import { debounce } from "lodash"; -import Categories from "@components/Categories"; -import SideMenu from "@components/SideMenu"; -import { useAuth } from "@/hook/useAuth"; + BellIcon, +} from "./style"; const Header = () => { const [hasBackdropFilter, setHasBackdropFilter] = useState(false); @@ -51,6 +49,8 @@ const Header = () => { const handleSearchSubmit = (e) => { if (e.key === "Enter" && searchValue.trim() !== "") { navigate(`/search?q=${searchValue}`); + } else { + navigate("/search"); } }; @@ -59,9 +59,7 @@ const Header = () => { }; const toggleMenu = () => { - setIsMenuOpen((prev) => { - return !prev; - }); + setIsMenuOpen((prev) => !prev); }; const closeMenu = () => { @@ -76,41 +74,53 @@ const Header = () => { return ( <> - - - keynema_logo - + + + keynema_logo + - - + {loading ? ( - ... + ... ) : user ? ( <> - +
- 내 프로필 - + + 내 프로필 + +
- + + + ) : ( - - + + )} @@ -120,11 +130,15 @@ const Header = () => { aria-label="메뉴" aria-expanded={isMenuOpen} > - +
-
-
+ +
diff --git a/src/components/Header/HeaderStyle.jsx b/src/components/Header/style.jsx similarity index 75% rename from src/components/Header/HeaderStyle.jsx rename to src/components/Header/style.jsx index cf8718b1..72c85e9a 100644 --- a/src/components/Header/HeaderStyle.jsx +++ b/src/components/Header/style.jsx @@ -1,10 +1,10 @@ -import styled from "styled-components"; +// components/Header/style.js +import styled from "@emotion/styled"; export const HeaderArea = styled.header` width: 100%; position: fixed; top: 0; - display: flex; justify-content: space-between; align-items: center; @@ -20,38 +20,43 @@ export const HeaderArea = styled.header` backdrop-filter: blur(50px); `} - /* 모바일 */ @media (max-width: 768px) { - padding: 52px 16px 20px; + padding: 0 16px 20px; backdrop-filter: blur(20px); } `; -export const StyledTopRow = styled.div` +export const TopRow = styled.div` display: flex; justify-content: space-between; align-items: center; width: 100%; + gap: 40px; @media (max-width: 960px) { flex-wrap: wrap; } `; -export const StyledLogo = styled.a` +export const Logo = styled.a` position: fixed; top: 20px; left: 100px; cursor: pointer; + width: 128px; + display: block; img { width: 128px; + height: auto; object-fit: contain; + display: block; } @media (max-width: 960px) { - top: 52px; + top: 44px; left: 16px; + width: 118px; img { width: 118px; @@ -59,8 +64,7 @@ export const StyledLogo = styled.a` } @media (max-width: 768px) { - top: 52px; - left: 16px; + width: 108px; img { width: 108px; @@ -69,42 +73,20 @@ export const StyledLogo = styled.a` } `; -export const StyledRightSection = styled.div` +export const RightSection = styled.div` display: flex; align-items: center; gap: 20px; - @media (max-width: 900px) { + @media (max-width: 1240px) { flex-wrap: wrap; width: 100%; justify-content: flex-end; - } -`; - -export const SearchBar = styled.input` - width: 400px; - height: 40px; - background-color: rgba(217, 217, 217, 0.1); - border-radius: 4px; - font-size: 16px; - color: #fff; - border: 1px solid #fff; - padding: 16px 12px; - transition: all 0.3s ease; - - &:focus { - outline: none; - border-color: #ff1a66; - } - - &::placeholder { - color: rgba(255, 255, 255, 0.5); - } - - @media (max-width: 1240px) { - width: 100%; order: 3; - margin-bottom: 12px; + + @media (max-width: 1240px) { + order: 2; + } } `; @@ -123,31 +105,35 @@ export const HamburgerBtn = styled.button` cursor: pointer; padding: 0; display: flex; - flex-direction: column; - justify-content: center; align-items: center; + justify-content: center; + + &:focus { + outline: none; + } `; export const UserMenu = styled.div` position: relative; - cursor: pointer; - .user { - color: #ff1a66; + .user-icon { + color: ${(props) => props.theme.colors.accent}; transition: transform 0.2s ease; + cursor: pointer; @media (max-width: 900px) { display: none; } } - &:hover .user { + + &:hover .user-icon { transform: scale(1.1); } .dropdown { display: none; position: absolute; - top: 100%px; + top: 100%; right: 0; background: rgba(26, 28, 32, 0.95); backdrop-filter: blur(20px); @@ -160,9 +146,8 @@ export const UserMenu = styled.div` a, button { display: block; - font-size: 16px; padding: 12px 16px; - color: white; + color: ${(props) => props.theme.colors.text}; text-decoration: none; background: transparent; border: none; @@ -174,7 +159,7 @@ export const UserMenu = styled.div` &:hover { background: rgba(26, 28, 32, 0.76); - color: #ff1a66; + color: ${(props) => props.theme.colors.accent}; } } } @@ -183,3 +168,17 @@ export const UserMenu = styled.div` display: block; } `; + +export const BellIcon = styled.div` + display: none; + cursor: pointer; + + @media (max-width: 900px) { + display: block; + } + + &:hover { + transform: scale(1.1); + transition: transform 0.3s ease; + } +`; diff --git a/src/App.css b/src/components/MyRivew/index.jsx similarity index 100% rename from src/App.css rename to src/components/MyRivew/index.jsx diff --git a/src/components/MyRivew/style.jsx b/src/components/MyRivew/style.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/PopularMovies.jsx b/src/components/PopularMovies.jsx deleted file mode 100644 index 48d5626c..00000000 --- a/src/components/PopularMovies.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from "react"; -import { useNavigate } from "react-router-dom"; -import { useFetchData } from "@hook/useFetchData"; -import styled from "styled-components"; -import { faChevronRight } from "@fortawesome/free-solid-svg-icons"; -import { PageContainer } from "@components/CommonStyle/ContainerStyle"; -import { - TitleWrap, - Title, - PlusBtn, - P, - StyledIcon, -} from "@components/CommonStyle/TitleStyle"; - -const PopularMovies = () => { - const navigate = useNavigate(); - - const { data: popularData, loading, error } = useFetchData("/movie/popular"); - // const { data: topRatedData } = useFetchData("movie/top_rated") - - if (loading) - return ( - - Loading... - - ); - if (error) - return ( - - Error: {error.message} - - ); - - const popular = popularData?.results || []; - // const topRated = topRatedData?.results || [] - - return ( - - - 지금 Hot한 영화 - -

더보기

- -
-
- - {popular - .filter((movie) => movie.poster_path) - .map((movie) => ( - navigate(`/movie/${movie.id}`)}> - - {movie.title} - - ))} - -
- ); -}; - -export default PopularMovies; - -const Container = styled(PageContainer)` - position: relative; - z-index: 0; -`; - -const Grid = styled.ul` - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 10px; - list-style: none; - padding: 0; - margin: 0; - - @media (max-width: 1400px) { - grid-template-columns: repeat(4, 1fr); - } - @media (max-width: 1000px) { - grid-template-columns: repeat(3, 1fr); - } - @media (max-width: 768px) { - grid-template-columns: repeat(2, 1fr); - } - @media (max-width: 375px) { - grid-template-columns: 1fr; - } -`; -const Card = styled.li` - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - cursor: pointer; - position: relative; - z-index: 1; - transition: transform 0.3s ease; - - &:hover { - transform: translateY(-8px); - } -`; - -const Poster = styled.img` - width: 100%; - max-width: 270px; - height: auto; - aspect-ratio: 2/3; - border-radius: 8px; - object-fit: cover; - transition: transform 0.3s ease, box-shadow 0.3s ease; - - ${Card}:hover & { - transform: scale(1.05); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); - z-index: 10; - } -`; - -// 영화 제목 -const MovieTitle = styled.p` - margin-top: 8px; - margin-bottom: 16px; - font-size: 16px; - font-weight: 700; - color: #fff; - line-height: 1.4; -`; - -const Message = styled.p` - font-size: 18px; - color: #d9d9d9; - text-align: center; - margin-top: 60px; -`; diff --git a/src/components/PopularMovies/index.jsx b/src/components/PopularMovies/index.jsx new file mode 100644 index 00000000..324a4aa1 --- /dev/null +++ b/src/components/PopularMovies/index.jsx @@ -0,0 +1,64 @@ +import { useNavigate } from "react-router-dom"; +import { useInfiniteMovies, useIntersectionObserver } from "@/hooks"; +import { LoadingBox } from "./style"; +import { + PageContainer, + MovieGrid, + Typography, + SectionTitle, +} from "@/components"; + +const PopularMovies = () => { + const navigate = useNavigate(); + + //무한 스크롤 훅 불러오기 + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + loading, + error, + } = useInfiniteMovies("popular"); + + //자동 로딩 + const observerTarget = useIntersectionObserver({ + onIntersect: () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + enabled: hasNextPage, + }); + + if (loading) return Loading...; + if (error) return Error: {error.message}; + + const allMovies = + data?.pages + .flatMap((page) => page.results) + .filter((movie) => movie.poster_path) || []; + + return ( + + navigate("/popular")} + /> + + + {/* 자동로딩영역 */} + + {isFetchingNextPage && 더 불러오는 중...} + + + ); +}; + +export default PopularMovies; diff --git a/src/components/PopularMovies/style.jsx b/src/components/PopularMovies/style.jsx new file mode 100644 index 00000000..d5f88906 --- /dev/null +++ b/src/components/PopularMovies/style.jsx @@ -0,0 +1,9 @@ +import styled from "@emotion/styled"; + +export const LoadingBox = styled.div` + height: 100px; + display: flex; + align-items: center; + justify-content: center; + margin: 40px 0; +`; diff --git a/src/components/SideMenu/SideMenuStyle.jsx b/src/components/SideMenu/SideMenuStyle.jsx deleted file mode 100644 index 95eb920a..00000000 --- a/src/components/SideMenu/SideMenuStyle.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import styled from "styled-components"; - -export const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - width: 100%; - max-height: 677px; - background-color: transparent; - backdrop-filter: blur(10px); - opacity: ${(props) => (props.$isOpen ? 1 : 0)}; - visibility: ${(props) => (props.$isOpen ? "visible" : "hidden")}; - transition: opacity 0.3s ease, visibility 0.3s ease; - z-index: 1999; -`; - -export const MenuContainer = styled.nav` - position: fixed; - top: 0; - right: 0; - max-height: 677px; - width: 100%; - background-color: rgba(26, 28, 32, 0.2); - backdrop-filter: blur(25px); - transform: translateX(${(props) => (props.$isOpen ? "0" : "100%")}); - transition: transform 0.3s ease; - z-index: 2000; - padding: 250px 98px 40px; - overflow-y: auto; - - @media (max-width: 768px) { - max-width: 100%; - } -`; - -export const CloseButton = styled.button` - position: absolute; - top: 20px; - right: 20px; - background: transparent; - border: none; - color: white; - font-size: 28px; - cursor: pointer; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.3s ease; - - &:hover { - color: #ff1a66; - } -`; - -export const CategoryTabs = styled.div` - display: flex; - gap: 24px; - margin-bottom: 40px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -`; - -export const CategoryTab = styled.button` - background: transparent; - border: none; - color: ${(props) => (props.$active ? "#ff1a66" : "rgba(255, 255, 255, 0.6)")}; - font-size: 32px; - font-weight: ${(props) => (props.$active ? "700" : "400")}; - padding: 12px 0; - cursor: pointer; - border-bottom: ${(props) => (props.$active ? "1px solid #ff1a66" : "none")}; - margin-bottom: -1px; - transition: color 0.3s ease; - - &:hover { - color: #ff1a66; - } -`; - -export const MenuList = styled.ul` - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 8px; -`; - -export const MenuItem = styled.li` - padding: 16px 12px; - color: ${(props) => (props.$active ? "#ff1a66" : "white")}; - font-size: 20px; - font-weight: ${(props) => (props.$active ? "700" : "400")}; - cursor: pointer; - border-radius: 8px; - transition: background-color 0.3s ease, color 0.3s ease; - - &:hover { - background-color: rgba(255, 26, 102, 0.1); - color: #ff1a66; - } -`; diff --git a/src/components/SideMenu/index.jsx b/src/components/SideMenu/index.jsx index 1dcad796..56bbbf53 100644 --- a/src/components/SideMenu/index.jsx +++ b/src/components/SideMenu/index.jsx @@ -1,21 +1,21 @@ -import React, { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; +import { SIDE_MENU_CATEGORIES, DEFAULT_CATEGORY } from "@/constants/menu"; import { - Overlay, MenuContainer, CloseButton, CategoryTabs, + CategorySection, CategoryTab, MenuList, MenuItem, -} from "./SideMenuStyle"; +} from "./style"; const SideMenu = ({ isOpen, onClose }) => { - const [activeCategory, setActiveCategory] = useState("kines-pick"); + const [activeCategory, setActiveCategory] = useState(DEFAULT_CATEGORY); const menuRef = useRef(null); const navigate = useNavigate(); const location = useLocation(); - const prevPathnameRef = useRef(location.pathname); useEffect(() => { const handleClickOutside = (e) => { @@ -28,72 +28,42 @@ const SideMenu = ({ isOpen, onClose }) => { return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOpen, onClose]); - // 페이지 이동 시 메뉴 닫기 - useEffect(() => { - if (prevPathnameRef.current !== location.pathname && isOpen) onClose(); - prevPathnameRef.current = location.pathname; - }, [location.pathname, isOpen, onClose]); - const handleNavigate = (path) => { navigate(path); onClose(); }; - const categories = { - "kines-pick": { - name: "Kine's pick", - items: [ - { label: "홈", path: "/" }, - { label: "인기 영화", path: "/popular" }, - { label: "검색", path: "/search" }, - ], - }, - community: { - name: "커뮤니티", - items: [ - { label: "자유게시판", path: "/community/free" }, - { label: "영화 리뷰", path: "/community/review" }, - ], - }, - mykinema: { - name: "나의KINEMA", - items: [ - { label: "내 정보", path: "/mypage" }, - { label: "찜한 영화", path: "/mypage/wishlist" }, - { label: "내 리뷰", path: "/mypage/reviews" }, - ], - }, - }; - return ( <> - - {Object.keys(categories).map((key) => ( - setActiveCategory(key)} - > - {categories[key].name} - - ))} - + {Object.keys(SIDE_MENU_CATEGORIES).map((key) => ( + + {/* 탭 버튼 */} + setActiveCategory(key)} + > + {SIDE_MENU_CATEGORIES[key].name} + - - {categories[activeCategory].items.map((item, index) => ( - handleNavigate(item.path)} - $active={location.pathname === item.path} - > - {item.label} - + {/* 하위 메뉴 (active된 탭만 보임) */} + + {SIDE_MENU_CATEGORIES[key].items.map((item, index) => ( + handleNavigate(item.path)} + $active={location.pathname === item.path} + > + {item.label} + + ))} + + ))} - + ); diff --git a/src/components/SideMenu/style.jsx b/src/components/SideMenu/style.jsx new file mode 100644 index 00000000..68779903 --- /dev/null +++ b/src/components/SideMenu/style.jsx @@ -0,0 +1,208 @@ +import styled, { keyframes } from "styled-components"; + +export const Overlay = styled.div` + display: none; +`; + +export const MenuContainer = styled.nav` + position: fixed; + top: 0; + right: 0; + width: 100%; + height: auto; + max-height: 90vh; + background-color: rgba(26, 28, 32, 0.2); + backdrop-filter: blur(25px); + transform: translateY(${(props) => (props.$isOpen ? "0px" : "-100%")}); + transition: transform 0.3s ease; + z-index: 2000; + padding: 250px 98px 40px; + overflow-x: auto; + + @media (max-width: 768px) { + left: auto; + right: 0; + width: 100%; + width: 280px; + height: 100vh; + max-height: 100vh; + padding: 80px 24px 40px; + transform: translateX(${(props) => (props.$isOpen ? "0" : "100%")}); + background-color: rgba(26, 28, 32, 0.2); + } + + @media (max-width: 480px) { + max-width: 280px; + padding: 80px 20px 32px; + } +`; + +export const CloseButton = styled.button` + position: absolute; + top: 20px; + right: 100px; + background: transparent; + border: none; + color: white; + font-size: 28px; + cursor: pointer; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.3s ease; + + &:hover { + color: #ff1a66; + } + + @media (max-width: 768px) { + position: absolute; + top: 40px; + right: 20px; + } +`; + +export const CategoryTabs = styled.div` + display: flex; + gap: 120px; + align-items: flex-start; + + @media (max-width: 1024px) { + gap: 60px; + } + + @media (max-width: 768px) { + flex-direction: column; + gap: 0; + width: 100%; + } +`; + +export const CategorySection = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + + @media (max-width: 768px) { + width: 100%; + margin-bottom: 32px; + + &:last-child { + margin-bottom: 0; + } + } +`; + +export const CategoryTab = styled.button` + background: transparent; + border: none; + color: ${(props) => (props.$active ? "#ff1a66" : "rgba(255, 255, 255, 0.6)")}; + font-size: 32px; + font-weight: ${(props) => (props.$active ? "700" : "400")}; + padding: 12px 0; + cursor: pointer; + border-bottom: ${(props) => + props.$active ? "4px solid #ff1a66" : "4px solid transparent"}; + margin-bottom: 20px; + transition: all 0.3s ease; + white-space: nowrap; + + @media (max-width: 1024px) { + font-size: 28px; + padding: 10px 0; + margin-bottom: 16px; + border-bottom-width: 3px; + } + + @media (max-width: 768px) { + font-size: 18px; + font-weight: 700; + padding: 8px 0; + margin-bottom: 16px; + width: 100%; + text-align: left; + color: #ff1a66; + border-bottom: none; + cursor: default; + pointer-events: none; + + &:hover { + color: #ff1a66; + } + } +`; + +const fadeSlideIn = keyframes` + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + +// 메뉴 리스트 (탭 아래 붙음) +export const MenuList = styled.ul` + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + min-width: 200px; + + /* active된 탭 아래에만 나타남 */ + opacity: ${(props) => (props.$visible ? 1 : 0)}; + visibility: ${(props) => (props.$visible ? "visible" : "hidden")}; + height: ${(props) => (props.$visible ? "auto" : "0")}; + overflow: hidden; + + animation: ${(props) => (props.$visible ? fadeSlideIn : "none")} 0.3s + ease-in-out; + transition: opacity 0.3s ease, visibility 0.3s ease; + + @media (max-width: 768px) { + min-width: 100%; + gap: 0; + opacity: 1; + visibility: visible; + height: auto; + } +`; + +export const MenuItem = styled.li` + padding: 16px 12px; + color: ${(props) => (props.$active ? "#ff1a66" : "white")}; + font-size: 20px; + font-weight: ${(props) => (props.$active ? "700" : "400")}; + cursor: pointer; + border-radius: 8px; + transition: background-color 0.3s ease, color 0.3s ease; + white-space: nowrap; + text-align: left; + + &:hover { + color: #ff1a66; + } + + @media (max-width: 1024px) { + font-size: 18px; + padding: 14px 10px; + } + + @media (max-width: 768px) { + font-size: 15px; + padding: 12px 8px; + font-weight: ${(props) => (props.$active ? "600" : "400")}; + } + + @media (max-width: 480px) { + font-size: 14px; + padding: 10px 8px; + } +`; diff --git a/src/components/TopRankedMovie.jsx b/src/components/TopRankedMovie.jsx deleted file mode 100644 index 5e5a9770..00000000 --- a/src/components/TopRankedMovie.jsx +++ /dev/null @@ -1,243 +0,0 @@ -import React from "react"; -import { useNavigate } from "react-router-dom"; -import { useFetchData } from "@/hook/useFetchData"; -import styled from "styled-components"; -import { PageContainer } from "@components/CommonStyle/ContainerStyle"; -import { - TitleWrap, - Title, - PlusBtn, - P, - StyledIcon, -} from "./CommonStyle/TitleStyle"; -import { - faChevronRight, - faChevronLeft, -} from "@fortawesome/free-solid-svg-icons"; -import { Container } from "@/pages/DetailPage"; -import { Swiper, SwiperSlide } from "swiper/react"; -import { Navigation } from "swiper/modules"; -import "swiper/css"; -import "swiper/css/navigation"; - -const TopRankedMovie = () => { - const navigate = useNavigate(); - - const { - data: topRagedData, - loading, - error, - } = useFetchData("/movie/top_rated"); - - if (loading) return Loading...; - if (error) - return ( - - Error: {error.message} - - ); - - const topRated = topRagedData?.results?.slice(0, 10) || []; - - return ( - <> - - - KINEMA HOT 랭킹 - navigate(`/movie/top_ranked`)}> -

더보기

- -
-
- - - - {topRated.map((movie, index) => ( - - navigate(`/movie/${movie.id}`)}> - {index + 1} - - - - ))} - - - {/* 커스텀 네비게이션 */} - - - - - - - -
- - ); -}; - -export default TopRankedMovie; - -const SwiperContainer = styled.div` - position: relative; - - &:hover .custom-prev, - &:hover .custom-next { - opacity: 1; - } -`; - -const CustomNavButton = styled.button` - position: absolute; - top: 50%; - transform: translateY(-50%); - ${(props) => (props.$isNext ? "right: 68px;" : "left: 68px;")} - - width: 50px; - height: 50px; - border-radius: 50%; - background-color: rgba(255, 255, 255, 0.2); - border: 0.8px solid #fff; - - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 10; - - opacity: 0; - transition: opacity 0.3s ease; - - svg { - color: #fff; - font-size: 20px; - } - - &:hover { - background-color: rgba(255, 255, 255, 0.4); - } - - &.swiper-button-disabled { - opacity: 0.3; - cursor: not-allowed; - } - - @media (max-width: 768px) { - display: none; - } -`; - -const RankingCard = styled.div` - position: relative; - width: 200px; - height: 300px; - flex-shrink: 0; - cursor: pointer; - - @media (max-width: 1240px) { - width: 180px; - height: 270px; - } - - @media (max-width: 960px) { - width: 160px; - height: 240px; - } - - @media (max-width: 768px) { - width: 140px; - height: 210px; - } -`; - -const RankNumber = styled.div` - position: absolute; - bottom: 40px; - left: 30%; - transform: translateX(-30%); - font-size: 200px; - font-weight: bold; - color: transparent; - -webkit-text-stroke: 5px #fff; /* 테두리만 보이게 */ - z-index: 1; - - @media (max-width: 1240px) { - font-size: 180px; - bottom: 30px; - -webkit-text-stroke: 4px #fff; - } - - @media (max-width: 960px) { - font-size: 140px; - bottom: 30px; - left: 20%; - transform: translateX(-20%); - -webkit-text-stroke: 3px #fff; - } - - @media (max-width: 768px) { - font-size: 100px; - bottom: 30px; - left: 5%; - transform: translateX(-10%); - -webkit-text-stroke: 2px #fff; - } -`; - -const Poster = styled.img` - position: absolute; - left: 100px; - width: 160px; - height: 240px; - object-fit: cover; - border-radius: 10px; - z-index: 2; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - - &:first-child { - position: absolute; - top: 20px; - left: 70px; - } - - @media (max-width: 1240px) { - left: 80px; - width: 140px; - height: 210px; - } - - @media (max-width: 960px) { - left: 70px; - width: 120px; - height: 180px; - } - - @media (max-width: 768px) { - left: 40px; - width: 120px; - height: 180px; - } -`; - -const Message = styled.div` - color: #fff; - font-size: 18px; -`; diff --git a/src/components/TopRankedMovie/index.jsx b/src/components/TopRankedMovie/index.jsx new file mode 100644 index 00000000..bde79c26 --- /dev/null +++ b/src/components/TopRankedMovie/index.jsx @@ -0,0 +1,89 @@ +import { useNavigate } from "react-router-dom"; +import { useFetchData } from "@/hooks"; +import { Navigation } from "swiper/modules"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { getImageUrl } from "@/constants/images"; + +import { PageContainer, SectionTitle, Typography, Icon } from "@components"; + +import "swiper/css"; +import "swiper/css/navigation"; +import { + SwiperContainer, + CustomNavButton, + RankingCard, + RankNumber, + Poster, +} from "./style"; + +const TopRankedMovie = () => { + const navigate = useNavigate(); + + const { + data: topRatedData, + loading, + error, + } = useFetchData("/movie/top_rated"); + + if (loading) return Loading...; + if (error) return Error: {error.message}; + + const topRated = topRatedData?.results?.slice(0, 10) || []; + + return ( + + navigate("/movie/top_ranked")} + /> + + + + {topRated.map((movie, index) => { + const rank = index + 1; + return ( + + navigate(`/movie/${movie.id}`)} + > + {rank} + + + + ); + })} + + + {/* 커스텀 네비게이션 */} + + + + + + + + + ); +}; + +export default TopRankedMovie; diff --git a/src/components/TopRankedMovie/style.jsx b/src/components/TopRankedMovie/style.jsx new file mode 100644 index 00000000..d2c8329c --- /dev/null +++ b/src/components/TopRankedMovie/style.jsx @@ -0,0 +1,190 @@ +import styled from "@emotion/styled"; + +export const SwiperContainer = styled.div` + position: relative; + padding: 0; + + &:hover .custom-prev, + &:hover .custom-next { + opacity: 1; + } +`; + +export const CustomNavButton = styled.button` + position: absolute; + top: 40%; + transform: translateY(-40%); + ${(props) => (props.$isNext ? "right: 20px;" : "left: 20px;")} + + width: 50px; + height: 50px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.2); + border: 0.8px solid ${(props) => props.theme.colors.text}; + + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10; + padding: 0; + + opacity: 0; + transition: opacity 0.3s ease, background-color 0.3s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.4); + } + + &:focus { + outline: none; + } + + &.swiper-button-disabled { + opacity: 0.3; + cursor: not-allowed; + } + + @media (max-width: 768px) { + display: none; + } +`; + +export const RankingCard = styled.div` + position: relative; + width: 200px; + height: 300px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: flex-start; + + /* 카드와 카드 사이 간격 */ + margin-right: 20px; + + /* 10번 이상일 때 카드 너비 증가 */ + ${(props) => + props.$rank >= 8 && + ` + width: 240px; + `} + + @media (max-width: 1240px) { + width: 180px; + height: 270px; + margin-right: 16px; + + ${(props) => + props.$rank >= 10 && + ` + width: 220px; + `} + } + + @media (max-width: 960px) { + width: 160px; + height: 240px; + margin-right: 12px; + + ${(props) => + props.$rank >= 10 && + ` + width: 190px; + `} + } + + @media (max-width: 768px) { + width: 140px; + height: 210px; + margin-right: 12px; + + ${(props) => + props.$rank >= 10 && + ` + width: 170px; + `} + } +`; + +export const RankNumber = styled.div` + position: absolute; + bottom: 30px; + left: ${(props) => { + if (props.$rank === 1) return "4px"; + if (props.$rank >= 8) return "-4px"; // 10번: 0 안 가려지게 + return "0px"; + }}; + font-size: 180px; + font-weight: 900; + font-family: ${(props) => props.theme.font.family}; + color: transparent; + -webkit-text-stroke: 5px ${(props) => props.theme.colors.text}; + z-index: 1; + pointer-events: none; + line-height: 1; + margin-left: 4px; + + @media (max-width: 1240px) { + font-size: 180px; + left: -16px; + -webkit-text-stroke: 4px ${(props) => props.theme.colors.text}; + } + + @media (max-width: 960px) { + font-size: 140px; + left: -16px; + -webkit-text-stroke: 3px ${(props) => props.theme.colors.text}; + } + + @media (max-width: 768px) { + font-size: 100px; + left: -8px; + -webkit-text-stroke: 2px ${(props) => props.theme.colors.text}; + } +`; + +export const Poster = styled.img` + width: 160px; + height: 240px; + object-fit: cover; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + display: block; + margin-left: 56px; + z-index: 2; + + /* 10번째 슬라이드만 margin-left 더 넓게! */ + .swiper-slide:nth-of-child(10) & { + margin-left: 75px; /* 👈 10번만 더 넓게 */ + } + + @media (max-width: 1240px) { + width: 140px; + height: 210px; + margin-left: 45px; + + .swiper-slide:nth-of-child(10) & { + margin-left: 65px; + } + } + + @media (max-width: 960px) { + width: 120px; + height: 180px; + margin-left: 35px; + + .swiper-slide:nth-of-child(10) & { + margin-left: 55px; + } + } + + @media (max-width: 768px) { + width: 100px; + height: 150px; + margin-left: 25px; + + .swiper-slide:nth-of-child(10) & { + margin-left: 40px; + } + } +`; diff --git a/src/components/common/Button.jsx b/src/components/common/Button.jsx new file mode 100644 index 00000000..b18bf77d --- /dev/null +++ b/src/components/common/Button.jsx @@ -0,0 +1,100 @@ +import styled from "@emotion/styled"; +import Typography from "./Typography"; + +const StyledButton = styled.button` + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-family: ${(props) => props.theme.font.family}; + transition: all 0.3s ease; + + ${(props) => + props.$variant === "primary" && + ` + background: ${props.theme.colors.accent}; + color: #fff; + border: none; + + &:hover { + opacity: 0.8; + } + + &:disabled { + background: ${props.theme.colors.disabled}; + cursor: not-allowed; + } + `} + + ${(props) => + props.$variant === "secondary" && + ` + background: transparent; + border: 1px solid ${props.theme.colors.accent}; + color: ${props.theme.colors.accent}; + + &:hover { + background: ${props.theme.colors.accent}; + color: #fff; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + `} + + /* 🔥 watch variant 추가 */ + ${(props) => + props.$variant === "watch" && + ` + width: 174px; + height: 51px; + background: transparent; + color: #aeb3be; + border: 1px solid #aeb3be; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + + &:hover:not(:disabled) { + background: #aeb3be; + color: #1a1c20 + transform: translateY(-2px); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + `} +`; + +const Button = ({ + label, + variant = "primary", + disabled, + onClick, + children, + ...props +}) => { + return ( + + + {children || label} + + + ); +}; + +export default Button; diff --git a/src/components/common/Carousel.jsx b/src/components/common/Carousel.jsx new file mode 100644 index 00000000..6789db13 --- /dev/null +++ b/src/components/common/Carousel.jsx @@ -0,0 +1,48 @@ +import styled from "@emotion/styled"; +import { Swiper, SwiperSlide } from "swiper/react"; +import "swiper/css"; + +const Container = styled.div` + width: 100%; + + .swiper-wrapper { + display: flex; + } +`; + +const Carousel = ({ + children, + data, + renderItem, + sliderPerView = 5, + spaceBetween = 16, + loop = false, + ...swiperProps +}) => { + return ( + + + {/* data + renderItem 패턴 */} + {data && renderItem + ? data.map((item, index) => ( + + {renderItem(item, index)} + + )) + : /* children 패턴 */ + children && Array.isArray(children) + ? children.map((child, idx) => ( + {child} + )) + : null} + + + ); +}; + +export default Carousel; diff --git a/src/components/CommonStyle/ContainerStyle.jsx b/src/components/common/Container.jsx similarity index 60% rename from src/components/CommonStyle/ContainerStyle.jsx rename to src/components/common/Container.jsx index cb456422..93a042b1 100644 --- a/src/components/CommonStyle/ContainerStyle.jsx +++ b/src/components/common/Container.jsx @@ -1,21 +1,19 @@ -import styled from "styled-components"; +import styled from "@emotion/styled"; -export const PageContainer = styled.div` +export const PageContainer = styled.section` margin: 0 auto; - padding: 0 80px; + padding: 0 100px; margin-bottom: 40px; @media (max-width: 1240px) { - padding: 0 60px; + padding: 0 40px; } - @media (max-width: 960px) { - padding: 0 40px; + padding: 0 24px; margin-bottom: 32px; } - @media (max-width: 768px) { - padding: 0 16px; + padding: 0 20px; margin-bottom: 24px; } `; diff --git a/src/components/common/Icon.jsx b/src/components/common/Icon.jsx new file mode 100644 index 00000000..0fb6c549 --- /dev/null +++ b/src/components/common/Icon.jsx @@ -0,0 +1,65 @@ +import styled from "@emotion/styled"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faHeart as faHeartSolid, + faUser, + faBell, + faBars, + faXmark, + faChevronRight, + faChevronLeft, + faStar as faStarSolid, +} from "@fortawesome/free-solid-svg-icons"; +import { + faHeart as faHeartRegular, + faStar as faStarRegular, +} from "@fortawesome/free-regular-svg-icons"; + +//아이콘 매핑 +const ICONS = { + heartSolid: faHeartSolid, + heartRegular: faHeartRegular, + starSolid: faStarSolid, + starRegular: faStarRegular, + user: faUser, + bell: faBell, + bars: faBars, + xmark: faXmark, + chevronRight: faChevronRight, + chevronLeft: faChevronLeft, +}; + +const StyledIcon = styled(FontAwesomeIcon, { + shouldForwardProp: (prop) => prop !== "$size", +})` + font-size: ${(props) => props.$size || "24px"}; + cursor: ${(props) => (props.onClick ? "pointer" : "default")}; + color: ${(props) => props.color || props.theme.colors.text}; + transition: transform 0.3s ease; +`; + +const Icon = ({ + name, + className, + onClick, + size = "24px", + color, + label, + ...rest +}) => { + const icon = ICONS[name]; + if (!icon) return null; + return ( + + ); +}; + +export default Icon; diff --git a/src/components/common/MovieCard.jsx b/src/components/common/MovieCard.jsx new file mode 100644 index 00000000..63ad66d9 --- /dev/null +++ b/src/components/common/MovieCard.jsx @@ -0,0 +1,86 @@ +import styled from "@emotion/styled"; +import Typography from "./Typography"; +import { getImageUrl } from "@/constants/images"; + +const Card = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const PosterWrapper = styled.div` + position: relative; + width: 100%; + padding-bottom: 150%; /* 2:3 비율 = (3/2) * 100% = 150% */ + overflow: hidden; + border-radius: 8px; + background-color: #1a1a1a; /* 로딩 중 배경색 */ + margin-bottom: 8px; +`; + +const Poster = styled.img` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; + transition: transform 0.3s ease, box-shadow 0.3s ease; + + &:hover { + transform: scale(1.05); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); + z-index: 10; + } +`; + +const RankBadge = styled.div` + margin-bottom: 4px; +`; + +const TitleWrapper = styled.div` + margin-top: 8px; + min-height: 48px; /* 제목 최소 높이 */ +`; + +const MovieCard = ({ title, poster, onClick, rank, size = "w500" }) => { + return ( + + {rank !== undefined && ( + + + #{rank} + + + )} + + + + + + {title} + + + + ); +}; + +export default MovieCard; diff --git a/src/components/common/MovieGrid.jsx b/src/components/common/MovieGrid.jsx new file mode 100644 index 00000000..9d427d17 --- /dev/null +++ b/src/components/common/MovieGrid.jsx @@ -0,0 +1,98 @@ +import { Link } from "react-router-dom"; +import styled from "@emotion/styled"; +import MovieCard from "./MovieCard"; + +const Container = styled.div` + margin: 0 auto; + padding: ${(props) => props.$padding || "20px 220px"}; + margin-bottom: 40px; + + @media (max-width: 1240px) { + padding: ${(props) => props.$paddingTablet || "50px 100px"}; + } + + @media (max-width: 960px) { + padding: ${(props) => props.$paddingMobile || "40px 16px"}; + margin-bottom: 32px; + } +`; + +const Title = styled.h2` + font-size: 32px; + font-weight: 700; + color: white; + padding: 0 60px; + + @media (max-width: 768px) { + font-size: 24px; + margin-bottom: 24px; + } +`; + +const Grid = styled.div` + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 20px; + padding: 0; + margin: 32px auto; + width: 100%; + + @media (max-width: 1240px) { + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 16px; + padding: 0; + } + + @media (max-width: 960px) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + padding: 0; + } + + @media (max-width: 768px) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + padding: 0; + } + + @media (max-width: 600px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 0; + } +`; + +const MovieGrid = ({ + movies, + title, + padding, + paddingTablet, + paddingMobile, +}) => { + return ( + + {title && {title}} + + {movies.map((movie, index) => ( + + + + ))} + + + ); +}; + +export default MovieGrid; diff --git a/src/components/common/SearchInput.jsx b/src/components/common/SearchInput.jsx new file mode 100644 index 00000000..e9ccc129 --- /dev/null +++ b/src/components/common/SearchInput.jsx @@ -0,0 +1,46 @@ +import styled from "@emotion/styled"; + +const StyledInput = styled.input` + width: 400px; + height: 40px; + background-color: rgba(217, 217, 217, 0.1); + border-radius: 4px; + font-size: 16px; + font-family: ${(props) => props.theme.font.family}; + color: ${(props) => props.theme.colors.text}; + border: 1px solid ${(props) => props.theme.colors.text}; + padding: 16px 12px; + transition: all 0.3s ease; + + &:focus { + outline: none; + border-color: ${(props) => props.theme.colors.accent}; + } + + &::placeholder { + color: ${(props) => props.theme.colors.disabled}; + } + + @media (max-width: 1240px) { + width: 100%; + order: 3; + } +`; + +const SearchInput = ({ + value, + onChange, + onKeyDown, + placeholder = "사람, 제목, 검색", +}) => { + return ( + + ); +}; +export default SearchInput; diff --git a/src/components/common/SectionTitle.jsx b/src/components/common/SectionTitle.jsx new file mode 100644 index 00000000..293ddb52 --- /dev/null +++ b/src/components/common/SectionTitle.jsx @@ -0,0 +1,76 @@ +import styled from "@emotion/styled"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import Typography from "./Typography"; + +const Wrapper = styled.div` + height: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0; + + @media (max-width: 768px) { + margin-bottom: 12px; + } +`; + +const MoreButton = styled.button` + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + background: none; + border: none; + color: ${(props) => props.theme.colors.text}; + transition: color 0.3s ease; + margin-right: 12px; + + &:hover { + color: ${(props) => props.theme.colors.accent}; + } + + &:focus { + outline: none; + } + + @media (max-width: 768px) { + font-size: 12px; + line-height: 12px; + } +`; + +const StyledIcon = styled(FontAwesomeIcon)` + font-size: 12px; + vertical-align: middle; + transition: color 0.3s ease; + + @media (max-width: 768px) { + font-size: 10px; + } +`; + +const SectionTitle = ({ + title, + showMoreButton = false, + onMoreClick, + moreText = "더보기", +}) => { + return ( + + + {title} + + {showMoreButton && ( + + + {moreText} + + + + )} + + ); +}; + +export default SectionTitle; diff --git a/src/components/common/StarRating.jsx b/src/components/common/StarRating.jsx new file mode 100644 index 00000000..8f430362 --- /dev/null +++ b/src/components/common/StarRating.jsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { Icon } from "@/components"; +import styled from "@emotion/styled"; + +const StarRating = ({ + rating, + onRatingChange, + size = "30px", + activeColor = "#ff1a66", + inactiveColor = "#d9d9d9", + interactive = false, +}) => { + const [hoverRating, setHoverRating] = useState(0); + + const handleClick = (star) => { + if (interactive && onRatingChange) { + onRatingChange(star); + } + }; + + const handleMouseEnter = (star) => { + if (interactive) { + setHoverRating(star); + } + }; + + const handleMouseLeave = () => { + if (interactive) { + setHoverRating(0); + } + }; + + return ( + + {[1, 2, 3, 4, 5].map((star) => ( + handleClick(star)} + onMouseEnter={() => handleMouseEnter(star)} + onMouseLeave={handleMouseLeave} + interactive={interactive} + disabled={!interactive} + > + + + ))} + + ); +}; + +export default StarRating; + +const StarContainer = styled.div` + display: flex; + gap: ${({ interactive }) => (interactive ? "12px" : "8px")}; + + ${({ interactive }) => + !interactive && + ` + svg { + font-size: ${({ size }) => size} !important; + width: ${({ size }) => size}; + height: ${({ size }) => size}; + } + `} + + @media (max-width: 768px) { + gap: ${({ interactive }) => (interactive ? "8px" : "4px")}; + + svg { + font-size: 30px !important; + width: 30px; + height: 30px; + } + } +`; + +const StarButton = styled.button` + background: transparent; + border: none; + cursor: ${({ interactive }) => (interactive ? "pointer" : "default")}; + padding: 0; + margin-top: 12px; + transition: ${({ interactive }) => + interactive ? "transform 0.2s ease" : "none"}; + display: flex; + align-items: center; + justify-content: center; + + ${({ interactive }) => + interactive && + ` + &:hover:not(:disabled) { + transform: scale(1.15); + } + `} + + &:disabled { + cursor: not-allowed; + opacity: ${({ interactive }) => (interactive ? "0.6" : "1")}; + } + + @media (max-width: 768px) { + margin-top: 8px; + } +`; diff --git a/src/components/common/TabMenu.jsx b/src/components/common/TabMenu.jsx new file mode 100644 index 00000000..654b4261 --- /dev/null +++ b/src/components/common/TabMenu.jsx @@ -0,0 +1,81 @@ +import styled from "@emotion/styled"; +import Typography from "./Typography"; + +const Wrapper = styled.div` + display: flex; + justify-content: flex-start; + margin-top: 99px; + padding-left: 225px; + + @media (max-width: 768px) { + margin-top: 40px; + padding: 0; + justify-content: center; + } +`; + +const TabContainer = styled.div` + display: flex; + gap: 8px; + + @media (max-width: 768px) { + max-width: 294px; + gap: 4px; + } +`; + +const Tab = styled.button` + padding: 10px 20px; + cursor: pointer; + font-family: ${(props) => props.theme.font.family}; + background: transparent; + border: none; + color: ${(props) => + props.$active ? props.theme.colors.text : "rgba(255,255,255,0.6)"}; + font-weight: ${(props) => (props.$active ? "700" : "400")}; + border-bottom: ${(props) => + props.$active ? `2px solid ${props.theme.colors.text}` : "none"}; + transition: color 0.2s, border-bottom 0.2s; + + &:hover { + color: ${(props) => props.theme.colors.text}; + } + + &:focus { + outline: none; + } + + @media (max-width: 768px) { + width: 74px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16px; + border: none; + background-color: ${(props) => + props.$active ? props.theme.colors.accent : "transparent"}; + font-size: 14px; + } +`; + +const TabMenu = ({ tabs, activeTab, onTabChange }) => { + return ( + + + {tabs.map((tab) => ( + onTabChange(tab.id)} + > + {tab.label} + + ))} + + + ); +}; + +export default TabMenu; diff --git a/src/components/common/Typography.jsx b/src/components/common/Typography.jsx new file mode 100644 index 00000000..e81ec31a --- /dev/null +++ b/src/components/common/Typography.jsx @@ -0,0 +1,110 @@ +import styled from "@emotion/styled"; + +const typographyVariants = { + //헤딩 + h1: ` + font-size: 48px; + font-weight: 700; + line-height: 1.2; + + @media (max-width: 768px) { + font-size: 24px; + } + `, + h2: ` + font-size: 28px; + font-weight: 700; + line-height: auto; + + @media (max-width: 768px) { + font-size: 20px; + } + `, + h3: ` + font-size: 24px; + font-weight: 500; + line-height: auto; + `, + h4: ` + font-size: 20px; + font-weight: 500; + line-height: auto; + `, + + //본문 + body: ` + font-size: 20px; + font-weight: 400; + line-height: 1.3; + + @media (max-width: 768px) { + font-size: 14px; + } + `, + + bodyMedium: ` + font-size: 18px; + font-weight: 400; + line-height: 1.5; + `, + + bodySmall: ` + font-size: 14px; + font-weight: 400; + line-height: 1.5; + `, + + //캡션 + caption: ` + font-size: 14px; + font-weight: 300; + + @media (max-width: 768px) { + font-size: 12px; + } + `, + + //섹션타이틀 + sectionTitle: ` + font-size: 24px; + font-weight: 700; + line-height: 1.2; + + @media (max-width: 768px) { + font-size: 20px; + } + `, + + //영화타이틀(카드) + movieTitle: ` + font-size: 14px; + font-weight: 500; + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + `, +}; + +const StyledTypography = styled.span` + color: ${(props) => props.color || props.theme.colors.text}; + font-family: ${(props) => props.theme.font.family}; + margin: 0; + ${(props) => typographyVariants[props.$variant] || typographyVariants.body} +`; + +const Typography = ({ + variant = "body", + color, + children, + tag = "span", + ...props +}) => { + return ( + + {children} + + ); +}; + +export default Typography; diff --git a/src/components/index.jsx b/src/components/index.jsx new file mode 100644 index 00000000..386b752f --- /dev/null +++ b/src/components/index.jsx @@ -0,0 +1,20 @@ +// common +export { default as Typography } from "./common/Typography"; +export { default as Button } from "./common/Button"; +export { default as Carousel } from "./common/Carousel"; +export { PageContainer } from "./common/Container"; +export { default as Icon } from "./common/Icon"; +export { default as StarRating } from "./common/StarRating"; +export { default as MovieCard } from "./common/MovieCard"; +export { default as MovieGrid } from "./common/MovieGrid"; +export { default as SearchInput } from "./common/SearchInput"; +export { default as SectionTitle } from "./common/SectionTitle"; +export { default as TabMenu } from "./common/TabMenu"; + +// 단일 컴포넌트 +export { default as Header } from "./Header"; +export { default as SideMenu } from "./SideMenu"; +export { default as Banner } from "./Banner"; +export { default as PopularMovies } from "./PopularMovies"; +export { default as TopRankedMovie } from "./TopRankedMovie"; +export { default as Categories } from "./Categories"; diff --git a/src/constants/images.jsx b/src/constants/images.jsx new file mode 100644 index 00000000..863a1c5d --- /dev/null +++ b/src/constants/images.jsx @@ -0,0 +1,14 @@ +export const IMAGE_BASE_URL = "https://image.tmdb.org/t/p"; + +export const IMAGE_SIZE = { + MEDIUM: "w500", + ORIGINAL: "original", +}; + +export const getImageUrl = (path, size = IMAGE_SIZE.MEDIUM) => { + if (!path) return "/movie/placeholder.jpg"; + return `${IMAGE_BASE_URL}/${size}${path}`; +}; + +export const LOGO_IMAGE = "/image/keynema_logo.png"; +export const SUBTRACT_IMAGE = "/image/subtract.png"; diff --git a/src/constants/menu.jsx b/src/constants/menu.jsx new file mode 100644 index 00000000..a1303caf --- /dev/null +++ b/src/constants/menu.jsx @@ -0,0 +1,27 @@ +export const SIDE_MENU_CATEGORIES = { + "kines-pick": { + name: "KINE's Pick", + items: [ + { label: "오늘의 추천영화", path: "/popular" }, + { label: "오늘의 랭킹", path: "/top_ranked" }, + ], + }, + community: { + name: "커뮤니티", + items: [ + { label: "요즘 뜨는 코멘트", path: "/community/free" }, + { label: "KINEMA 라운지", path: "/community/review" }, + ], + }, + mykinema: { + name: "나의KINEMA", + items: [ + { label: "마이페이지", path: "/mypage" }, + { label: "내가 찜한 영화", path: "/mypage/wishlist" }, + { label: "내가 쓴 리뷰", path: "/mypage/reviews" }, + ], + }, +}; + +//기본 활성화 카테고리 +export const DEFAULT_CATEGORY = "kines-pick"; diff --git a/src/hooks/index.jsx b/src/hooks/index.jsx new file mode 100644 index 00000000..4ec44b3f --- /dev/null +++ b/src/hooks/index.jsx @@ -0,0 +1,8 @@ +export { default as useAuth } from "./useAuth"; +export { default as useDebounce } from "./useDebounce"; +export { default as useFetchData } from "./useFetchData"; +export { default as useInfiniteMovies } from "./useInfiniteMovies"; +export { default as useIntersectionObserver } from "./useIntersectionObserver"; +export { default as useRating } from "./useRating"; +export { default as useWishlist } from "./useWishlist"; +export { default as useNoOverlay } from "./useNoOverlay"; diff --git a/src/hook/useAuth.jsx b/src/hooks/useAuth.jsx similarity index 81% rename from src/hook/useAuth.jsx rename to src/hooks/useAuth.jsx index 2b034632..87eabb3e 100644 --- a/src/hook/useAuth.jsx +++ b/src/hooks/useAuth.jsx @@ -1,11 +1,12 @@ -import React from "react"; import { useContext } from "react"; import { AuthContext } from "@context/AuthContext"; -export const useAuth = () => { +const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth는 AuthProvide 안에서 사용해야 합니다"); } return context; }; + +export default useAuth; diff --git a/src/hook/useDebounce.jsx b/src/hooks/useDebounce.jsx similarity index 82% rename from src/hook/useDebounce.jsx rename to src/hooks/useDebounce.jsx index 0fcd8501..35f85bd3 100644 --- a/src/hook/useDebounce.jsx +++ b/src/hooks/useDebounce.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -export const useDebounce = (value, delay) => { +const useDebounce = (value, delay) => { const [debouncevalue, setDebounceValue] = useState(value); useEffect(() => { @@ -14,3 +14,5 @@ export const useDebounce = (value, delay) => { return debouncevalue; }; + +export default useDebounce; diff --git a/src/hook/useFetchData.jsx b/src/hooks/useFetchData.jsx similarity index 89% rename from src/hook/useFetchData.jsx rename to src/hooks/useFetchData.jsx index 4a15550b..f4f1e58c 100644 --- a/src/hook/useFetchData.jsx +++ b/src/hooks/useFetchData.jsx @@ -1,7 +1,7 @@ import { useEffect, useState, useMemo } from "react"; -import { fetchData } from "@api/api"; +import { fetchData } from "@/api"; -export const useFetchData = (endpoint, params = {}) => { +const useFetchData = (endpoint, params = {}) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -46,3 +46,5 @@ export const useFetchData = (endpoint, params = {}) => { return { data, loading, error }; }; + +export default useFetchData; diff --git a/src/hooks/useInfiniteMovies.jsx b/src/hooks/useInfiniteMovies.jsx new file mode 100644 index 00000000..9ef81006 --- /dev/null +++ b/src/hooks/useInfiniteMovies.jsx @@ -0,0 +1,44 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { + fetchTopRagedMovies, + fetchSimilarMovies, + fetchPopularMovies, +} from "@/api"; + +const ITEMS_PER_PAGE = 18; + +//무한스크롤 영화 데이터 +const useInfiniteMovies = (type, movieId = null) => { + return useInfiniteQuery({ + queryKey: movieId ? ["movies", type, movieId] : ["movies", type], + + queryFn: ({ pageParam }) => { + if (type === "similar" && movieId) { + return fetchSimilarMovies({ movieId, pageParam }); + } + if (type === "popular") { + return fetchPopularMovies({ pageParam }); + } + return fetchTopRagedMovies({ pageParam }); + }, + + getNextPageParam: (lastPage, allPages) => { + if (allPages.length < lastPage.totalPages) { + return lastPage.nextPage; + } + return undefined; + }, + + select: (data) => ({ + pages: data.pages.map((page) => ({ + ...page, + results: page.results.slice(0, ITEMS_PER_PAGE), + })), + pageParams: data.pageParams, + }), + + initialPageParam: 1, + }); +}; + +export default useInfiniteMovies; diff --git a/src/hooks/useIntersectionObserver.jsx b/src/hooks/useIntersectionObserver.jsx new file mode 100644 index 00000000..4f5be25e --- /dev/null +++ b/src/hooks/useIntersectionObserver.jsx @@ -0,0 +1,39 @@ +import { useEffect, useRef } from "react"; + +const useIntersectionObserver = ({ onIntersect, enabled = true }) => { + const targetRef = useRef(null); + + useEffect(() => { + //enabled가 false면 observeer 동작 안함 + if (!enabled) return; + + const observer = new IntersectionObserver( + (entries) => { + //요소가 화면에 보이면 + if (entries[0].isIntersecting) { + onIntersect(); + } + }, + { + threshold: 0.1, // 요소의 10%가 보이면 트리거 + rootMargin: "100px", // 100px 전에 미리 로딩 + } + ); + + const currentTarget = targetRef.current; + if (currentTarget) { + observer.observe(currentTarget); + } + + //클린업: 컴포넌트 언마운트 시 observer 해제 + return () => { + if (currentTarget) { + observer.unobserve(currentTarget); + } + }; + }, [onIntersect, enabled]); + + return targetRef; +}; + +export default useIntersectionObserver; diff --git a/src/hooks/useNoOverlay.jsx b/src/hooks/useNoOverlay.jsx new file mode 100644 index 00000000..eb560409 --- /dev/null +++ b/src/hooks/useNoOverlay.jsx @@ -0,0 +1,13 @@ +import { useEffect } from "react"; + +export const useNoOverlay = () => { + useEffect(() => { + document.body.classList.add("no-overlay"); + + return () => { + document.body.classList.remove("no-overlay"); + }; + }, []); +}; + +export default useNoOverlay; diff --git a/src/hooks/useRating.jsx b/src/hooks/useRating.jsx new file mode 100644 index 00000000..20eef9b7 --- /dev/null +++ b/src/hooks/useRating.jsx @@ -0,0 +1,122 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/api/supabase"; +import { useAuth } from "@/hooks"; + +const useRating = (movieId, movieTitle, moviePoster) => { + const { user } = useAuth(); + const [rating, setRating] = useState(0); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + // 사용자의 기존 평점 불러오기 + useEffect(() => { + const fetchRating = async () => { + if (!user || !movieId) { + setLoading(false); + return; + } + + try { + const { data, error } = await supabase + .from("ratings") + .select("rating") + .eq("user_id", user.id) + .eq("movie_id", movieId) + .maybeSingle(); + + if (error) { + console.error("Error fetching rating:", error); + return; + } + + if (data) { + setRating(data.rating); + } + } catch (error) { + console.error("Error:", error); + } finally { + setLoading(false); + } + }; + + fetchRating(); + }, [user, movieId]); + + // 평점 저장/업데이트 + const saveRating = async (newRating) => { + if (!user) { + alert("로그인이 필요합니다."); + return false; + } + + if (newRating < 1 || newRating > 5) { + alert("1~5점 사이의 평점을 선택해주세요."); + return false; + } + + setSaving(true); + + try { + const ratingData = { + user_id: user.id, + movie_id: movieId, + rating: newRating, + movie_title: movieTitle, + movie_poster: moviePoster, + updated_at: new Date().toISOString(), + }; + + // upsert: 존재하면 업데이트, 없으면 삽입 + const { error } = await supabase.from("ratings").upsert(ratingData, { + onConflict: "user_id,movie_id", + }); + + if (error) throw error; + + setRating(newRating); + return true; + } catch (error) { + console.error("Error saving rating:", error); + alert("평점 저장에 실패했습니다."); + return false; + } finally { + setSaving(false); + } + }; + + // 평점 삭제 + const deleteRating = async () => { + if (!user) return false; + + setSaving(true); + + try { + const { error } = await supabase + .from("ratings") + .delete() + .eq("user_id", user.id) + .eq("movie_id", movieId); + + if (error) throw error; + + setRating(0); + return true; + } catch (error) { + console.error("Error deleting rating:", error); + alert("평점 삭제에 실패했습니다."); + return false; + } finally { + setSaving(false); + } + }; + + return { + rating, + loading, + saving, + saveRating, + deleteRating, + }; +}; + +export default useRating; diff --git a/src/hooks/useReview.jsx b/src/hooks/useReview.jsx new file mode 100644 index 00000000..1c6375ae --- /dev/null +++ b/src/hooks/useReview.jsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from "react"; +import { supabase } from "@/api"; +import { useAuth } from "@/hooks"; + +const useReview = (movieId) => { + const { user } = useAuth(); + const [reviews, setReviews] = useState([]); + const [myReview, setMyReview] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + //전체 리뷰 불러오기 + useEffect(() => { + const fetchReviews = async () => { + if (!movieId) return; + + try { + const { data, error } = await supabase + .from("reviews") + .select("*") + .eq("movie_id", movieId) + .order("created_at", { ascending: false }); + + if (error) throw error; + + setReviews(data || []); + + // 내 리뷰 찾기 + if (user) { + const mine = data?.find((r) => r.user_id === user.id); + setMyReview(mine || null); + } + } catch (error) { + console.error("Error fetching reviews:", error); + } finally { + setLoading(false); + } + }; + + fetchReviews(); + }, [movieId, user]); + + //리뷰 저장&수정 + const saveReview = async (reviewText, rating, movieData) => { + if (!user) { + alert("로그인이 필요합니다."); + return false; + } + + if (!reviewText.trim()) { + alert("리뷰 내용을 입력해주세요."); + return false; + } + + setSaving(true); + + try { + const reviewData = { + user_id: user.id, + movie_id: movieData.id, + movie_title: movieData.title, + movie_poster: movieData.poster_path, + rating, + review_text: reviewText, + updated_at: new Date().toISOString(), + }; + + const { data, error } = await supabase + .from("reviews") + .upsert(reviewData, { + onConflict: "user_id,movie_id", + }) + .select() + .maybeSingle(); + + if (error) throw error; + setMyReview(data); + + //리뷰 목록 업데이트 + setReviews((prev) => { + const filtered = prev.filter((r) => r.user_id !== user.id); + return [data, ...filtered]; + }); + return true; + } catch (error) { + console.error("Error saving review:", error); + alert("리뷰 저장에 실패했습니다."); + return false; + } finally { + setSaving(false); + } + }; + + //리뷰 삭제 + const deleteReview = async () => { + if (!user || !myReview) return false; + + setSaving(true); + + try { + const { error } = await supabase + .from("reviews") + .delete() + .eq("user_id", user.id) + .eq("movie_id", movieId); + + if (error) throw error; + + setReviews(null); + setReviews((prev) => prev.filter((r) => r.user_id !== user.id)); + + return true; + } catch (error) { + console.error("Error deleting review:", error); + alert("리뷰 삭제에 실패했습니다."); + return false; + } finally { + setSaving(false); + } + }; + + return { + reviews, + myReview, + loading, + saving, + saveReview, + deleteReview, + }; +}; + +export default useReview; diff --git a/src/hooks/useWishlist.jsx b/src/hooks/useWishlist.jsx new file mode 100644 index 00000000..ea4e1845 --- /dev/null +++ b/src/hooks/useWishlist.jsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from "react"; +import { supabase } from "@/api/supabase"; +import { useAuth } from "@/hooks"; + +const useWishlist = (movieId) => { + const { user } = useAuth(); + const [isWishlisted, setIsWishlisted] = useState(false); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + // 찜 상태 확인 + useEffect(() => { + const checkWishlist = async () => { + if (!user || !movieId) { + setLoading(false); + return; + } + + try { + const { data, error } = await supabase + .from("wishlist") + .select("id") + .eq("user_id", user.id) + .eq("movie_id", movieId) + .maybeSingle(); + + if (error) { + console.error("Error checking wishlist:", error); + return; + } + + setIsWishlisted(!!data); + } catch (error) { + console.error("Error:", error); + } finally { + setLoading(false); + } + }; + + checkWishlist(); + }, [user, movieId]); + + // 찜 추가 + const addToWishlist = async (movieData) => { + if (!user) { + alert("로그인이 필요합니다."); + return false; + } + + setSaving(true); + + try { + const wishlistData = { + user_id: user.id, + movie_id: movieData.id, + movie_title: movieData.title, + movie_poster: movieData.poster_path, + movie_overview: movieData.overview, + movie_release_date: movieData.release_date, + movie_vote_average: movieData.vote_average, + }; + + const { error } = await supabase.from("wishlist").insert(wishlistData); + + if (error) throw error; + + setIsWishlisted(true); + return true; + } catch (error) { + console.error("Error adding to wishlist:", error); + alert("찜 추가에 실패했습니다."); + return false; + } finally { + setSaving(false); + } + }; + + // 찜 제거 + const removeFromWishlist = async () => { + if (!user) return false; + + setSaving(true); + + try { + const { error } = await supabase + .from("wishlist") + .delete() + .eq("user_id", user.id) + .eq("movie_id", movieId); + + if (error) throw error; + + setIsWishlisted(false); + return true; + } catch (error) { + console.error("Error removing from wishlist:", error); + alert("찜 제거에 실패했습니다."); + return false; + } finally { + setSaving(false); + } + }; + + // 토글 (추가 & 제거) + const toggleWishlist = async (movieData) => { + if (isWishlisted) { + return await removeFromWishlist(); + } else { + return await addToWishlist(movieData); + } + }; + + return { + isWishlisted, + loading, + saving, + addToWishlist, + removeFromWishlist, + toggleWishlist, + }; +}; + +export default useWishlist; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 2fb1282e..00000000 --- a/src/index.css +++ /dev/null @@ -1,36 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -img { - width: 100%; - height: 100%; -} - -body { - position: relative; - background: #1a1c20; - color: #fff; - margin: 0; - min-height: 100vh; - font-family: "Pretendard", sans-serif; -} - -body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.5; - background: linear-gradient( - 220deg, - rgba(255, 26, 102, 0.3) 0%, - rgba(26, 28, 32, 0.3) 54% - ); - pointer-events: none; - z-index: 0; -} diff --git a/src/main.jsx b/src/main.jsx index e54c61ce..23d4a7ec 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,5 +1,11 @@ -import { createRoot } from "react-dom/client"; -import "./index.css"; -import App from "./App.jsx"; +import ReactDOM from "react-dom/client"; +import { ThemeProvider } from "@emotion/react"; +import { GlobalStyle, theme } from "@/styles"; +import App from "./App"; -createRoot(document.getElementById("root")).render(); +ReactDOM.createRoot(document.getElementById("root")).render( + + + + +); diff --git a/src/pages/DetailPage/MovieInfo/index.jsx b/src/pages/DetailPage/MovieInfo/index.jsx new file mode 100644 index 00000000..a3f3569a --- /dev/null +++ b/src/pages/DetailPage/MovieInfo/index.jsx @@ -0,0 +1,56 @@ +import { getImageUrl } from "@/constants/images"; +import { + Content, + ContentBox, + Poster, + InfoColumn, + InfoRow, + Label, + Value, + Divider, + Overview, +} from "./style"; + +const MovieInfo = ({ detail, credits }) => { + const director = credits?.crew.find((c) => c.job === "Director")?.name || ""; + const castList = + credits?.cast + .slice(0, 5) + .map((c) => c.name) + .join(", ") || ""; + + if (!detail) { + return
Loading...
; + } + + return ( + + + + + + + {director} + + + + {castList} + + + + + {detail.production_countries.map((c) => c.name).join(", ")} + + + + {detail.overview} + + + + ); +}; + +export default MovieInfo; diff --git a/src/pages/DetailPage/MovieInfo/style.jsx b/src/pages/DetailPage/MovieInfo/style.jsx new file mode 100644 index 00000000..3ce64a8f --- /dev/null +++ b/src/pages/DetailPage/MovieInfo/style.jsx @@ -0,0 +1,117 @@ +import styled from "styled-components"; + +export const Content = styled.div` + display: flex; + margin-top: 68px; + gap: 40px; + padding: 0 225px; + margin-bottom: 172px; + + @media (max-width: 768px) { + margin-top: 40px; + padding: 0 16px; + z-index: 11; + } +`; + +export const ContentBox = styled.div` + display: flex; + gap: 40px; + width: 100%; + + @media (max-width: 768px) { + flex-direction: row; + gap: 16px; + /* ✅ 배경만 투명하게 - ::before 사용 */ + position: relative; + border: 0.4px solid rgba(108, 117, 133); + backdrop-filter: blur(10px); + border-radius: 12px; + padding: 20px; + + /* 배경 레이어 */ + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(-350deg, #ff1a66 0%, #353a42 32%); + opacity: 0.3; /* 배경만 투명 */ + border-radius: 12px; + z-index: -1; /* 뒤로 보내기 */ + } + } +`; + +export const Poster = styled.img` + width: 337px; + height: 496px; + border-radius: 8px; + object-fit: cover; + + @media (max-width: 768px) { + width: 120px; + height: 180px; + flex-shrink: 0; + } +`; + +export const InfoColumn = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; + + @media (max-width: 768px) { + gap: 8px; + } +`; + +export const InfoRow = styled.div` + display: flex; + gap: 12px; + font-size: 14px; + + @media (max-width: 768px) { + flex-direction: row; + align-items: flex-start; + font-size: 12px; + } +`; + +export const Label = styled.span` + font-weight: 700; + min-width: 60px; + + @media (max-width: 768px) { + min-width: 40px; + } +`; + +export const Value = styled.span` + color: rgba(255, 255, 255, 0.8); + flex: 1; +`; + +export const Divider = styled.hr` + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.3); + margin: 20px 0; + + @media (max-width: 768px) { + margin: 12px 0; + } +`; + +export const Overview = styled.p` + line-height: 1.6; + font-size: 14px; + color: rgba(255, 255, 255, 0.8); + + @media (max-width: 768px) { + font-size: 12px; + line-height: 1.5; + } +`; diff --git a/src/pages/DetailPage/MovieOTT/index.jsx b/src/pages/DetailPage/MovieOTT/index.jsx new file mode 100644 index 00000000..b2adc178 --- /dev/null +++ b/src/pages/DetailPage/MovieOTT/index.jsx @@ -0,0 +1,107 @@ +import { useFetchData } from "@/hooks"; +import { getImageUrl } from "@/constants/images"; +import { Typography, Button } from "@/components"; +import { + Content, + ContentBox, + TitleSection, + ProviderList, + ProviderItem, + ProviderInfo, + ProviderLogo, + Divider, + NoProvider, + Attribution, +} from "./style"; + +const MovieOTT = ({ movieId }) => { + const { data: providers, loading } = useFetchData( + `/movie/${movieId}/watch/providers` + ); + + const krStreaming = providers?.results?.KR?.flatrate; + + const handleWatch = (providerName) => { + alert(`${providerName}에서 시청하기`); + }; + + if (loading) { + return ( + + OTT 정보 불러오는 중... + + ); + } + + return ( + + + + 어디서 볼 수 있나요? + + + {!krStreaming || krStreaming.length === 0 ? ( + + + 현재 국내 OTT 서비스에서 제공되지 않습니다. + + + ) : ( + + {krStreaming.map((provider, index) => ( + + + + + {provider.provider_name} + + + + {/* 🔥 기존 Button 컴포넌트 사용 */} + + + {index < krStreaming.length - 1 && } + + ))} + + )} + + {krStreaming && krStreaming.length > 0 && ( + + + OTT 정보 제공: JustWatch + + + )} + + + ); +}; +export default MovieOTT; diff --git a/src/pages/DetailPage/MovieOTT/style.jsx b/src/pages/DetailPage/MovieOTT/style.jsx new file mode 100644 index 00000000..2ed5e86a --- /dev/null +++ b/src/pages/DetailPage/MovieOTT/style.jsx @@ -0,0 +1,89 @@ +import styled from "@emotion/styled"; + +export const Content = styled.div` + display: flex; + margin-top: 68px; + padding: 0 225px; + margin-bottom: 172px; + + @media (max-width: 768px) { + margin-top: 40px; + padding: 0 16px; + } +`; + +export const ContentBox = styled.div` + width: 100%; +`; + +export const TitleSection = styled.div` + margin-bottom: 40px; + + @media (max-width: 768px) { + margin-bottom: 24px; + } +`; + +export const ProviderList = styled.div` + display: flex; + flex-direction: column; +`; + +export const ProviderItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 24px 0; + position: relative; + + @media (max-width: 1200px) { + flex-direction: column; + align-items: flex-start; + gap: 16px; + } +`; + +export const ProviderInfo = styled.div` + display: flex; + align-items: center; + flex: 1; +`; + +export const ProviderLogo = styled.img` + width: 60px; + height: 60px; + border-radius: 8px; + object-fit: cover; + + @media (max-width: 768px) { + width: 50px; + height: 50px; + } +`; + +export const Divider = styled.div` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: rgba(255, 255, 255, 0.1); +`; + +export const NoProvider = styled.div` + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 48px; + text-align: center; + + @media (max-width: 768px) { + padding: 32px 16px; + } +`; + +export const Attribution = styled.div` + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +`; diff --git a/src/pages/DetailPage/MovieRating/index.jsx b/src/pages/DetailPage/MovieRating/index.jsx new file mode 100644 index 00000000..5047010c --- /dev/null +++ b/src/pages/DetailPage/MovieRating/index.jsx @@ -0,0 +1,130 @@ +import { useRating, useAuth } from "@/hooks"; +import { useNavigate } from "react-router-dom"; +import { Typography, StarRating } from "@/components"; +import { + Content, + ContentBox, + TitleSection, + RatingSelectSection, + LoginPrompt, + LoginLink, + RatingInfoGrid, + InfoBox, + AverageBox, + DeleteButton, +} from "./style"; + +const MovieRating = ({ detail, movieId }) => { + const navigate = useNavigate(); + const { user } = useAuth(); + const { rating, loading, saving, saveRating, deleteRating } = useRating( + movieId, + detail.title, + detail.poster_path + ); + + // TMDB 평점을 5점 만점으로 변환 + const tmdbRating = detail.vote_average + ? Math.round(detail.vote_average / 2) + : 0; + + const handleStarClick = async (newRating) => { + if (!user) { + alert("로그인이 필요한 서비스입니다."); + navigate("/login"); + return; + } + + const success = await saveRating(newRating); + if (success) { + alert("평점이 저장되었습니다!"); + } + }; + + const handleDeleteRating = async () => { + if (window.confirm("평점을 삭제하시겠습니까?")) { + const success = await deleteRating(); + if (success) { + alert("평점이 삭제되었습니다."); + } + } + }; + + const getRatingMessage = () => { + if (rating > 0) return `내가 준 평점: ${rating * 2}점`; + if (user) return "아직 평점을 주지 않았습니다"; + return "로그인 후 평점을 남겨보세요"; + }; + + if (loading) { + return ( + + + 평점 불러오는 중... + + + ); + } + + return ( + + + + 별점을 선택해주세요. + + + {/* 상단: 별점 선택 섹션 */} + + + + {!user && ( + + navigate("/login")}>로그인 + + 하고 평점을 남겨보세요! + + + )} + + + {/* 하단: 평점 정보 박스 2개 */} + + {/* 실관람객 평점 (TMDB) */} + + 실관람객평점 + + + {detail.vote_average?.toFixed(1) || 0} / 10점 + + + + + + {/* 내 평점 */} + + 내 평점 + + {getRatingMessage()} + + + + {rating > 0 && user && ( + + + {saving ? "처리중..." : "평점 삭제"} + + + )} + + + + + ); +}; + +export default MovieRating; diff --git a/src/pages/DetailPage/MovieRating/style.jsx b/src/pages/DetailPage/MovieRating/style.jsx new file mode 100644 index 00000000..6899c779 --- /dev/null +++ b/src/pages/DetailPage/MovieRating/style.jsx @@ -0,0 +1,259 @@ +import styled from "@emotion/styled"; + +export const Content = styled.div` + display: flex; + justify-content: center; + margin-top: 68px; + margin-bottom: 172px; + padding: 0 80px; + + @media (max-width: 768px) { + margin-top: 40px; + padding: 0 16px; + margin-bottom: 80px; + } +`; + +export const ContentBox = styled.div` + width: 100%; + max-width: 1200px; + display: flex; + flex-direction: column; + gap: 60px; + + @media (max-width: 768px) { + gap: 32px; + } +`; + +export const TitleSection = styled.div` + display: flex; + justify-content: center; + align-content: center; +`; + +export const RatingSelectSection = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + + @media (max-width: 768px) { + padding: 20px 0; + gap: 24px; + } +`; + +export const StarContainer = styled.div` + display: flex; + gap: 12px; + + svg { + font-size: 60px !important; + width: 60px; + height: 60px; + } + + @media (max-width: 768px) { + gap: 8px; + + svg { + font-size: 30px !important; + width: 20px; + height: 20px; + } + } +`; + +export const StarButton = styled.button` + background: transparent; + border: none; + cursor: pointer; + padding: 0; + transition: transform 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + + &:hover:not(:disabled) { + transform: scale(1.15); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } +`; + +export const LoginPrompt = styled.div` + display: flex; + align-items: center; + gap: 4px; +`; + +export const LoginLink = styled.span` + color: ${(props) => props.theme?.colors?.accent || "#ff1a66"}; + cursor: pointer; + font-weight: 600; + text-decoration: underline; + font-size: 16px; + + &:hover { + color: #e01558; + } +`; + +export const RatingInfoGrid = styled.div` + display: flex; + justify-content: center; + gap: 32px; + flex-wrap: wrap; + + @media (max-width: 1248px) { + gap: 20px; + } + + @media (max-width: 768px) { + gap: 16px; + } + + @media (max-width: 600px) { + gap: 12px; + } +`; + +export const InfoBox = styled.div` + width: 489px; + max-width: 100%; + min-height: 400px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 40px; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + gap: 24px; + + &::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(13deg, #ff1a66 0%, #353a42 32%); + opacity: 0.3; + border-radius: 12px; + z-index: -1; + } + + @media (max-width: 1024px) { + width: 45%; + min-width: 320px; + padding: 32px 24px; + } + + @media (max-width: 768px) { + width: 48%; + min-width: 280px; + min-height: 350px; + padding: 24px 16px; + gap: 20px; + } + + @media (max-width: 600px) { + width: 48%; + min-width: 160px; + padding: 20px 12px; + min-height: 320px; + gap: 16px; + } +`; + +export const AverageBox = styled.div` + width: 100%; + height: 160px; + background-color: #1a1c20; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0 20px; + text-align: center; + + @media (max-width: 1024px) { + height: 140px; + } + + @media (max-width: 768px) { + height: 120px; + padding: 0 12px; + } + + @media (max-width: 600px) { + height: 100px; + padding: 0 8px; + } +`; + +export const StarsDisplay = styled.div` + display: flex; + gap: 8px; + justify-content: flex-start; + margin-top: 20px; + + svg { + font-size: 28px !important; + width: 28px; + height: 28px; + } + + @media (max-width: 768px) { + gap: 4px; + + svg { + font-size: 18px !important; + width: 18px; + height: 18px; + } + } + + @media (max-width: 600px) { + gap: 3px; + } +`; + +export const DeleteButton = styled.button` + padding: 12px 24px; + background: rgba(255, 255, 255, 0.1); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + align-self: flex-end; + + &:hover:not(:disabled) { + background: rgba(255, 26, 102, 0.2); + border-color: ${(props) => props.theme?.colors?.accent || "#ff1a66"}; + color: ${(props) => props.theme?.colors?.accent || "#ff1a66"}; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + @media (max-width: 768px) { + padding: 10px 16px; + font-size: 14px; + } + + @media (max-width: 600px) { + padding: 8px 12px; + font-size: 13px; + } +`; diff --git a/src/pages/DetailPage/index.jsx b/src/pages/DetailPage/index.jsx index f9f297e4..ae086614 100644 --- a/src/pages/DetailPage/index.jsx +++ b/src/pages/DetailPage/index.jsx @@ -1,340 +1,139 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { useParams } from "react-router-dom"; -import { useFetchData } from "@hook/useFetchData"; -import styled from "styled-components"; +import { getImageUrl } from "@/constants/images"; +import { useAuth, useWishlist, useFetchData, useNoOverlay } from "@/hooks"; +import { Typography, Icon } from "@/components"; +import { DETAIL_PAGE_TABS } from "./tabConfig"; +import { + Container, + Banner, + TitleWrapper, + MovieTitle, + MovieInfo as MovieInfoStyle, + TabsWrapper, + Tabs, + Tab, + WishlistButton, + CenteredMessage, +} from "./style"; const Details = () => { - let { id } = useParams(); - - const { data: detail, loading: detailLoading } = useFetchData(`/movie/${id}`); - const { data: credits, loading: creditsLoading } = useFetchData( - `/movie/${id}/credits` - ); - + useNoOverlay(); + const { id } = useParams(); const [activeTab, setActiveTab] = useState("info"); + const { user } = useAuth(); + + const { + data: detail, + loading: detailLoading, + error: detailError, + } = useFetchData(`/movie/${id}`); + const { + data: credits, + loading: creditsLoading, + error: creditsError, + } = useFetchData(`/movie/${id}/credits`); + const { isWishlisted, saving, toggleWishlist } = useWishlist(id); + + const handleWishlistClick = async () => { + if (!user) return alert("로그인이 필요합니다."); + + const success = await toggleWishlist(detail); + if (success) { + alert( + isWishlisted + ? "찜 목록에서 제거되었습니다." + : "찜 목록에 추가되었습니다!" + ); + } + }; + + // 로딩/에러 상태 + const loading = detailLoading || creditsLoading; + const error = detailError || creditsError; + + const getErrorMessage = () => { + if (loading) return "로딩 중..."; + if (error) return "오류가 발생했습니다."; + return "영화를 찾을 수 없습니다."; + }; + + if (loading || error || !detail) { + return ( + + + {getErrorMessage()} + + + ); + } - if (detailLoading || creditsLoading) return
Loading...
; - if (!detail) return
영화를 찾을 수 없습니다.
; - - const director = credits?.crew.find((c) => c.job === "Director")?.name || ""; - const castList = - credits?.cast - .slice(0, 5) - .map((c) => c.name) - .join(", ") || ""; + const ActiveComponent = DETAIL_PAGE_TABS[activeTab].component; return ( - + - {detail.title} - - {detail.release_date} - .{detail.runtime}분 - .{detail.genres.map((g) => g.name).join(", ")} - - .{detail.production_countries.map((c) => c.name).join(", ")} - - +
+ {detail.title} + + + +
+ + + + {detail.release_date || "날짜 정보 없음"} + + {detail.runtime && ( + + .{detail.runtime}분 + + )} + {detail.genres?.length > 0 && ( + + .{detail.genres.map((g) => g.name).join(", ")} + + )} + {detail.production_countries?.length > 0 && ( + + .{detail.production_countries.map((c) => c.name).join(", ")} + + )} +
- setActiveTab("info")} - > - 기본정보 - - setActiveTab("rating")} - > - 평점 - - setActiveTab("ott")}> - OTT 정보 - - setActiveTab("etc")}> - 기타 - + {Object.entries(DETAIL_PAGE_TABS).map(([key, { label }]) => ( + setActiveTab(key)} + > + {label} + + ))} - {activeTab === "info" && ( - - - - - - - {detail.genres.map((g) => g.name).join(", ")} - - - - {director} - - - - {castList} - - - - - {detail.production_countries.map((c) => c.name).join(", ")} - - - - {detail.overview} - - - - )} +
); }; export default Details; -export const Container = styled.div` - width: 100%; - max-width: 100%; - margin: 0 auto; - color: white; - font-family: "Noto Sans KR", sans-serif; -`; - -export const Banner = styled.div` - width: 100vw; - height: 717px; - background: url(${(props) => props.bg}) center/cover no-repeat; - background-position: center top; - position: relative; - overflow: hidden; - - /* 모바일 배경 블러 효과 */ - @media (max-width: 768px) { - height: 467px; - margin-top: 100px; - - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: url(${(props) => props.bg}) center/cover no-repeat; - opacity: 0.2; - z-index: 0; - } - } -`; - -// export const BannerInfo = styled.div` -// display: flex; -// flex-direction: column; -// gap: 4px; -// font-size: 14px; -// `; - -export const TitleWrapper = styled.div` - position: absolute; - bottom: 144px; // 바텀에서 164px - left: 220px; // 왼쪽에서 220px - display: flex; - flex-direction: column; - gap: 8px; - - @media (max-width: 768px) { - left: 16px; - bottom: 20px; - z-index: 1; - } -`; - -export const MovieTitle = styled.h1` - font-size: 72px; - font-weight: 700; - margin: 0; - - @media (max-width: 768px) { - font-size: 24px; - font-weight: 700; - } -`; - -export const MovieInfo = styled.div` - font-size: 24px; - font-weight: 400; - color: rgba(255, 255, 255, 0.85); - display: flex; - gap: 24px; // info 항목 간격 - - @media (max-width: 768px) { - display: none; /* 모바일에서 숨김 */ - } -`; - -export const TabsWrapper = styled.div` - display: flex; - justify-content: flex-start; - margin-top: 99px; - padding-left: 225px; - - @media (max-width: 768px) { - margin-top: 40px; - padding: 0; - justify-content: center; - } -`; - -export const Tabs = styled.div` - display: flex; - gap: 8px; - - @media (max-width: 768px) { - max-width: 294px; - gap: 4px; - } -`; - -export const Tab = styled.div` - padding: 10px 20px; - cursor: pointer; - color: ${(props) => (props.active ? "#fff" : "rgba(255,255,255,0.6)")}; - font-weight: ${(props) => (props.active ? "700" : "400")}; - border-bottom: ${(props) => (props.active ? "2px solid #fff" : "none")}; - transition: color 0.2s, border-bottom 0.2s; - - @media (max-width: 768px) { - width: 74px; - height: 32px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 16px; - border: none; - background-color: ${(props) => (props.active ? "#ff1a66" : "transparent")}; - font-size: 14px; - } -`; - -export const Content = styled.div` - display: flex; - margin-top: 68px; - gap: 40px; - padding: 0 225px; - - @media (max-width: 768px) { - margin-top: 40px; - padding: 0 16px; - z-index: 11; - } -`; - -export const ContentBox = styled.div` - display: flex; - gap: 40px; - width: 100%; - - @media (max-width: 768px) { - flex-direction: row; - gap: 16px; - /* ✅ 배경만 투명하게 - ::before 사용 */ - position: relative; - border: 0.4px solid rgba(108, 117, 133); - backdrop-filter: blur(10px); - border-radius: 12px; - padding: 20px; - - /* 배경 레이어 */ - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(-350deg, #ff1a66 0%, #353a42 32%); - opacity: 0.3; /* 배경만 투명 */ - border-radius: 12px; - z-index: -1; /* 뒤로 보내기 */ - } - } -`; - -export const Poster = styled.img` - width: 337px; - height: 496px; - border-radius: 8px; - object-fit: cover; - - @media (max-width: 768px) { - width: 120px; - height: 180px; - flex-shrink: 0; - } -`; - -export const InfoColumn = styled.div` - display: flex; - flex-direction: column; - gap: 12px; - flex: 1; - - @media (max-width: 768px) { - gap: 8px; - } -`; - -export const InfoRow = styled.div` - display: flex; - gap: 12px; - font-size: 14px; - - @media (max-width: 768px) { - flex-direction: row; - align-items: flex-start; - font-size: 12px; - } -`; - -export const Label = styled.span` - font-weight: 700; - min-width: 60px; - - @media (max-width: 768px) { - min-width: 40px; - } -`; - -export const Value = styled.span` - color: rgba(255, 255, 255, 0.8); - flex: 1; -`; - -export const Divider = styled.hr` - border: 0; - border-top: 1px solid rgba(255, 255, 255, 0.3); - margin: 20px 0; - - @media (max-width: 768px) { - margin: 12px 0; - } -`; - -export const Overview = styled.p` - line-height: 1.6; - font-size: 14px; - color: rgba(255, 255, 255, 0.8); - - @media (max-width: 768px) { - font-size: 12px; - line-height: 1.5; - } -`; diff --git a/src/pages/DetailPage/style.jsx b/src/pages/DetailPage/style.jsx new file mode 100644 index 00000000..223dde82 --- /dev/null +++ b/src/pages/DetailPage/style.jsx @@ -0,0 +1,154 @@ +import styled from "styled-components"; + +export const Container = styled.div` + width: 100%; + max-width: 100%; + margin: 0 auto; + color: white; + font-family: "Noto Sans KR", sans-serif; +`; + +export const Banner = styled.div` + width: 100vw; + height: 717px; + background: url(${(props) => props.$bg}) center/cover no-repeat; + background-position: center top; + position: relative; + overflow: hidden; + + /* 모바일 배경 블러 효과 */ + @media (max-width: 768px) { + height: 467px; + margin-top: 100px; + + &::before { + content: ""; + position: absolute; + inset: 0; + background: url(${(props) => props.$bg}) center/cover no-repeat; + opacity: 0.2; + z-index: 0; + } + } +`; +export const TitleWrapper = styled.div` + position: absolute; + bottom: 144px; + left: 220px; + display: flex; + flex-direction: column; + gap: 8px; + + @media (max-width: 768px) { + left: 16px; + bottom: 20px; + z-index: 1; + } +`; + +export const MovieTitle = styled.h1` + font-size: 72px; + font-weight: 700; + margin: 0; + + @media (max-width: 768px) { + font-size: 24px; + font-weight: 700; + } +`; + +export const MovieInfo = styled.div` + font-size: 24px; + font-weight: 400; + color: rgba(255, 255, 255, 0.85); + display: flex; + gap: 24px; + + @media (max-width: 768px) { + display: none; /* 모바일에서 숨김 */ + } +`; + +export const TabsWrapper = styled.div` + display: flex; + justify-content: flex-start; + margin-top: 99px; + padding-left: 225px; + + @media (max-width: 768px) { + margin-top: 40px; + padding: 0; + justify-content: flex-start; + padding-left: 20px; + } +`; + +export const Tabs = styled.div` + display: flex; + gap: 8px; + + @media (max-width: 768px) { + max-width: 294px; + gap: 4px; + } +`; + +export const Tab = styled.div` + padding: 10px 20px; + cursor: pointer; + color: ${(props) => (props.$active ? "#fff" : "rgba(255,255,255,0.6)")}; + font-weight: ${(props) => (props.$active ? "700" : "400")}; + border-bottom: ${(props) => (props.$active ? "2px solid #fff" : "none")}; + transition: color 0.2s, border-bottom 0.2s; + + @media (max-width: 768px) { + flex: 1; + padding: 10px 12px; + text-align: center; + font-size: 14px; + border-radius: 20px; + border: 1px solid + ${(props) => (props.$active ? "#ff1a66" : "rgba(255, 255, 255, 0.3)")}; + background-color: ${(props) => (props.$active ? "#ff1a66" : "transparent")}; + white-space: nowrap; + border-bottom: none; + color: ${(props) => (props.$active ? "#fff" : "rgba(255,255,255,0.6)")}; + font-weight: ${(props) => (props.$active ? "600" : "400")}; + } +`; + +export const WishlistButton = styled.button` + background: transparent; + border: none; + padding: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + + &:hover:not(:disabled) { + transform: scale(1.1); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + @media (max-width: 768px) { + width: 40px; + height: 40px; + + svg { + font-size: 20px; + } + } +`; + +export const CenteredMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; +`; diff --git a/src/pages/DetailPage/tabConfig.jsx b/src/pages/DetailPage/tabConfig.jsx new file mode 100644 index 00000000..51f0adc7 --- /dev/null +++ b/src/pages/DetailPage/tabConfig.jsx @@ -0,0 +1,9 @@ +import MovieInfo from "@/pages/DetailPage/MovieInfo"; +import MovieRating from "@/pages/DetailPage/MovieRating"; +import MovieOTT from "@/pages/DetailPage/MovieOTT"; + +export const DETAIL_PAGE_TABS = { + info: { label: "기본정보", component: MovieInfo }, + rating: { label: "평점", component: MovieRating }, + ott: { label: "OTT 정보", component: MovieOTT }, +}; diff --git a/src/pages/Layout/index.jsx b/src/pages/Layout/index.jsx index 66501300..4b19c74b 100644 --- a/src/pages/Layout/index.jsx +++ b/src/pages/Layout/index.jsx @@ -1,5 +1,4 @@ -import React from "react"; -import Header from "@components/Header/Header"; +import Header from "@components/Header"; import { Outlet } from "react-router-dom"; const Layout = () => { diff --git a/src/pages/LoginPage/index.jsx b/src/pages/LoginPage/index.jsx index 6b387210..13b28886 100644 --- a/src/pages/LoginPage/index.jsx +++ b/src/pages/LoginPage/index.jsx @@ -9,7 +9,7 @@ import { ErrorMessage, Divider, SocialButtons, -} from "./LoginStyle"; +} from "./style"; export default function LoginPage() { const navigate = useNavigate(); diff --git a/src/pages/LoginPage/LoginStyle.jsx b/src/pages/LoginPage/style.jsx similarity index 100% rename from src/pages/LoginPage/LoginStyle.jsx rename to src/pages/LoginPage/style.jsx diff --git a/src/pages/MainPage/index.jsx b/src/pages/MainPage/index.jsx index 6663befb..693da5d6 100644 --- a/src/pages/MainPage/index.jsx +++ b/src/pages/MainPage/index.jsx @@ -1,6 +1,6 @@ -import React from "react"; -import Banner from "@/components/Banner/Banner"; -import PopularMovies from "@components/PopularMovies"; +import Header from "@/components/Header"; +import Banner from "@/components/Banner"; +import PopularMovies from "@/components/PopularMovies"; import TopRankedMovie from "@/components/TopRankedMovie"; const MainPage = () => { diff --git a/src/pages/ProfilePage/index.jsx b/src/pages/ProfilePage/index.jsx index d1e50d0b..f47d2f33 100644 --- a/src/pages/ProfilePage/index.jsx +++ b/src/pages/ProfilePage/index.jsx @@ -1,5 +1,3 @@ -import React from "react"; - const ProfilePage = () => { return
; }; diff --git a/src/pages/ProfilePage/style.jsx b/src/pages/ProfilePage/style.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/SearchPage/SearchEmpty.jsx b/src/pages/SearchPage/SearchEmpty.jsx new file mode 100644 index 00000000..2a8df427 --- /dev/null +++ b/src/pages/SearchPage/SearchEmpty.jsx @@ -0,0 +1,39 @@ +import { Typography } from "@/components"; +import { EmptyState } from "./style"; + +const SearchEmpty = ({ type, searchTerm }) => { + const content = { + loading: { + title: "검색 중...", + description: null, + }, + empty: { + title: "영화를 검색해보세요.", + description: "제목, 장르, 감독 등으로 검색할 수 있습니다.", + }, + noResults: { + title: `"${searchTerm}"에 검색 결과가 없습니다.`, + description: "다른 검색어로 시도해보세요.", + }, + }; + + const { title, description } = content[type]; + + return ( + + + {title} + + {description && ( + + {description} + + )} + + ); +}; + +export default SearchEmpty; diff --git a/src/pages/SearchPage/index.jsx b/src/pages/SearchPage/index.jsx index 94a126dc..f98c376d 100644 --- a/src/pages/SearchPage/index.jsx +++ b/src/pages/SearchPage/index.jsx @@ -1,13 +1,10 @@ -import React from "react"; -import { useLocation, useNavigate } from "react-router-dom"; -import { useFetchData } from "@hook/useFetchData"; -import { useDebounce } from "@/hook/useDebounce"; -import styled from "styled-components"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faFilter } from "@fortawesome/free-solid-svg-icons"; +import { useLocation } from "react-router-dom"; +import { useFetchData, useDebounce } from "@/hooks"; +import { PageContainer, MovieGrid, Typography } from "@/components"; +import { SearchContainer } from "./style"; +import SearchEmpty from "./SearchEmpty"; const SearchPage = () => { - const navigate = useNavigate(); const location = useLocation(); const searchParams = new URLSearchParams(location.search); @@ -19,148 +16,40 @@ const SearchPage = () => { : null; const { data, loading } = useFetchData(endpoint); - const results = data?.results || []; + const results = data?.results?.filter((movie) => movie.poster_path) || []; - const handleClick = (id) => { - navigate(`/movie/${id}`); - }; - - if (loading) - return ( - - Loading... - - ); - return ( - - - - 🔍 "{searchTerm}" 검색 결과 ({results.length}건) - - - - - - - {results.length > 0 ? ( - - {results.map((movie) => ( - handleClick(movie.id)}> - {movie.title} -

{movie.title}

-
- ))} -
- ) : ( - 검색 결과가 없습니다. - )} -
- ); -}; - -export default SearchPage; - -// 스타일 정의 -const Container = styled.div` - padding: 140px 80px; - min-height: 100vh; - color: #fff; - - @media (max-width: 1240px) { - padding: 120px 60px; - } - - @media (max-width: 960px) { - padding: 120px 40px; - margin-top: 40px; - } - - @media (max-width: 768px) { - padding: 120px 16px; - margin-top: 100px; - } -`; - -const TopSection = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; - - @media (max-width: 768px) { - margin-bottom: 20px; - } -`; - -const Title = styled.h2` - font-size: 24px; - margin-bottom: 24px; -`; - -const FilterButton = styled.button` - display: none; -`; - -const MovieGrid = styled.div` - display: grid; - grid-template-columns: repeat(6, 1fr); - gap: 20px; - - @media (max-width: 1240px) { - grid-template-columns: repeat(5, 1fr); - gap: 16px; - } - - @media (max-width: 960px) { - grid-template-columns: repeat(4, 1fr); - } - - @media (max-width: 768px) { - grid-template-columns: repeat(2, 1fr); - gap: 12px; - } -`; - -const MovieCard = styled.div` - cursor: pointer; - text-align: center; - display: flex; - flex-direction: column; + const renderEmptyState = () => { + if (loading) { + return ; + } - img { - width: 100%; - border-radius: 8px; - margin-bottom: 12px; - transition: transform 0.3s ease; - display: block; - } + if (!searchTerm || searchTerm.trim() === "") { + return ; + } - &:hover img { - transform: scale(1.05); - } + if (results.length === 0) { + return ; + } + return null; + }; - h3 { - font-size: 16px; - font-weight: 500; + const emptyState = renderEmptyState(); - @media (max-width: 768px) { - font-size: 14px; - } + if (emptyState) { + return {emptyState}; } -`; -const Message = styled.p` - font-size: 18px; - color: #d9d9d9; + return ( + + + "{searchTerm}" 검색 결과 ({results.length}) + + + + ); +}; - @media (max-width: 768px) { - font-size: 16px; - } -`; +export default SearchPage; diff --git a/src/pages/SearchPage/style.jsx b/src/pages/SearchPage/style.jsx new file mode 100644 index 00000000..363808c3 --- /dev/null +++ b/src/pages/SearchPage/style.jsx @@ -0,0 +1,20 @@ +import styled from "@emotion/styled"; + +export const SearchContainer = styled.div` + padding-top: 140px; + min-height: 100vh; + + @media (max-width: 768px) { + padding-top: 100px; + } +`; + +export const EmptyState = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + text-align: center; + padding: 40px 20px; +`; diff --git a/src/pages/SignupPage/index.jsx b/src/pages/SignupPage/index.jsx index a63e43e2..eed4aa23 100644 --- a/src/pages/SignupPage/index.jsx +++ b/src/pages/SignupPage/index.jsx @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ -import React, { useState } from "react"; +import { useState } from "react"; import { useNavigate, Link } from "react-router-dom"; -import { supabase } from "@/api/supabase"; +import { supabase } from "@/api"; import { PageWrapper, SignupContainer, @@ -11,7 +11,7 @@ import { ErrorMessage, SuccessMessage, Divider, -} from "./SignupStyle"; +} from "./style"; export default function SignupPage() { const navigate = useNavigate(); diff --git a/src/pages/SignupPage/SignupStyle.jsx b/src/pages/SignupPage/style.jsx similarity index 100% rename from src/pages/SignupPage/SignupStyle.jsx rename to src/pages/SignupPage/style.jsx diff --git a/src/pages/index.jsx b/src/pages/index.jsx new file mode 100644 index 00000000..3a0cc067 --- /dev/null +++ b/src/pages/index.jsx @@ -0,0 +1,7 @@ +export { default as DetailPage } from "./DetailPage"; +export { default as Layout } from "./Layout"; +export { default as LoginPage } from "./LoginPage"; +export { default as MainPage } from "./MainPage"; +export { default as ProfilePage } from "./ProfilePage"; +export { default as SearchPage } from "./SearchPage"; +export { default as SignupPage } from "./SignupPage"; diff --git a/src/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx new file mode 100644 index 00000000..f121ab68 --- /dev/null +++ b/src/styles/GlobalStyle.jsx @@ -0,0 +1,49 @@ +import { Global, css, useTheme } from "@emotion/react"; + +const GlobalStyle = () => { + const theme = useTheme(); + + return ( + + ); +}; +export default GlobalStyle; diff --git a/src/styles/index.jsx b/src/styles/index.jsx new file mode 100644 index 00000000..191851b6 --- /dev/null +++ b/src/styles/index.jsx @@ -0,0 +1,3 @@ +/* eslint-disable react-refresh/only-export-components */ +export { default as theme } from "./theme"; +export { default as GlobalStyle } from "./GlobalStyle"; diff --git a/src/styles/theme.jsx b/src/styles/theme.jsx new file mode 100644 index 00000000..c42a66c6 --- /dev/null +++ b/src/styles/theme.jsx @@ -0,0 +1,16 @@ +const theme = { + colors: { + bg: "#1a1c20", + text: "#fff", + accent: "#ff1a66", + disabled: "rgba(255, 255, 255, 0.3)", + sectionGradient: "linear-gradient(-350deg, #ff1a66 0%, #353a42 32%)", + overlay: + "linear-gradient(220deg, rgba(255, 26, 102, 0.3) 0%, rgba(26, 28, 32, 0.3) 54%)", + }, + font: { + family: `"Pretendard", sans-serif`, + }, +}; + +export default theme; diff --git a/src/utils/text.jsx b/src/utils/text.jsx new file mode 100644 index 00000000..61835b08 --- /dev/null +++ b/src/utils/text.jsx @@ -0,0 +1,7 @@ +// 20자 초과 시 '...'으로 표시되게 +export const truncateText = (text, wordLimit) => { + if (!text) return ""; + const words = text.split(" "); + if (words.length <= wordLimit) return text; + return words.slice(0, wordLimit).join(" ") + "..."; +};