add packages

This commit is contained in:
Hossein 2026-05-03 19:51:38 +03:30
parent f746a9ff89
commit b6e32ded7b
27 changed files with 2198 additions and 0 deletions

View File

@ -29,5 +29,25 @@ services:
db: db:
condition: service_healthy 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: volumes:
postgres_data: postgres_data:

8
frontend/.dockerignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
.next
.env.local
.git
*.log
README.md
AGENTS.md
CLAUDE.md

41
frontend/.gitignore vendored Normal file
View File

@ -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

4
frontend/.npmrc Normal file
View File

@ -0,0 +1,4 @@
fetch-timeout=600000
fetch-retries=5
fetch-retry-mintimeout=20000
fetch-retry-maxtimeout=120000

5
frontend/AGENTS.md Normal file
View File

@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# 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.
<!-- END:nextjs-agent-rules -->

1
frontend/CLAUDE.md Normal file
View File

@ -0,0 +1 @@
@AGENTS.md

28
frontend/Dockerfile Normal file
View File

@ -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"]

36
frontend/README.md Normal file
View File

@ -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.

7
frontend/next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

976
frontend/package-lock.json generated Normal file
View File

@ -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"
}
}
}

21
frontend/package.json Normal file
View File

@ -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"
}
}

1
frontend/public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
frontend/public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@ -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; }
}

View File

@ -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 (
<html
lang={lang}
dir={dir(lang)}
className={`${vazirmatn.variable} ${inter.variable}`}
>
<body>{children}</body>
</html>
);
}

View File

@ -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 (
<div className="landing">
<nav className="nav">
<div className="nav-inner">
<Link href={`/${lang}`} className="nav-brand">
<div className="nav-mark">il</div>
<span>{lang === "fa" ? "ایلو" : "Ilo"}</span>
</Link>
<div className="nav-links">
<a className="nav-link">{nav.courses}</a>
<a className="nav-link">{nav.instructors}</a>
<a className="nav-link">{nav.business}</a>
<a className="nav-link">{nav.blog}</a>
</div>
<div className="nav-cta">
<Link className="btn sm" href={`/${lang}/login`}>
{nav.login}
</Link>
<Link className="btn primary sm" href={`/${lang}/signup`}>
{nav.signup}
</Link>
</div>
</div>
</nav>
<header className="hero">
<div>
<span className="eyebrow">🔥 {t.eyebrow}</span>
<h1>
{t.headline_a} <em>{t.headline_b}</em>
{t.headline_c}
<br />
{t.headline_d}
</h1>
<p>{t.lede}</p>
<div className="hero-search">
<input placeholder={t.search_placeholder} />
<button className="btn primary">{t.search_button}</button>
</div>
<div className="hero-trust">
<span>
<b>۲۴۰+</b> {t.trust_courses}
</span>
<span>
<b>۸۰+</b> {t.trust_instructors}
</span>
<span>
<b>۱۲۰هزار</b> {t.trust_students}
</span>
<span>
<b>۴.۸</b> {t.trust_rating}
</span>
</div>
</div>
<div className="hero-visual">
<div className="hero-cards">
<div className="hv-card">
<div className="swatch" style={{ background: "oklch(0.78 0.12 40)" }} />
<div className="ttl">طراحی رابط کاربری</div>
<div className="meta">آرش پاکدل</div>
<div className="row">
<span> ۴.۹</span>
<span>·</span>
<span className="num">۱۲۸۴ دانشجو</span>
</div>
</div>
<div className="hv-card">
<div className="swatch" style={{ background: "oklch(0.74 0.12 145)" }} />
<div className="ttl">پایتون از صفر</div>
<div className="meta">رامین صدر</div>
<div className="row">
<span> ۴.۹</span>
<span>·</span>
<span className="num">۲۱۰۴ دانشجو</span>
</div>
</div>
<div className="hv-card">
<div className="swatch" style={{ background: "oklch(0.74 0.12 80)" }} />
<div className="ttl">سئو و بازاریابی</div>
<div className="meta">ندا فرهادی</div>
<div className="row">
<span> ۴.۸</span>
<span>·</span>
<span className="num">۶۳۴ دانشجو</span>
</div>
</div>
<div className="hv-card">
<div className="swatch" style={{ background: "oklch(0.74 0.12 320)" }} />
<div className="ttl">عکاسی موبایل</div>
<div className="meta">سارا کاویانی</div>
<div className="row">
<span> ۴.۷</span>
<span>·</span>
<span className="num">۳۱۲ دانشجو</span>
</div>
</div>
</div>
</div>
</header>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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; }

52
frontend/src/lib/api.ts Normal file
View File

@ -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<T>(path: string, init: FetchInit = {}): Promise<T> {
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<T>;
}
export const api = {
baseUrl,
get: <T>(path: string, init?: FetchInit) => request<T>(path, { ...init, method: "GET" }),
post: <T>(path: string, body?: unknown, init?: FetchInit) =>
request<T>(path, { ...init, method: "POST", body: body ? JSON.stringify(body) : undefined }),
patch: <T>(path: string, body?: unknown, init?: FetchInit) =>
request<T>(path, { ...init, method: "PATCH", body: body ? JSON.stringify(body) : undefined }),
delete: <T>(path: string, init?: FetchInit) => request<T>(path, { ...init, method: "DELETE" }),
};

View File

@ -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"
}
}

View File

@ -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": "میانگین رضایت"
}
}

20
frontend/src/lib/i18n.ts Normal file
View File

@ -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<Locale, () => Promise<unknown>>;
export type Dictionary = Awaited<ReturnType<(typeof dictionaries)["fa"]>>;
export const getDictionary = (locale: Locale): Promise<Dictionary> =>
dictionaries[locale]() as Promise<Dictionary>;

30
frontend/src/proxy.ts Normal file
View File

@ -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|.*\\..*).*)"],
};

34
frontend/tsconfig.json Normal file
View File

@ -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"]
}