From b6e32ded7bdd328a2863a7ef85044d111886602a Mon Sep 17 00:00:00 2001 From: Hossein Date: Sun, 3 May 2026 19:51:38 +0330 Subject: [PATCH] add packages --- docker-compose.yml | 20 + frontend/.dockerignore | 8 + frontend/.gitignore | 41 ++ frontend/.npmrc | 4 + frontend/AGENTS.md | 5 + frontend/CLAUDE.md | 1 + frontend/Dockerfile | 28 + frontend/README.md | 36 + frontend/next.config.ts | 7 + frontend/package-lock.json | 976 ++++++++++++++++++++++++++ frontend/package.json | 21 + frontend/public/file.svg | 1 + frontend/public/globe.svg | 1 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/public/window.svg | 1 + frontend/src/app/[lang]/landing.css | 152 ++++ frontend/src/app/[lang]/layout.tsx | 45 ++ frontend/src/app/[lang]/page.tsx | 115 +++ frontend/src/app/favicon.ico | Bin 0 -> 25931 bytes frontend/src/app/globals.css | 550 +++++++++++++++ frontend/src/lib/api.ts | 52 ++ frontend/src/lib/dictionaries/en.json | 24 + frontend/src/lib/dictionaries/fa.json | 24 + frontend/src/lib/i18n.ts | 20 + frontend/src/proxy.ts | 30 + frontend/tsconfig.json | 34 + 27 files changed, 2198 insertions(+) create mode 100644 frontend/.dockerignore create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/AGENTS.md create mode 100644 frontend/CLAUDE.md create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/next.config.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/file.svg create mode 100644 frontend/public/globe.svg create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/public/window.svg create mode 100644 frontend/src/app/[lang]/landing.css create mode 100644 frontend/src/app/[lang]/layout.tsx create mode 100644 frontend/src/app/[lang]/page.tsx create mode 100644 frontend/src/app/favicon.ico create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/dictionaries/en.json create mode 100644 frontend/src/lib/dictionaries/fa.json create mode 100644 frontend/src/lib/i18n.ts create mode 100644 frontend/src/proxy.ts create mode 100644 frontend/tsconfig.json diff --git a/docker-compose.yml b/docker-compose.yml index d68f70c..e0e2d75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,5 +29,25 @@ services: db: condition: service_healthy + frontend: + build: + context: ./frontend + args: + HTTP_PROXY: ${HTTP_PROXY:-} + HTTPS_PROXY: ${HTTPS_PROXY:-} + # Browser-side: NEXT_PUBLIC_API_URL is what the user's browser hits — must be host-reachable. + # Server-side: INTERNAL_API_URL is what the Next.js server hits when SSR fetches data — uses the compose network. + environment: + NEXT_PUBLIC_API_URL: http://localhost:8000 + INTERNAL_API_URL: http://web:8000 + volumes: + - ./frontend:/app + - /app/node_modules + - /app/.next + ports: + - "3000:3000" + depends_on: + - web + volumes: postgres_data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..eb5b285 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.next +.env.local +.git +*.log +README.md +AGENTS.md +CLAUDE.md diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..18bad35 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,4 @@ +fetch-timeout=600000 +fetch-retries=5 +fetch-retry-mintimeout=20000 +fetch-retry-maxtimeout=120000 diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..ebf340d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,28 @@ +FROM node:22-alpine + +ENV NEXT_TELEMETRY_DISABLED=1 + +WORKDIR /app + +# Build-time proxy. Compose passes these via build args. +# Active only during `npm install` below; cleared again before runtime. +ARG HTTP_PROXY= +ARG HTTPS_PROXY= +ENV HTTP_PROXY=${HTTP_PROXY} \ + HTTPS_PROXY=${HTTPS_PROXY} \ + http_proxy=${HTTP_PROXY} \ + https_proxy=${HTTPS_PROXY} + +COPY .npmrc package.json package-lock.json ./ +# `npm install` (not `npm ci`) is forgiving when the lockfile was generated +# against a different registry (e.g. npmjs) than the one in .npmrc. +RUN npm install --no-audit --no-fund + +# Drop the proxy from the runtime image — only the install step needed it. +ENV HTTP_PROXY= HTTPS_PROXY= http_proxy= https_proxy= + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "dev", "--", "-H", "0.0.0.0"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ac5728c --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,976 @@ +{ + "name": "frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.1.0", + "dependencies": { + "next": "16.2.4", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", + "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", + "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", + "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", + "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", + "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", + "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", + "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", + "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", + "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", + "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.4", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.4", + "@next/swc-darwin-x64": "16.2.4", + "@next/swc-linux-arm64-gnu": "16.2.4", + "@next/swc-linux-arm64-musl": "16.2.4", + "@next/swc-linux-x64-gnu": "16.2.4", + "@next/swc-linux-x64-musl": "16.2.4", + "@next/swc-win32-arm64-msvc": "16.2.4", + "@next/swc-win32-x64-msvc": "16.2.4", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e99b4a6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "16.2.4", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/frontend/public/file.svg b/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/[lang]/landing.css b/frontend/src/app/[lang]/landing.css new file mode 100644 index 0000000..683f173 --- /dev/null +++ b/frontend/src/app/[lang]/landing.css @@ -0,0 +1,152 @@ +/* Landing page styles — extracted from design/Ilo Landing.html */ +:root { --max-w: 1200px; } + +.landing { + background: var(--surface); + color: var(--ink); + overflow-x: hidden; +} + +/* — top nav — */ +.nav { + position: sticky; top: 0; z-index: 50; + background: rgba(255,255,255,.85); + backdrop-filter: saturate(140%) blur(12px); + -webkit-backdrop-filter: saturate(140%) blur(12px); + border-bottom: 1px solid var(--line-soft); +} +.nav-inner { + max-width: var(--max-w); + margin: 0 auto; + display: flex; align-items: center; gap: 32px; + padding: 14px 24px; +} +.nav-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 17px; text-decoration: none; color: inherit; } +.nav-mark { + width: 32px; height: 32px; border-radius: 9px; + background: linear-gradient(140deg, var(--accent), var(--accent-2)); + color: white; font-family: var(--font-en); font-weight: 700; font-size: 13px; + display: grid; place-items: center; + box-shadow: 0 3px 10px oklch(0.72 0.14 40 / .35); +} +.nav-links { display: flex; gap: 4px; } +.nav-link { + padding: 8px 12px; border-radius: 8px; + font-size: 13.5px; color: var(--ink-2); + cursor: pointer; text-decoration: none; +} +.nav-link:hover { background: var(--surface-2); color: var(--ink); } +.nav-cta { margin-inline-start: auto; display: flex; gap: 8px; } +.nav-cta .btn { text-decoration: none; } + +/* — hero — */ +.hero { + max-width: var(--max-w); + margin: 0 auto; + padding: 72px 24px 56px; + display: grid; + grid-template-columns: 1.15fr 1fr; + gap: 48px; + align-items: center; + position: relative; +} +.hero::before { + content: ""; position: absolute; + inset: -10% 0 -20% auto; width: 60%; + background: radial-gradient(60% 60% at 70% 40%, oklch(0.84 0.10 40 / .35), transparent 70%); + filter: blur(40px); + z-index: -1; +} +.eyebrow { + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 12px; border-radius: 999px; + background: var(--accent-soft); color: var(--accent-ink); + font-size: 12.5px; font-weight: 500; + margin-bottom: 20px; +} +.hero h1 { + font-size: 56px; line-height: 1.1; letter-spacing: -0.025em; + font-weight: 700; margin: 0 0 20px; + text-wrap: balance; +} +.hero h1 em { + font-style: normal; + background: linear-gradient(120deg, var(--accent), var(--accent-2)); + -webkit-background-clip: text; background-clip: text; color: transparent; +} +.hero p { + font-size: 18px; line-height: 1.65; color: var(--ink-2); + max-width: 540px; margin: 0 0 28px; + text-wrap: pretty; +} +.hero-search { + max-width: 540px; + background: white; + border-radius: 16px; + padding: 12px; + display: flex; gap: 8px; align-items: center; + border: 1px solid var(--line); + box-shadow: 0 12px 32px oklch(0.6 0.05 40 / .12); + margin-top: 28px; +} +.hero-search input { + flex: 1; border: 0; outline: 0; background: transparent; + font: inherit; font-size: 15px; padding: 6px 10px; + color: var(--ink); +} +.hero-search .btn { padding: 10px 18px; } +.hero-trust { + display: flex; gap: 24px; margin-top: 32px; + color: var(--ink-3); font-size: 13px; +} +.hero-trust b { color: var(--ink); font-family: var(--font-num); font-weight: 700; } + +.hero-visual { + position: relative; + aspect-ratio: 4/4.4; + border-radius: 24px; + background: linear-gradient(150deg, oklch(0.86 0.10 40), oklch(0.82 0.08 80)); + overflow: hidden; + box-shadow: 0 24px 60px oklch(0.6 0.10 40 / .25); +} +.hero-cards { + position: absolute; inset: 0; + display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; + gap: 16px; padding: 24px; +} +.hv-card { + background: white; border-radius: 14px; + padding: 14px; display: flex; flex-direction: column; gap: 6px; + box-shadow: 0 6px 20px rgba(0,0,0,.08); +} +.hv-card .swatch { height: 60px; border-radius: 8px; margin-bottom: 4px; } +.hv-card .ttl { font-size: 13.5px; font-weight: 600; line-height: 1.4; } +.hv-card .meta { font-size: 11px; color: var(--ink-3); } +.hv-card .row { display: flex; align-items: center; gap: 6px; margin-top: auto; font-size: 11px; color: var(--ink-3); } + +/* — section base — */ +section.s { + max-width: var(--max-w); + margin: 0 auto; + padding: 64px 24px; +} +.s-h { + text-align: center; + margin-bottom: 40px; +} +.s-h .eyebrow { margin: 0 auto 14px; } +.s-h h2 { + font-size: 36px; letter-spacing: -0.02em; font-weight: 700; + margin: 0 0 10px; +} +.s-h p { + font-size: 16px; color: var(--ink-2); margin: 0; + max-width: 560px; margin: 0 auto; +} + +@media (max-width: 900px) { + .hero { grid-template-columns: 1fr; padding: 48px 20px; } + .hero h1 { font-size: 38px; } + .hero-visual { display: none; } + .nav-links { display: none; } +} diff --git a/frontend/src/app/[lang]/layout.tsx b/frontend/src/app/[lang]/layout.tsx new file mode 100644 index 0000000..213169a --- /dev/null +++ b/frontend/src/app/[lang]/layout.tsx @@ -0,0 +1,45 @@ +import type { Metadata } from "next"; +import { Inter, Vazirmatn } from "next/font/google"; +import { notFound } from "next/navigation"; +import { dir, isLocale, locales } from "@/lib/i18n"; +import "../globals.css"; + +const vazirmatn = Vazirmatn({ + subsets: ["arabic", "latin"], + display: "swap", + variable: "--font-vazirmatn", +}); + +const inter = Inter({ + subsets: ["latin"], + display: "swap", + variable: "--font-inter", +}); + +export const metadata: Metadata = { + title: "ایلو — یاد بگیر، رشد کن", + description: + "ایلو پلتفرم یادگیری آنلاین فارسی‌زبانه. صدها دوره از مدرس‌های متخصص.", +}; + +export function generateStaticParams() { + return locales.map((lang) => ({ lang })); +} + +export default async function RootLayout({ + children, + params, +}: LayoutProps<"/[lang]">) { + const { lang } = await params; + if (!isLocale(lang)) notFound(); + + return ( + + {children} + + ); +} diff --git a/frontend/src/app/[lang]/page.tsx b/frontend/src/app/[lang]/page.tsx new file mode 100644 index 0000000..10ae53c --- /dev/null +++ b/frontend/src/app/[lang]/page.tsx @@ -0,0 +1,115 @@ +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { getDictionary, isLocale } from "@/lib/i18n"; +import "./landing.css"; + +export default async function LandingPage({ params }: PageProps<"/[lang]">) { + const { lang } = await params; + if (!isLocale(lang)) notFound(); + const dict = await getDictionary(lang); + const t = dict.landing; + const nav = dict.nav; + + return ( +
+ + +
+
+ 🔥 {t.eyebrow} +

+ {t.headline_a} {t.headline_b} + {t.headline_c} +
+ {t.headline_d} +

+

{t.lede}

+
+ + +
+
+ + ۲۴۰+ {t.trust_courses} + + + ۸۰+ {t.trust_instructors} + + + ۱۲۰هزار {t.trust_students} + + + ۴.۸ ⭐ {t.trust_rating} + +
+
+ +
+
+
+
+
طراحی رابط کاربری
+
آرش پاکدل
+
+ ⭐ ۴.۹ + · + ۱۲۸۴ دانشجو +
+
+
+
+
پایتون از صفر
+
رامین صدر
+
+ ⭐ ۴.۹ + · + ۲۱۰۴ دانشجو +
+
+
+
+
سئو و بازاریابی
+
ندا فرهادی
+
+ ⭐ ۴.۸ + · + ۶۳۴ دانشجو +
+
+
+
+
عکاسی موبایل
+
سارا کاویانی
+
+ ⭐ ۴.۷ + · + ۳۱۲ دانشجو +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..03456d2 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,550 @@ +/* ── Ilo design tokens ─────────────────────────────────────────── */ +:root { + /* type — --font-vazirmatn / --font-inter come from next/font in layout.tsx */ + --font-fa: var(--font-vazirmatn), ui-sans-serif, system-ui, sans-serif; + --font-en: var(--font-inter), ui-sans-serif, system-ui, sans-serif; + --font-num: var(--font-inter), var(--font-vazirmatn), ui-sans-serif, sans-serif; + + /* warm neutrals (light) */ + --bg: oklch(0.985 0.005 80); + --surface: oklch(1 0 0); + --surface-2: oklch(0.97 0.006 80); + --line: oklch(0.91 0.008 80); + --line-soft: oklch(0.94 0.006 80); + --ink: oklch(0.24 0.012 60); + --ink-2: oklch(0.40 0.010 60); + --ink-3: oklch(0.58 0.010 60); + --ink-4: oklch(0.74 0.008 60); + + /* accents (matched chroma 0.14) */ + --accent: oklch(0.72 0.14 40); /* coral-orange */ + --accent-2: oklch(0.66 0.14 40); + --accent-soft: oklch(0.94 0.04 40); + --accent-ink: oklch(0.42 0.10 40); + + --ok: oklch(0.70 0.14 155); + --ok-soft: oklch(0.94 0.04 155); + --warn: oklch(0.78 0.14 75); + --warn-soft: oklch(0.95 0.04 75); + --info: oklch(0.70 0.10 240); + --info-soft: oklch(0.94 0.03 240); + + /* radii */ + --r-xs: 6px; + --r-sm: 8px; + --r-md: 12px; + --r-lg: 16px; + --r-xl: 24px; + --r-pill: 999px; + + /* shadows */ + --sh-1: 0 1px 0 rgba(20, 14, 0, 0.02), 0 1px 2px rgba(20, 14, 0, 0.04); + --sh-2: 0 1px 2px rgba(20, 14, 0, 0.04), 0 8px 24px rgba(20, 14, 0, 0.06); + --sh-3: 0 12px 40px rgba(20, 14, 0, 0.10); + + /* density (regular) */ + --dens: 1; + --pad-card: 20px; + --pad-row: 14px; + --gap-md: 16px; + --gap-lg: 24px; + + /* layout */ + --sidebar-w: 248px; + --topbar-h: 60px; +} + +[data-theme="dark"] { + --bg: oklch(0.20 0.012 60); + --surface: oklch(0.25 0.012 60); + --surface-2: oklch(0.22 0.012 60); + --line: oklch(0.32 0.012 60); + --line-soft: oklch(0.28 0.012 60); + --ink: oklch(0.96 0.005 80); + --ink-2: oklch(0.82 0.008 70); + --ink-3: oklch(0.66 0.010 70); + --ink-4: oklch(0.50 0.010 70); + --accent-soft: oklch(0.32 0.06 40); + --accent-ink: oklch(0.84 0.10 40); + --ok-soft: oklch(0.30 0.05 155); + --warn-soft: oklch(0.32 0.06 75); + --info-soft: oklch(0.30 0.04 240); + --sh-1: 0 1px 0 rgba(0,0,0,.2), 0 1px 2px rgba(0,0,0,.25); + --sh-2: 0 1px 2px rgba(0,0,0,.2), 0 8px 24px rgba(0,0,0,.3); +} + +[data-density="compact"] { + --pad-card: 14px; + --pad-row: 10px; + --gap-md: 12px; + --gap-lg: 18px; + --sidebar-w: 220px; + --topbar-h: 52px; +} +[data-density="cozy"] { + --pad-card: 26px; + --pad-row: 18px; + --gap-md: 20px; + --gap-lg: 32px; + --sidebar-w: 268px; + --topbar-h: 68px; +} + +/* ── base ─────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } +html, body, #root { height: 100%; } +body { + margin: 0; + font-family: var(--font-fa); + font-size: 14px; + line-height: 1.55; + color: var(--ink); + background: var(--bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +[dir="rtl"] body { letter-spacing: 0; } +.en, [lang="en"] { font-family: var(--font-en); letter-spacing: -0.01em; } +.num { font-family: var(--font-num); font-variant-numeric: tabular-nums; } + +button { font-family: inherit; } +::selection { background: var(--accent-soft); color: var(--accent-ink); } + +/* ── shell ────────────────────────────────────────────────────── */ +.app { + display: grid; + grid-template-columns: var(--sidebar-w) 1fr; + height: 100vh; + background: var(--bg); +} + +/* sidebar */ +.sb { + display: flex; + flex-direction: column; + border-inline-end: 1px solid var(--line); + background: var(--surface-2); + padding: 16px 12px; + gap: 4px; + overflow-y: auto; +} +.sb-brand { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px 18px; +} +.sb-mark { + width: 30px; height: 30px; + border-radius: 9px; + background: linear-gradient(140deg, var(--accent), var(--accent-2)); + display: grid; place-items: center; + color: white; + font-weight: 700; + font-family: var(--font-en); + font-size: 14px; + letter-spacing: -0.02em; + box-shadow: var(--sh-1); +} +.sb-name { + font-weight: 700; + font-size: 16px; + letter-spacing: -0.01em; +} +.sb-sub { + font-size: 11px; + color: var(--ink-3); + margin-top: -2px; +} +.sb-section { + padding: 16px 10px 6px; + font-size: 10.5px; + letter-spacing: 0.06em; + color: var(--ink-3); + font-weight: 600; + text-transform: uppercase; +} +.sb-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: var(--r-sm); + color: var(--ink-2); + cursor: pointer; + font-size: 13.5px; + font-weight: 500; + user-select: none; + border: 1px solid transparent; + background: transparent; + width: 100%; + text-align: start; +} +.sb-item:hover { background: var(--line-soft); color: var(--ink); } +.sb-item[aria-current="page"] { + background: var(--surface); + color: var(--ink); + border-color: var(--line); + box-shadow: var(--sh-1); +} +.sb-item .ic { color: var(--ink-3); } +.sb-item[aria-current="page"] .ic { color: var(--accent); } +.sb-item .badge { + margin-inline-start: auto; + font-size: 10.5px; + background: var(--accent-soft); + color: var(--accent-ink); + padding: 1px 7px; + border-radius: var(--r-pill); + font-weight: 600; +} + +.sb-bottom { + margin-top: auto; + padding-top: 12px; + border-top: 1px solid var(--line-soft); +} +.sb-user { + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; + border-radius: var(--r-sm); + cursor: pointer; +} +.sb-user:hover { background: var(--line-soft); } +.sb-avatar { + width: 32px; height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, oklch(0.85 0.05 80), oklch(0.78 0.07 60)); + display: grid; place-items: center; + color: white; + font-weight: 600; + font-size: 12px; +} +.sb-user-meta { flex: 1; min-width: 0; } +.sb-user-name { font-size: 13px; font-weight: 600; } +.sb-user-role { font-size: 11px; color: var(--ink-3); } + +/* main */ +.main { + display: flex; + flex-direction: column; + min-width: 0; + overflow: hidden; +} +.topbar { + height: var(--topbar-h); + border-bottom: 1px solid var(--line); + display: flex; + align-items: center; + gap: 12px; + padding: 0 24px; + background: color-mix(in oklch, var(--surface) 75%, transparent); + backdrop-filter: blur(12px); + position: sticky; top: 0; z-index: 5; + flex-shrink: 0; +} +.crumb { + display: flex; align-items: center; gap: 6px; + font-size: 13px; + color: var(--ink-3); +} +.crumb b { color: var(--ink); font-weight: 600; } +.crumb .sep { color: var(--ink-4); } +.topbar-spacer { flex: 1; } + +.search { + display: flex; align-items: center; gap: 8px; + background: var(--surface-2); + border: 1px solid var(--line); + border-radius: var(--r-sm); + padding: 6px 12px; + width: 280px; + color: var(--ink-3); +} +.search input { + flex: 1; + border: 0; outline: 0; background: transparent; + font: inherit; color: var(--ink); +} +.search kbd { + font-family: var(--font-en); + font-size: 10px; + background: var(--surface); + border: 1px solid var(--line); + padding: 1px 6px; + border-radius: 4px; + color: var(--ink-3); +} + +.iconbtn { + width: 34px; height: 34px; + border: 1px solid var(--line); + background: var(--surface); + border-radius: var(--r-sm); + display: grid; place-items: center; + color: var(--ink-2); + cursor: pointer; + position: relative; +} +.iconbtn:hover { background: var(--surface-2); color: var(--ink); } +.iconbtn .dot { + position: absolute; top: 6px; inset-inline-end: 6px; + width: 7px; height: 7px; + background: var(--accent); + border-radius: 50%; + border: 2px solid var(--surface); +} + +.page { + flex: 1; + overflow-y: auto; + padding: var(--gap-lg); +} +.page-inner { + max-width: 1280px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: var(--gap-lg); +} + +.page-h { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.page-h h1 { + font-size: 26px; + letter-spacing: -0.015em; + margin: 0 0 4px; + font-weight: 700; +} +.page-h .lede { + color: var(--ink-3); + margin: 0; + font-size: 14px; +} + +/* ── primitives ───────────────────────────────────────────────── */ +.card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--r-md); + box-shadow: var(--sh-1); +} +.card-pad { padding: var(--pad-card); } +.card-h { + padding: 16px var(--pad-card); + border-bottom: 1px solid var(--line-soft); + display: flex; align-items: center; justify-content: space-between; + gap: 12px; +} +.card-h h3 { + margin: 0; + font-size: 15px; + font-weight: 600; +} +.card-h .meta { + font-size: 12px; color: var(--ink-3); +} + +.btn { + appearance: none; + border: 1px solid var(--line); + background: var(--surface); + color: var(--ink); + padding: 8px 14px; + border-radius: var(--r-sm); + font-size: 13px; + font-weight: 500; + cursor: pointer; + display: inline-flex; align-items: center; gap: 6px; + transition: background .15s, transform .05s; +} +.btn:hover { background: var(--surface-2); } +.btn:active { transform: translateY(1px); } +.btn.primary { + background: var(--accent); + color: white; + border-color: transparent; + box-shadow: 0 1px 2px rgba(180, 80, 30, 0.25); +} +.btn.primary:hover { background: var(--accent-2); } +.btn.ghost { border-color: transparent; background: transparent; } +.btn.ghost:hover { background: var(--line-soft); } +.btn.sm { padding: 5px 10px; font-size: 12px; } + +.pill { + display: inline-flex; align-items: center; gap: 5px; + font-size: 11.5px; + padding: 2px 9px; + border-radius: var(--r-pill); + background: var(--surface-2); + color: var(--ink-2); + font-weight: 500; + border: 1px solid var(--line-soft); +} +.pill.ok { background: var(--ok-soft); color: oklch(0.40 0.10 155); border-color: transparent; } +.pill.warn { background: var(--warn-soft); color: oklch(0.46 0.10 75); border-color: transparent; } +.pill.info { background: var(--info-soft); color: oklch(0.42 0.08 240); border-color: transparent; } +.pill.accent { background: var(--accent-soft); color: var(--accent-ink); border-color: transparent; } + +.dot-i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; } + +.divider { height: 1px; background: var(--line-soft); } + +.muted { color: var(--ink-3); } +.tabular { font-variant-numeric: tabular-nums; } + +/* avatar */ +.av { + width: 32px; height: 32px; + border-radius: 50%; + display: grid; place-items: center; + color: white; + font-weight: 600; + font-size: 12px; + font-family: var(--font-en); + flex-shrink: 0; +} +.av-sm { width: 24px; height: 24px; font-size: 10px; } +.av-lg { width: 40px; height: 40px; font-size: 14px; } + +/* progress */ +.prog { + height: 6px; + background: var(--line-soft); + border-radius: var(--r-pill); + overflow: hidden; +} +.prog > i { + display: block; + height: 100%; + background: var(--accent); + border-radius: var(--r-pill); +} + +/* tabs */ +.tabs { + display: flex; + gap: 2px; + border-bottom: 1px solid var(--line); +} +.tab { + appearance: none; background: transparent; border: 0; + padding: 10px 14px; + font: inherit; font-size: 13px; font-weight: 500; + color: var(--ink-3); + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} +.tab:hover { color: var(--ink); } +.tab[aria-selected="true"] { + color: var(--ink); + border-bottom-color: var(--accent); +} + +/* table */ +.tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.tbl th, .tbl td { + text-align: start; + padding: 12px 16px; + border-bottom: 1px solid var(--line-soft); +} +.tbl th { + font-weight: 500; + font-size: 11.5px; + color: var(--ink-3); + text-transform: uppercase; + letter-spacing: 0.04em; + background: var(--surface-2); +} +.tbl tr:last-child td { border-bottom: 0; } +.tbl tbody tr { cursor: pointer; } +.tbl tbody tr:hover { background: var(--surface-2); } + +/* checkbox */ +.chk { + appearance: none; + width: 16px; height: 16px; + border: 1.5px solid var(--ink-4); + border-radius: 4px; + display: inline-grid; place-items: center; + cursor: pointer; + background: var(--surface); + position: relative; +} +.chk:checked { + background: var(--accent); + border-color: var(--accent); +} +.chk:checked::after { + content: ""; + width: 9px; height: 5px; + border-left: 1.5px solid white; + border-bottom: 1.5px solid white; + transform: rotate(-45deg) translate(1px, -1px); +} + +/* input */ +.inp { + width: 100%; + padding: 9px 12px; + border: 1px solid var(--line); + background: var(--surface); + border-radius: var(--r-sm); + font: inherit; + color: var(--ink); + outline: none; +} +.inp:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } + +/* utility flex grids */ +.row { display: flex; gap: var(--gap-md); } +.col { display: flex; flex-direction: column; gap: var(--gap-md); } +.between { display: flex; align-items: center; justify-content: space-between; gap: 12px; } + +/* skeleton stripe placeholder */ +.placeholder { + background: + repeating-linear-gradient( + 135deg, + var(--surface-2), + var(--surface-2) 10px, + var(--line-soft) 10px, + var(--line-soft) 11px + ); + border: 1px dashed var(--line); + border-radius: var(--r-sm); + display: grid; place-items: center; + color: var(--ink-3); + font-family: var(--font-en); + font-size: 11px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +/* ── icons ───────────────────────────────────────────────────── */ +.ic { width: 18px; height: 18px; flex-shrink: 0; } +.ic-sm { width: 14px; height: 14px; flex-shrink: 0; } +.ic-lg { width: 22px; height: 22px; flex-shrink: 0; } + +/* RTL flips for the chevrons */ +[dir="rtl"] .flipx { transform: scaleX(-1); } + +/* ── motion ──────────────────────────────────────────────────── */ +@keyframes fadeUp { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} +.page-inner > * { animation: fadeUp .22s ease both; } +.page-inner > *:nth-child(2) { animation-delay: 0.04s; } +.page-inner > *:nth-child(3) { animation-delay: 0.08s; } +.page-inner > *:nth-child(4) { animation-delay: 0.12s; } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..014a30c --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,52 @@ +// On the server (SSR / route handlers / RSC) prefer INTERNAL_API_URL — Docker compose service name. +// In the browser, NEXT_PUBLIC_API_URL is baked in at build time and points at the host. +const rawBaseUrl = + typeof window === "undefined" + ? process.env.INTERNAL_API_URL ?? process.env.NEXT_PUBLIC_API_URL + : process.env.NEXT_PUBLIC_API_URL; +const baseUrl = (rawBaseUrl ?? "http://localhost:8000").replace(/\/$/, ""); + +export class ApiError extends Error { + constructor(public status: number, message: string, public body?: unknown) { + super(message); + this.name = "ApiError"; + } +} + +type FetchInit = RequestInit & { token?: string; locale?: string }; + +async function request(path: string, init: FetchInit = {}): Promise { + const { token, locale, headers, ...rest } = init; + const res = await fetch(`${baseUrl}${path.startsWith("/") ? path : `/${path}`}`, { + ...rest, + headers: { + "Content-Type": "application/json", + ...(locale ? { "Accept-Language": locale } : {}), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...headers, + }, + }); + + if (!res.ok) { + let body: unknown; + try { + body = await res.json(); + } catch { + body = await res.text().catch(() => undefined); + } + throw new ApiError(res.status, `${res.status} ${res.statusText}`, body); + } + + if (res.status === 204) return undefined as T; + return res.json() as Promise; +} + +export const api = { + baseUrl, + get: (path: string, init?: FetchInit) => request(path, { ...init, method: "GET" }), + post: (path: string, body?: unknown, init?: FetchInit) => + request(path, { ...init, method: "POST", body: body ? JSON.stringify(body) : undefined }), + patch: (path: string, body?: unknown, init?: FetchInit) => + request(path, { ...init, method: "PATCH", body: body ? JSON.stringify(body) : undefined }), + delete: (path: string, init?: FetchInit) => request(path, { ...init, method: "DELETE" }), +}; diff --git a/frontend/src/lib/dictionaries/en.json b/frontend/src/lib/dictionaries/en.json new file mode 100644 index 0000000..11e692c --- /dev/null +++ b/frontend/src/lib/dictionaries/en.json @@ -0,0 +1,24 @@ +{ + "nav": { + "courses": "Courses", + "instructors": "Instructors", + "business": "For business", + "blog": "Blog", + "login": "Log in", + "signup": "Sign up free" + }, + "landing": { + "eyebrow": "+2,000 active students this week", + "headline_a": "Learn from", + "headline_b": "the best instructors", + "headline_c": ",", + "headline_d": "anywhere you are.", + "lede": "Ilo is a Persian-language online learning platform. Hundreds of courses from expert instructors — from design and coding to business and photography.", + "search_placeholder": "What do you want to learn?", + "search_button": "Search", + "trust_courses": "courses", + "trust_instructors": "expert instructors", + "trust_students": "students", + "trust_rating": "avg. satisfaction" + } +} diff --git a/frontend/src/lib/dictionaries/fa.json b/frontend/src/lib/dictionaries/fa.json new file mode 100644 index 0000000..6f70412 --- /dev/null +++ b/frontend/src/lib/dictionaries/fa.json @@ -0,0 +1,24 @@ +{ + "nav": { + "courses": "دوره‌ها", + "instructors": "مدرس‌ها", + "business": "برای کسب‌وکارها", + "blog": "وبلاگ", + "login": "ورود", + "signup": "ثبت‌نام رایگان" + }, + "landing": { + "eyebrow": "+۲٬۰۰۰ دانشجوی فعال این هفته", + "headline_a": "یاد بگیر از", + "headline_b": "بهترین مدرس‌ها", + "headline_c": "،", + "headline_d": "هر جا که هستی.", + "lede": "ایلو پلتفرم یادگیری آنلاین فارسی‌زبانه. صدها دوره از مدرس‌های متخصص — از طراحی و کدنویسی گرفته تا کسب‌وکار و عکاسی.", + "search_placeholder": "چی می‌خوای یاد بگیری؟", + "search_button": "جستجو", + "trust_courses": "دوره", + "trust_instructors": "مدرس متخصص", + "trust_students": "دانشجو", + "trust_rating": "میانگین رضایت" + } +} diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts new file mode 100644 index 0000000..10790ca --- /dev/null +++ b/frontend/src/lib/i18n.ts @@ -0,0 +1,20 @@ +import "server-only"; + +export const locales = ["fa", "en"] as const; +export type Locale = (typeof locales)[number]; +export const defaultLocale: Locale = "fa"; + +export const isLocale = (value: string): value is Locale => + (locales as readonly string[]).includes(value); + +export const dir = (locale: Locale) => (locale === "fa" ? "rtl" : "ltr"); + +const dictionaries = { + fa: () => import("./dictionaries/fa.json").then((m) => m.default), + en: () => import("./dictionaries/en.json").then((m) => m.default), +} satisfies Record Promise>; + +export type Dictionary = Awaited>; + +export const getDictionary = (locale: Locale): Promise => + dictionaries[locale]() as Promise; diff --git a/frontend/src/proxy.ts b/frontend/src/proxy.ts new file mode 100644 index 0000000..932a1f6 --- /dev/null +++ b/frontend/src/proxy.ts @@ -0,0 +1,30 @@ +import { NextResponse, type NextRequest } from "next/server"; + +const locales = ["fa", "en"] as const; +const defaultLocale = "fa"; + +function pickLocale(req: NextRequest): string { + const accept = req.headers.get("accept-language") ?? ""; + const preferred = accept + .split(",") + .map((p) => p.trim().split(";")[0]!.toLowerCase().split("-")[0]) + .find((tag) => (locales as readonly string[]).includes(tag)); + return preferred ?? defaultLocale; +} + +export function proxy(req: NextRequest) { + const { pathname } = req.nextUrl; + const hasLocale = locales.some( + (loc) => pathname === `/${loc}` || pathname.startsWith(`/${loc}/`), + ); + if (hasLocale) return; + + const locale = pickLocale(req); + const url = req.nextUrl.clone(); + url.pathname = `/${locale}${pathname === "/" ? "" : pathname}`; + return NextResponse.redirect(url); +} + +export const config = { + matcher: ["/((?!_next|api|favicon.ico|.*\\..*).*)"], +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +}