add packages
This commit is contained in:
parent
f746a9ff89
commit
b6e32ded7b
@ -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
8
frontend/.dockerignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.env.local
|
||||||
|
.git
|
||||||
|
*.log
|
||||||
|
README.md
|
||||||
|
AGENTS.md
|
||||||
|
CLAUDE.md
|
||||||
41
frontend/.gitignore
vendored
Normal file
41
frontend/.gitignore
vendored
Normal 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
4
frontend/.npmrc
Normal 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
5
frontend/AGENTS.md
Normal 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
1
frontend/CLAUDE.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
@AGENTS.md
|
||||||
28
frontend/Dockerfile
Normal file
28
frontend/Dockerfile
Normal 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
36
frontend/README.md
Normal 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
7
frontend/next.config.ts
Normal 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
976
frontend/package-lock.json
generated
Normal 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
21
frontend/package.json
Normal 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
1
frontend/public/file.svg
Normal 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 |
1
frontend/public/globe.svg
Normal file
1
frontend/public/globe.svg
Normal 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
1
frontend/public/next.svg
Normal 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 |
1
frontend/public/vercel.svg
Normal file
1
frontend/public/vercel.svg
Normal 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 |
1
frontend/public/window.svg
Normal file
1
frontend/public/window.svg
Normal 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 |
152
frontend/src/app/[lang]/landing.css
Normal file
152
frontend/src/app/[lang]/landing.css
Normal 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; }
|
||||||
|
}
|
||||||
45
frontend/src/app/[lang]/layout.tsx
Normal file
45
frontend/src/app/[lang]/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
frontend/src/app/[lang]/page.tsx
Normal file
115
frontend/src/app/[lang]/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
550
frontend/src/app/globals.css
Normal file
550
frontend/src/app/globals.css
Normal 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
52
frontend/src/lib/api.ts
Normal 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" }),
|
||||||
|
};
|
||||||
24
frontend/src/lib/dictionaries/en.json
Normal file
24
frontend/src/lib/dictionaries/en.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
frontend/src/lib/dictionaries/fa.json
Normal file
24
frontend/src/lib/dictionaries/fa.json
Normal 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
20
frontend/src/lib/i18n.ts
Normal 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
30
frontend/src/proxy.ts
Normal 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
34
frontend/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user