Initial commit

This commit is contained in:
2026-10-08 19:51:09 +03:30
commit a133dc43bf
60 changed files with 2779 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
exec figma-analyze routes "$@"

4
.figma/make/deploy Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
pnpm run build
figma make deploy --build-dir dist

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Build in development mode to emit sourcemaps.
pnpm run build --mode development
figma make deploy-preview --build-dir dist

4
.figma/make/dev Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
pnpm run dev

22
.figma/make/dev.json Normal file
View File

@@ -0,0 +1,22 @@
{
// Files whose changes trigger the install script + dev server restart.
// Glob syntax: doublestar (supports **, *, ?, [abc], {a,b}). Paths are
// repo-relative and case-sensitive. `.mise.toml` and the scripts under
// `.figma/make/` are always watched regardless of these lists.
//
// Each key accepts a shorthand array of globs:
// "installOn": ["package.json"]
// or an object with includes/excludes:
// "installOn": { "includes": ["**/*.json"], "excludes": ["test/**"] }
//
// Only list files that affect the *toolchain or dependency tree* here —
// NOT application source files. Vite's dev server already watches
// application code (src/**/*) and config (vite.config.ts) via its own HMR
// pipeline, so including them here would cause a redundant full restart.
"installOn": [
"package.json",
"pnpm-lock.yaml"
],
// Files whose changes trigger only a dev server restart (no install).
"restartOn": []
}

4
.figma/make/format Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
pnpm run format -- "$@"

4
.figma/make/install Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
pnpm install --prefer-offline --no-frozen-lockfile

3
.figma/make/langserver Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec pnpm dlx --package=@vtsls/language-server@^0.3.0 vtsls --stdio

10
.figma/make/site.json Normal file
View File

@@ -0,0 +1,10 @@
{
"description": "A modern, responsive Persian educational magazine site offering students easy access to articles, grades info, and educational content without login.",
"robots": {
"index": false
},
"accessibility": {
"addBypassLinks": false,
"ignoreReducedMotion": false
}
}

146
.gitattributes vendored Normal file
View File

@@ -0,0 +1,146 @@
# Git LFS Tracking Rules
# Generated for binary, large, and non-diffable file types
# ---------------------------------------------------------------------------
# Images & Graphics
# ---------------------------------------------------------------------------
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.bmp filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.ai filter=lfs diff=lfs merge=lfs -text
*.sketch filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Video
# ---------------------------------------------------------------------------
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.mov filter=lfs diff=lfs merge=lfs -text
*.avi filter=lfs diff=lfs merge=lfs -text
*.mkv filter=lfs diff=lfs merge=lfs -text
*.wmv filter=lfs diff=lfs merge=lfs -text
*.flv filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
*.m4v filter=lfs diff=lfs merge=lfs -text
*.mpg filter=lfs diff=lfs merge=lfs -text
*.mpeg filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Audio
# ---------------------------------------------------------------------------
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.flac filter=lfs diff=lfs merge=lfs -text
*.aac filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.m4a filter=lfs diff=lfs merge=lfs -text
*.wma filter=lfs diff=lfs merge=lfs -text
*.aiff filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# 3D & Game Assets
# ---------------------------------------------------------------------------
*.fbx filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.dae filter=lfs diff=lfs merge=lfs -text
*.3ds filter=lfs diff=lfs merge=lfs -text
*.max filter=lfs diff=lfs merge=lfs -text
*.unity filter=lfs diff=lfs merge=lfs -text
*.unitypackage filter=lfs diff=lfs merge=lfs -text
*.uasset filter=lfs diff=lfs merge=lfs -text
*.umap filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Archives & Compressed Files
# ---------------------------------------------------------------------------
*.zip filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.7z filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Documents & Fonts
# ---------------------------------------------------------------------------
*.pdf filter=lfs diff=lfs merge=lfs -text
*.docx filter=lfs diff=lfs merge=lfs -text
*.xlsx filter=lfs diff=lfs merge=lfs -text
*.pptx filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.woff filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text
*.eot filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Data & Machine Learning
# ---------------------------------------------------------------------------
*.parquet filter=lfs diff=lfs merge=lfs -text
*.hdf5 filter=lfs diff=lfs merge=lfs -text
*.h5 filter=lfs diff=lfs merge=lfs -text
*.pkl filter=lfs diff=lfs merge=lfs -text
*.pickle filter=lfs diff=lfs merge=lfs -text
*.npy filter=lfs diff=lfs merge=lfs -text
*.npz filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.pt filter=lfs diff=lfs merge=lfs -text
*.pth filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
*.pb filter=lfs diff=lfs merge=lfs -text
*.safetensors filter=lfs diff=lfs merge=lfs -text
*.gguf filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Databases
# ---------------------------------------------------------------------------
*.db filter=lfs diff=lfs merge=lfs -text
*.sqlite filter=lfs diff=lfs merge=lfs -text
*.sqlite3 filter=lfs diff=lfs merge=lfs -text
*.mdb filter=lfs diff=lfs merge=lfs -text
*.accdb filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Executables & Libraries
# ---------------------------------------------------------------------------
*.exe filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.so filter=lfs diff=lfs merge=lfs -text
*.dylib filter=lfs diff=lfs merge=lfs -text
*.a filter=lfs diff=lfs merge=lfs -text
*.lib filter=lfs diff=lfs merge=lfs -text
*.wasm filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# Build Artifacts & Packages
# ---------------------------------------------------------------------------
*.jar filter=lfs diff=lfs merge=lfs -text
*.war filter=lfs diff=lfs merge=lfs -text
*.ear filter=lfs diff=lfs merge=lfs -text
*.apk filter=lfs diff=lfs merge=lfs -text
*.ipa filter=lfs diff=lfs merge=lfs -text
*.deb filter=lfs diff=lfs merge=lfs -text
*.rpm filter=lfs diff=lfs merge=lfs -text
*.dmg filter=lfs diff=lfs merge=lfs -text
*.iso filter=lfs diff=lfs merge=lfs -text
*.img filter=lfs diff=lfs merge=lfs -text
# ---------------------------------------------------------------------------
# CAD & Scientific
# ---------------------------------------------------------------------------
*.dwg filter=lfs diff=lfs merge=lfs -text
*.dxf filter=lfs diff=lfs merge=lfs -text
*.step filter=lfs diff=lfs merge=lfs -text
*.stp filter=lfs diff=lfs merge=lfs -text
*.iges filter=lfs diff=lfs merge=lfs -text
*.stl filter=lfs diff=lfs merge=lfs -text
*.nc filter=lfs diff=lfs merge=lfs -text
*.mat filter=lfs diff=lfs merge=lfs -text

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
node_modules/
dist/
build/
.cache/
.env*
.vite/
vite.config.*.timestamp-*
*.tsbuildinfo
*-debug.log*
logs/
*.log
.claude-replay.*
.tmp-*
/.figma/design-context/
# Platform-managed agentplat state, not project source. In the
# client-initiated single-repo workspace layout (make's `Path: "."` spec) the
# repo root IS the workspace root, so these land inside this repo rather than
# beside it.
/.plugins/
/.opencode-skills/

3
.mise.toml Normal file
View File

@@ -0,0 +1,3 @@
[tools]
node = "22"
"npm:pnpm" = "10.34.3"

41
AGENTS.md Normal file
View File

@@ -0,0 +1,41 @@
# figma-make-app
React + Vite + Tailwind CSS project running inside Figma Make.
## Development Server
A Vite development server is **already running** on `$PORT` (default 8443). You don't need to start it manually.
- Preview URL: The user can access the running app through the preview panel
- Hot reload: Changes to source files are reflected immediately
## Project Structure
This is the canonical project structure. Start with task-relevant files below. Only follow imports or inspect other files when required, when a documented path is missing, or when the repository contradicts this guide.
- `src/main.tsx` - React entrypoint; imports `src/index.css` and mounts `src/App.tsx` into the `#root` element
- `src/App.tsx` - Primary application component and the usual starting point for UI work
- `src/index.css` - Global CSS entrypoint and Tailwind CSS v4 import
- `index.html` - Vite HTML shell containing the `#root` element and loading `src/main.tsx`
- `package.json` - Project dependencies and the Vite build, development, preview, and formatting scripts
- `vite.config.ts` - Vite configuration with React, Tailwind CSS v4, and Figma Make plugins plus the `@` alias for `src`
- `.mise.toml` - Toolchain versions for Node.js and pnpm
## Dependencies
- Runtime: React 19 and React DOM 19
- Styling: Tailwind CSS v4 with the `@tailwindcss/vite` plugin
- Build tooling: Vite 8, TypeScript 5.7, and `@vitejs/plugin-react`
- Formatting: oxfmt
## Styling
This project uses **Tailwind CSS v4** through the `@tailwindcss/vite` plugin configured in `vite.config.ts`. `src/index.css` imports Tailwind with `@import 'tailwindcss';`. Use Tailwind utility classes directly in JSX and put global CSS or Tailwind v4 theme customization in `src/index.css`. This scaffold does not need a Tailwind config file or PostCSS config.
`src/main.tsx` imports `src/index.css`, so global font wiring belongs in `src/index.css`. Keep CSS `@import` statements first, then add any `@font-face` rules and font-family defaults there.
## Code quality
- Use double quotes for strings containing apostrophes (`"We're here to help"`), or escape them in single-quoted strings. An unescaped apostrophe in a single-quoted string breaks the build.
- Ensure JSX tags are closed and braces are balanced.
- Export components as default exports.

1
CLAUDE.md Normal file
View File

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

16
index.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="<!-- figma:lang -->">
<head>
<!-- figma:head-start -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><!-- figma:title --></title>
<!-- figma:head-end -->
</head>
<body>
<!-- figma:body-start -->
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<!-- figma:body-end -->
</body>
</html>

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "figma-make-app",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"format": "oxfmt"
},
"dependencies": {
"lucide-react": "^1.52.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.13.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^6.0.0",
"oxfmt": "^0.2.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0",
"vite": "^8.0.5"
}
}

976
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,976 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
lucide-react:
specifier: ^1.52.0
version: 1.52.0(@types/react@19.2.14)(react@19.2.4)
react:
specifier: ^19.0.0
version: 19.2.4
react-dom:
specifier: ^19.0.0
version: 19.2.4(react@19.2.4)
react-router:
specifier: ^7.13.1
version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
devDependencies:
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.2.2(vite@8.0.5(@types/node@22.19.17)(jiti@2.6.1))
'@types/node':
specifier: ^22.0.0
version: 22.19.17
'@types/react':
specifier: ^19.0.0
version: 19.2.14
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: ^6.0.0
version: 6.0.1(vite@8.0.5(@types/node@22.19.17)(jiti@2.6.1))
oxfmt:
specifier: ^0.2.0
version: 0.2.0
tailwindcss:
specifier: ^4.0.0
version: 4.2.2
typescript:
specifier: ^5.7.0
version: 5.9.3
vite:
specifier: ^8.0.5
version: 8.0.5(@types/node@22.19.17)(jiti@2.6.1)
packages:
'@emnapi/core@1.9.1':
resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
'@emnapi/runtime@1.9.1':
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
'@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
'@oxc-project/types@0.122.0':
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
'@oxfmt/darwin-arm64@0.2.0':
resolution: {integrity: sha512-NK7iEPqRovUvKac+4dn2ui8v5Y5q6UJ9v4z5Zjr5lmEzTlBwXToP3TwY75IAaCYeu0g8Es7ToJpS7qCQuxUhuA==}
cpu: [arm64]
os: [darwin]
'@oxfmt/darwin-x64@0.2.0':
resolution: {integrity: sha512-eXDgT+DbIMnA3sWE+w38rOvXaxkP4RvHi41rPpWb9XoRGbM+tOo0c8RYqCIX39n7PizOlbF/xUBh9nMhiW01wQ==}
cpu: [x64]
os: [darwin]
'@oxfmt/linux-arm64-gnu@0.2.0':
resolution: {integrity: sha512-h4mw9/Lck5X/SU1RPE26VraoBJgn9SOoLl8GnaYvtmRBFHK0v+2KpnBSwMWeiaIxCdJSsrVc9mpKSOcPsen7Cg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/linux-arm64-musl@0.2.0':
resolution: {integrity: sha512-kDZ/kVCgh9kSczMH2gPXZs+rsY/VcCQ1BoQnND8D3v1Le91LceJ5s/a4XBBA3ADI3NiPdqNmK2ssaxY4BfgXDg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/linux-x64-gnu@0.2.0':
resolution: {integrity: sha512-qvf6cNuf4z3yjzjSvg4Jwr88jDyeFaYPU+4dBWVr6fdWhAA+BIuIvurJWYQGvWNNSkobVWH3A7m+wp+h5fNsSg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/linux-x64-musl@0.2.0':
resolution: {integrity: sha512-D0rw4BVLHEbtMB6p9e+Ph7Y/56oD1DRP4Qcbbv1B1w0kJanWBB3vMH+v2rqgeYsErqJOtLpYRkPFmJSU9In2HA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/win32-arm64@0.2.0':
resolution: {integrity: sha512-QM0fP8YUvBNyuNa6d+jNonV34A6zGvvQ2mX93wnERpX25jIbFKljKiKDiWo2M1nUsW8zL2z4ESEeaLO9bx8djg==}
cpu: [arm64]
os: [win32]
'@oxfmt/win32-x64@0.2.0':
resolution: {integrity: sha512-WkFXliDbwFiziBSfvWXeM0Y3x07Kxxrl39kgsp+N3n5QgzsM8q6qe094+OW+hItDQ+9SSd2y9u231sMCsKxlog==}
cpu: [x64]
os: [win32]
'@rolldown/binding-android-arm64@1.0.0-rc.12':
resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.12':
resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.12':
resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.12':
resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-rc.12':
resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==}
'@rolldown/pluginutils@1.0.0-rc.7':
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
'@tailwindcss/node@4.2.2':
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
'@tailwindcss/oxide-android-arm64@4.2.2':
resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.2.2':
resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.2.2':
resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.2.2':
resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.2.2':
resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.2.2':
resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
engines: {node: '>= 20'}
'@tailwindcss/vite@4.2.2':
resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/node@22.19.17':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
'@types/react': ^19.2.0
'@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
'@vitejs/plugin-react@6.0.1':
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
babel-plugin-react-compiler: ^1.0.0
vite: ^8.0.0
peerDependenciesMeta:
'@rolldown/plugin-babel':
optional: true
babel-plugin-react-compiler:
optional: true
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
lucide-react@1.52.0:
resolution: {integrity: sha512-TUgPtl5ZI9WgxOcbu4ckaC5B3l1qxPQrCMgb6OfUB9owx/OvSJfcf93SMgUWAxRw5/1XDfh9uI5F5iNGXCcGDQ==}
peerDependencies:
'@types/react': '*'
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
nanoid@3.3.19:
resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
oxfmt@0.2.0:
resolution: {integrity: sha512-99bPAiYDiYikUej8U0Slv8pcFVbMe38Ulm+T6bJ4KQgm+UsbgTKCBpWxbD/LRz1CuYw3PRpqfPv6AkQsIAo3Ag==}
engines: {node: '>=8.*'}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
postcss@8.5.23:
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
react: ^19.2.4
react-router@7.13.1:
resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
rolldown@1.0.0-rc.12:
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tailwindcss@4.2.2:
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
tapable@2.3.2:
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
engines: {node: '>=6'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
vite@8.0.5:
resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
'@vitejs/devtools':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
snapshots:
'@emnapi/core@1.9.1':
dependencies:
'@emnapi/wasi-threads': 1.2.0
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.9.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.0':
dependencies:
tslib: 2.8.1
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/wasm-runtime@1.1.1':
dependencies:
'@emnapi/core': 1.9.1
'@emnapi/runtime': 1.9.1
'@tybys/wasm-util': 0.10.1
optional: true
'@oxc-project/types@0.122.0': {}
'@oxfmt/darwin-arm64@0.2.0':
optional: true
'@oxfmt/darwin-x64@0.2.0':
optional: true
'@oxfmt/linux-arm64-gnu@0.2.0':
optional: true
'@oxfmt/linux-arm64-musl@0.2.0':
optional: true
'@oxfmt/linux-x64-gnu@0.2.0':
optional: true
'@oxfmt/linux-x64-musl@0.2.0':
optional: true
'@oxfmt/win32-arm64@0.2.0':
optional: true
'@oxfmt/win32-x64@0.2.0':
optional: true
'@rolldown/binding-android-arm64@1.0.0-rc.12':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.12':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.12':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.12':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
dependencies:
'@napi-rs/wasm-runtime': 1.1.1
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
optional: true
'@rolldown/pluginutils@1.0.0-rc.12': {}
'@rolldown/pluginutils@1.0.0-rc.7': {}
'@tailwindcss/node@4.2.2':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.20.1
jiti: 2.6.1
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.2.2
'@tailwindcss/oxide-android-arm64@4.2.2':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.2.2':
optional: true
'@tailwindcss/oxide-darwin-x64@4.2.2':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.2.2':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.2.2':
optional: true
'@tailwindcss/oxide@4.2.2':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.2.2
'@tailwindcss/oxide-darwin-arm64': 4.2.2
'@tailwindcss/oxide-darwin-x64': 4.2.2
'@tailwindcss/oxide-freebsd-x64': 4.2.2
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2
'@tailwindcss/oxide-linux-arm64-gnu': 4.2.2
'@tailwindcss/oxide-linux-arm64-musl': 4.2.2
'@tailwindcss/oxide-linux-x64-gnu': 4.2.2
'@tailwindcss/oxide-linux-x64-musl': 4.2.2
'@tailwindcss/oxide-wasm32-wasi': 4.2.2
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2
'@tailwindcss/vite@4.2.2(vite@8.0.5(@types/node@22.19.17)(jiti@2.6.1))':
dependencies:
'@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2
tailwindcss: 4.2.2
vite: 8.0.5(@types/node@22.19.17)(jiti@2.6.1)
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
optional: true
'@types/node@22.19.17':
dependencies:
undici-types: 6.21.0
'@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies:
'@types/react': 19.2.14
'@types/react@19.2.14':
dependencies:
csstype: 3.2.3
'@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.17)(jiti@2.6.1))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.5(@types/node@22.19.17)(jiti@2.6.1)
cookie@1.1.1: {}
csstype@3.2.3: {}
detect-libc@2.1.2: {}
enhanced-resolve@5.20.1:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.2
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fsevents@2.3.3:
optional: true
graceful-fs@4.2.11: {}
jiti@2.6.1: {}
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.32.0
lightningcss-darwin-arm64: 1.32.0
lightningcss-darwin-x64: 1.32.0
lightningcss-freebsd-x64: 1.32.0
lightningcss-linux-arm-gnueabihf: 1.32.0
lightningcss-linux-arm64-gnu: 1.32.0
lightningcss-linux-arm64-musl: 1.32.0
lightningcss-linux-x64-gnu: 1.32.0
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
lucide-react@1.52.0(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
nanoid@3.3.19: {}
oxfmt@0.2.0:
optionalDependencies:
'@oxfmt/darwin-arm64': 0.2.0
'@oxfmt/darwin-x64': 0.2.0
'@oxfmt/linux-arm64-gnu': 0.2.0
'@oxfmt/linux-arm64-musl': 0.2.0
'@oxfmt/linux-x64-gnu': 0.2.0
'@oxfmt/linux-x64-musl': 0.2.0
'@oxfmt/win32-arm64': 0.2.0
'@oxfmt/win32-x64': 0.2.0
picocolors@1.1.1: {}
picomatch@4.0.4: {}
postcss@8.5.23:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
source-map-js: 1.2.1
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4
scheduler: 0.27.0
react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
cookie: 1.1.1
react: 19.2.4
set-cookie-parser: 2.7.2
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
react@19.2.4: {}
rolldown@1.0.0-rc.12:
dependencies:
'@oxc-project/types': 0.122.0
'@rolldown/pluginutils': 1.0.0-rc.12
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.12
'@rolldown/binding-darwin-arm64': 1.0.0-rc.12
'@rolldown/binding-darwin-x64': 1.0.0-rc.12
'@rolldown/binding-freebsd-x64': 1.0.0-rc.12
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.12
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.12
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.12
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12
scheduler@0.27.0: {}
set-cookie-parser@2.7.2: {}
source-map-js@1.2.1: {}
tailwindcss@4.2.2: {}
tapable@2.3.2: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tslib@2.8.1:
optional: true
typescript@5.9.3: {}
undici-types@6.21.0: {}
vite@8.0.5(@types/node@22.19.17)(jiti@2.6.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.23
rolldown: 1.0.0-rc.12
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.17
fsevents: 2.3.3
jiti: 2.6.1

50
src/api/articles.ts Normal file
View File

@@ -0,0 +1,50 @@
import { articles, type Article } from "./mockData";
import { apiClient } from "./client";
export type ArticleQuery = {
search?: string;
category?: string;
grade?: string;
sort?: "newest" | "popular" | "shortest";
page?: number;
limit?: number;
};
export type PaginatedArticles = {
items: Article[];
total: number;
page: number;
totalPages: number;
};
// Mock-backed methods keep the UI usable now while matching the future API boundary.
export async function getArticles(query: ArticleQuery = {}): Promise<PaginatedArticles> {
const page = query.page ?? 1;
const limit = query.limit ?? 6;
let result = articles.filter((article) =>
(!query.search || article.title.includes(query.search) || article.excerpt.includes(query.search)) &&
(!query.category || query.category === "همه" || article.category === query.category) &&
(!query.grade || query.grade === "همه پایه‌ها" || article.grade === query.grade || article.grade === "همه پایه‌ها")
);
result = [...result].sort((a, b) => {
if (query.sort === "popular") return b.views - a.views;
if (query.sort === "shortest") return Number.parseInt(a.readTime) - Number.parseInt(b.readTime);
return articles.indexOf(a) - articles.indexOf(b);
});
return {
items: result.slice((page - 1) * limit, page * limit),
total: result.length,
page,
totalPages: Math.max(1, Math.ceil(result.length / limit)),
};
}
export async function getArticleBySlug(slug: string): Promise<Article | undefined> {
return articles.find((article) => article.slug === slug);
}
export async function getArticlesFromApi(query: ArticleQuery = {}) {
return apiClient<PaginatedArticles>("articles", { params: query });
}

11
src/api/categories.ts Normal file
View File

@@ -0,0 +1,11 @@
import { apiClient } from "./client";
export const mockCategories = ["همه", "مهارت مطالعه", "ریاضی", "علوم", "یادگیری", "امتحانات"];
export function getCategories() {
return Promise.resolve(mockCategories);
}
export function getCategoriesFromApi() {
return apiClient<string[]>("categories");
}

31
src/api/client.ts Normal file
View File

@@ -0,0 +1,31 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
type RequestOptions = RequestInit & {
params?: Record<string, string | number | boolean | undefined>;
};
export async function apiClient<T>(path: string, options: RequestOptions = {}): Promise<T> {
if (!API_BASE_URL) {
throw new Error("VITE_API_BASE_URL is not configured");
}
const url = new URL(`${API_BASE_URL.replace(/\/$/, "")}/${path.replace(/^\//, "")}`);
Object.entries(options.params ?? {}).forEach(([key, value]) => {
if (value !== undefined) url.searchParams.set(key, String(value));
});
const response = await fetch(url, {
...options,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
return response.json() as Promise<T>;
}

13
src/api/comments.ts Normal file
View File

@@ -0,0 +1,13 @@
import { apiClient } from "./client";
export type Comment = {
id: string;
articleSlug: string;
author: string;
body: string;
createdAt: string;
};
export function getComments(articleSlug: string) {
return apiClient<Comment[]>(`articles/${articleSlug}/comments`);
}

10
src/api/grades.ts Normal file
View File

@@ -0,0 +1,10 @@
import { gradeItems } from "./mockData";
import { apiClient } from "./client";
export function getGrades() {
return Promise.resolve(gradeItems);
}
export function getGradesFromApi() {
return apiClient<typeof gradeItems>("grades");
}

109
src/api/mockData.ts Normal file
View File

@@ -0,0 +1,109 @@
import { pic } from "@/pic";
export type Article = {
slug: string;
title: string;
excerpt: string;
category: string;
grade: string;
readTime: string;
date: string;
image: string;
imageAlt: string;
color: string;
views: number;
likes: number;
};
export const articles: Article[] = [
{
slug: "smart-study-plan",
title: "چطور یک برنامه درسی بنویسیم که واقعاً انجامش بدهیم؟",
excerpt: "یک راهنمای ساده و کاربردی برای ساختن برنامه‌ای که با زندگی واقعی تو هماهنگ باشد.",
category: "مهارت مطالعه",
grade: "همه پایه‌ها",
readTime: "۸ دقیقه",
date: "۲۴ اردیبهشت ۱۴۰۴",
image: pic.smartStudyPlan,
imageAlt: "دانش‌آموز در حال نوشتن و مطالعه پشت میز",
color: "bg-amber-100 text-amber-700",
views: 2840,
likes: 186,
},
{
slug: "math-without-stress",
title: "ریاضی بدون استرس؛ ۵ روش برای حل مسئله‌های سخت",
excerpt: "با شکستن مسئله‌های پیچیده به قدم‌های کوچک، ریاضی را به یک بازی فکری تبدیل کن.",
category: "ریاضی",
grade: "متوسطه اول",
readTime: "۶ دقیقه",
date: "۲۰ اردیبهشت ۱۴۰۴",
image: pic.mathWithoutStress,
imageAlt: "تخته کلاس با فرمول‌های ریاضی و فیزیک",
color: "bg-blue-100 text-blue-700",
views: 2310,
likes: 142,
},
{
slug: "better-reading",
title: "چگونه سریع‌تر بخوانیم و بیشتر یاد بگیریم؟",
excerpt: "تکنیک‌های خلاصه‌نویسی، مرور فعال و نقشه ذهنی برای یادگیری عمیق‌تر.",
category: "یادگیری",
grade: "متوسطه دوم",
readTime: "۷ دقیقه",
date: "۱۷ اردیبهشت ۱۴۰۴",
image: pic.betterReading,
imageAlt: "یادگیری آنلاین با لپ‌تاپ در محیط آموزشی",
color: "bg-emerald-100 text-emerald-700",
views: 1970,
likes: 128,
},
{
slug: "science-at-home",
title: "سه آزمایش علمی جذاب که در خانه انجام می‌شوند",
excerpt: "با وسایل ساده خانه، مفاهیم مهم علوم را ببین و تجربه کن.",
category: "علوم",
grade: "ابتدایی",
readTime: "۵ دقیقه",
date: "۱۲ اردیبهشت ۱۴۰۴",
image: pic.scienceAtHome,
imageAlt: "معلم در حال کمک به دانش‌آموز برای حل تمرین",
color: "bg-violet-100 text-violet-700",
views: 1640,
likes: 97,
},
{
slug: "exam-night",
title: "شب امتحان چه کارهایی انجام دهیم و چه کارهایی نه؟",
excerpt: "چک‌لیست کوتاهی برای مرور آرام، خواب بهتر و شروع مطمئن امتحان.",
category: "امتحانات",
grade: "همه پایه‌ها",
readTime: "۴ دقیقه",
date: "۸ اردیبهشت ۱۴۰۴",
image: pic.examNight,
imageAlt: "دانش‌آموزان در حال شرکت در آزمون کلاسی",
color: "bg-rose-100 text-rose-700",
views: 3180,
likes: 214,
},
{
slug: "group-study",
title: "درس خواندن گروهی؛ چطور از وقتمان بهتر استفاده کنیم؟",
excerpt: "قانون‌های یک گروه مطالعاتی مؤثر که هم مفید باشد و هم خوش بگذرد.",
category: "مهارت مطالعه",
grade: "متوسطه اول",
readTime: "۶ دقیقه",
date: "۴ اردیبهشت ۱۴۰۴",
image: pic.groupStudy,
imageAlt: "گروهی از دانش‌آموزان در کلاس درس",
color: "bg-orange-100 text-orange-700",
views: 1290,
likes: 84,
},
];
export const gradeItems = [
{ title: "دوره ابتدایی", range: "پایه اول تا ششم", icon: "۱۲۳", color: "bg-amber-50 border-amber-200", accent: "bg-amber-400" },
{ title: "متوسطه اول", range: "پایه هفتم تا نهم", icon: "ABC", color: "bg-emerald-50 border-emerald-200", accent: "bg-emerald-500" },
{ title: "متوسطه دوم", range: "پایه دهم تا دوازدهم", icon: "x²", color: "bg-blue-50 border-blue-200", accent: "bg-blue-500" },
];

6
src/app/App.tsx Normal file
View File

@@ -0,0 +1,6 @@
import { RouterProvider } from "react-router";
import { router } from "./routes";
export default function App() {
return <RouterProvider router={router} />;
}

26
src/app/Layout.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { useEffect } from "react";
import { Outlet, useLocation } from "react-router";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
export function Layout() {
const location = useLocation();
useEffect(() => {
document.documentElement.lang = "fa";
document.documentElement.dir = "rtl";
window.scrollTo({ top: 0, behavior: "smooth" });
}, [location.pathname]);
return (
<div dir="rtl" className="min-h-screen bg-[#fbfaf7] text-slate-800">
<Header />
<main>
<Outlet />
</main>
<Footer />
</div>
);
}

380
src/app/Pages.tsx Normal file
View File

@@ -0,0 +1,380 @@
import { useState } from "react";
import { Link, useParams, useSearchParams } from "react-router";
import {
ArrowLeft,
ArrowRight,
BookMarked,
BookOpen,
Brain,
Calculator,
ChevronLeft,
Clock3,
Compass,
Eye,
FlaskConical,
GraduationCap,
Heart,
Lightbulb,
Mail,
Palette,
Quote,
Search,
Share2,
Sparkles,
Target,
TrendingUp,
Trophy,
Users,
} from "lucide-react";
import { articles, gradeItems } from "@/api/mockData";
import ArticleCard from "@/components/ArticleCard";
import ArticleGrid from "@/components/ArticleGrid";
import Breadcrumb from "@/components/Breadcrumb";
import { RelatedArticles, RecommendedArticles } from "@/components/ArticleCollections";
import { CategoryFilter, GradeFilter, SearchBar, SortFilter } from "@/components/Filters";
import Pagination from "@/components/Pagination";
import SafeImage from "@/components/SafeImage";
import Hero from "@/components/Hero";
import { useArticles } from "@/hooks/useArticles";
import { usePagination } from "@/hooks/usePagination";
import { mockCategories } from "@/api/categories";
function SectionTitle({ eyebrow, title, text }: { eyebrow: string; title: string; text?: string }) {
return (
<div>
<span className="mb-3 inline-flex items-center gap-2 text-sm font-black text-[#5b50d6]">
<span className="h-1.5 w-6 rounded-full bg-[#ffd05b]" />
{eyebrow}
</span>
<h2 className="text-2xl font-black leading-[1.45] text-slate-950 sm:text-3xl">{title}</h2>
{text && <p className="mt-3 max-w-2xl text-sm leading-7 text-slate-500 sm:text-base">{text}</p>}
</div>
);
}
export function Home() {
const categories = [
{ title: "مهارت مطالعه", count: "۳۲ مطلب", icon: Brain, color: "bg-violet-100 text-violet-600" },
{ title: "ریاضی", count: "۲۴ مطلب", icon: Calculator, color: "bg-blue-100 text-blue-600" },
{ title: "علوم", count: "۱۸ مطلب", icon: FlaskConical, color: "bg-emerald-100 text-emerald-600" },
{ title: "امتحانات", count: "۲۱ مطلب", icon: Trophy, color: "bg-amber-100 text-amber-700" },
{ title: "خلاقیت", count: "۱۴ مطلب", icon: Palette, color: "bg-rose-100 text-rose-600" },
{ title: "یادگیری", count: "۲۷ مطلب", icon: BookOpen, color: "bg-cyan-100 text-cyan-700" },
];
return (
<>
<Hero />
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="flex flex-col justify-between gap-5 sm:flex-row sm:items-end">
<SectionTitle eyebrow="منتخب سردبیر" title="مطالب ویژه این هفته" text="پیشنهادهایی کاربردی برای مطالعه بهتر و پیشرفت قدم‌به‌قدم." />
<Link to="/magazine" className="inline-flex shrink-0 items-center gap-1.5 text-sm font-black text-[#5b50d6]">همه مقاله‌ها <ArrowLeft size={16} /></Link>
</div>
<div className="mt-9 grid gap-6 lg:grid-cols-2">
<ArticleCard article={articles[0]} featured />
<div className="grid gap-6 sm:grid-cols-2">
<ArticleCard article={articles[1]} />
<ArticleCard article={articles[2]} />
</div>
</div>
</section>
<section className="border-y border-slate-200 bg-white py-16 lg:py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionTitle eyebrow="موضوعات مجله" title="دنبال چه چیزی هستی؟" text="از میان موضوعات متنوع، مسیر یادگیری مورد علاقه‌ات را انتخاب کن." />
<div className="mt-9 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{categories.map(({ title, count, icon: Icon, color }) => (
<Link to="/magazine" key={title} className="group rounded-[22px] border border-slate-200 bg-[#fbfaf7] p-4 transition hover:-translate-y-1 hover:border-[#bdb7f5] hover:shadow-lg">
<span className={`grid size-11 place-items-center rounded-2xl ${color}`}><Icon size={21} /></span>
<h3 className="mt-4 text-sm font-black text-slate-900">{title}</h3>
<p className="mt-1 text-xs text-slate-400">{count}</p>
</Link>
))}
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="flex items-end justify-between gap-5">
<SectionTitle eyebrow="تازه منتشر شده" title="آخرین مطالب" />
<Link to="/magazine" className="hidden items-center gap-1.5 text-sm font-black text-[#5b50d6] sm:inline-flex">مشاهده همه <ArrowLeft size={16} /></Link>
</div>
<div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{articles.slice(3, 6).map((article) => <ArticleCard key={article.slug} article={article} />)}
</div>
</section>
<section className="border-y border-slate-200 bg-[#f2f0ff] py-16 lg:py-20">
<div className="mx-auto grid max-w-7xl gap-8 px-4 sm:px-6 lg:grid-cols-[1.15fr_.85fr] lg:px-8">
<div>
<SectionTitle eyebrow="پربازدیدها" title="محبوب بین دانش‌آموزها" />
<div className="mt-7 space-y-3">
{articles.slice(0, 4).map((article, index) => (
<Link key={article.slug} to={`/magazine/articles/${article.slug}`} className="group flex items-center gap-4 rounded-2xl border border-white bg-white/80 p-3 transition hover:bg-white hover:shadow-md">
<span className="grid size-10 shrink-0 place-items-center rounded-xl bg-[#eeecff] text-sm font-black text-[#5b50d6]">{(index + 1).toLocaleString("fa-IR")}</span>
<SafeImage src={article.image} alt={article.imageAlt} className="size-16 shrink-0 rounded-xl object-cover" />
<span className="min-w-0 flex-1">
<strong className="line-clamp-1 block text-sm font-black text-slate-900 group-hover:text-[#5b50d6]">{article.title}</strong>
<small className="mt-1 flex items-center gap-1 text-slate-400"><Clock3 size={12} /> {article.readTime}</small>
</span>
<ChevronLeft size={18} className="shrink-0 text-slate-300" />
</Link>
))}
</div>
</div>
<div className="relative overflow-hidden rounded-[30px] bg-[#182038] p-7 text-white sm:p-9">
<div className="absolute -left-12 -top-12 size-40 rounded-full border-[30px] border-white/5" />
<span className="relative inline-flex items-center gap-2 rounded-full bg-[#ffd05b] px-3 py-1.5 text-xs font-black text-slate-900"><TrendingUp size={14} /> پیشنهاد تدریسینو</span>
<h2 className="relative mt-7 text-2xl font-black leading-[1.7]">{articles[2].title}</h2>
<p className="relative mt-3 text-sm leading-7 text-slate-300">{articles[2].excerpt}</p>
<div className="relative mt-8 overflow-hidden rounded-2xl">
<SafeImage src={articles[2].image} alt={articles[2].imageAlt} className="aspect-[16/8] w-full object-cover opacity-85" />
</div>
<Link to={`/magazine/articles/${articles[2].slug}`} className="relative mt-6 inline-flex items-center gap-2 text-sm font-black text-[#ffd05b]">مطالعه مقاله <ArrowLeft size={17} /></Link>
</div>
</div>
</section>
<section className="bg-white py-16 lg:py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionTitle eyebrow="برای هر مرحله" title="پایه‌ات را انتخاب کن" text="محتوایی متناسب با سن، درس‌ها و دغدغه‌های همین روزهایت پیدا کن." />
<div className="mt-9 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{gradeItems.map((item) => (
<Link to="/grades" key={item.title} className={`group relative overflow-hidden rounded-[24px] border p-5 transition hover:-translate-y-1 hover:shadow-lg ${item.color}`}>
<span className={`mb-8 grid size-14 place-items-center rounded-2xl text-lg font-black text-white shadow-md ${item.accent}`}>{item.icon}</span>
<h3 className="text-lg font-black text-slate-900">{item.title}</h3>
<p className="mt-1 text-sm text-slate-500">{item.range}</p>
<ChevronLeft className="absolute bottom-5 left-5 text-slate-400 transition group-hover:-translate-x-1" size={20} />
</Link>
))}
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="relative overflow-hidden rounded-[32px] bg-[#5b50d6] px-6 py-12 text-center text-white sm:px-12">
<div className="absolute -right-10 -top-16 size-52 rounded-full border-[40px] border-white/5" />
<div className="absolute -bottom-20 -left-12 size-60 rounded-full border-[45px] border-[#ffd05b]/15" />
<div className="relative mx-auto max-w-2xl">
<span className="mx-auto grid size-14 place-items-center rounded-2xl bg-white/12"><Mail size={26} /></span>
<h2 className="mt-5 text-2xl font-black sm:text-3xl">یک نکته تازه، هر هفته</h2>
<p className="mt-3 text-sm leading-7 text-indigo-100">به خبرنامه تدریسینو بپیوند و بهترین مقاله‌های آموزشی را در ایمیلت بخوان.</p>
<form className="mx-auto mt-6 flex max-w-lg flex-col gap-2 rounded-2xl bg-white p-2 sm:flex-row" onSubmit={(e) => e.preventDefault()}>
<input type="email" placeholder="ایمیل شما" aria-label="ایمیل شما" className="min-w-0 flex-1 rounded-xl px-4 py-3 text-sm text-slate-900 outline-none" />
<button className="rounded-xl bg-[#ffd05b] px-6 py-3 text-sm font-black text-slate-900">عضویت رایگان</button>
</form>
</div>
</div>
</section>
</>
);
}
export function Magazine() {
const [searchParams] = useSearchParams();
const [query, setQuery] = useState("");
const [category, setCategory] = useState(searchParams.get("category") ?? "همه");
const [grade, setGrade] = useState(searchParams.get("grade") ?? "همه پایه‌ها");
const [sort, setSort] = useState<"newest" | "popular" | "shortest">("newest");
const { page, setPage } = usePagination([query, category, grade, sort]);
const { items, total, totalPages, loading } = useArticles({ search: query, category, grade, sort, page, limit: 3 });
return (
<>
<section className="border-b border-slate-200 bg-[#f1efff]">
<div className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8">
<span className="text-sm font-black text-[#5b50d6]">مجله تدریسینو</span>
<h1 className="mt-3 text-3xl font-black text-slate-950 sm:text-4xl">کشف کن، بخوان، یاد بگیر</h1>
<p className="mt-3 max-w-2xl leading-8 text-slate-600">مجموعه‌ای از مقاله‌های کاربردی برای درس خواندن بهتر، رشد مهارت‌ها و پیدا کردن مسیر خودت.</p>
<div className="mt-7"><SearchBar value={query} onChange={setQuery} /></div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="rounded-3xl border border-slate-200 bg-white p-4 shadow-sm">
<CategoryFilter items={mockCategories} value={category} onChange={setCategory} />
<div className="grid gap-3 border-t border-slate-100 pt-4 sm:grid-cols-2">
<GradeFilter value={grade} onChange={setGrade} />
<SortFilter value={sort} onChange={(value) => setSort(value as typeof sort)} />
</div>
</div>
<div className="mt-7 flex items-center justify-between">
<h2 className="text-xl font-black text-slate-900">{category === "همه" ? "همه مقاله‌ها" : category}</h2>
<span className="text-sm text-slate-400">{total.toLocaleString("fa-IR")} مطلب</span>
</div>
{loading ? (
<div className="mt-7 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">{[1, 2, 3].map((item) => <div key={item} className="h-[430px] animate-pulse rounded-3xl bg-slate-200" />)}</div>
) : items.length ? (
<div className="mt-7"><ArticleGrid articles={items} /><Pagination page={page} totalPages={totalPages} onChange={setPage} /></div>
) : (
<div className="mt-10 rounded-3xl border border-dashed border-slate-300 bg-white py-16 text-center">
<Search className="mx-auto text-slate-300" size={35} />
<p className="mt-4 font-bold text-slate-600">مطلبی با این مشخصات پیدا نشد.</p>
<button onClick={() => { setQuery(""); setCategory("همه"); setGrade("همه پایه‌ها"); setSort("newest"); }} className="mt-3 text-sm font-black text-[#5b50d6]">پاک کردن فیلترها</button>
</div>
)}
</section>
</>
);
}
export function ArticleDetail() {
const { slug } = useParams();
const article = articles.find((item) => item.slug === slug) ?? articles[0];
const [liked, setLiked] = useState(false);
const related = articles.filter((item) => item.slug !== article.slug && item.category === article.category);
const recommended = articles.filter((item) => item.slug !== article.slug && item.category !== article.category).slice(0, 3);
return (
<article>
<header className="border-b border-slate-200 bg-white">
<div className="mx-auto max-w-4xl px-4 py-12 text-center sm:px-6 sm:py-16">
<Breadcrumb items={[{ label: "خانه", to: "/" }, { label: "مجله", to: "/magazine" }, { label: article.title }]} />
<div className="mt-6 flex justify-center gap-2"><span className={`rounded-full px-3 py-1.5 text-xs font-black ${article.color}`}>{article.category}</span></div>
<h1 className="mx-auto mt-5 max-w-3xl text-3xl font-black leading-[1.55] text-slate-950 sm:text-4xl lg:text-5xl">{article.title}</h1>
<p className="mx-auto mt-5 max-w-2xl leading-8 text-slate-500">{article.excerpt}</p>
<div className="mt-7 flex flex-wrap items-center justify-center gap-4 text-xs font-bold text-slate-400">
<span className="inline-flex items-center gap-2"><span className="grid size-8 place-items-center rounded-full bg-[#5b50d6] text-[10px] font-black text-white">تـ</span>تیم تحریریه تدریسینو</span><span className="size-1 rounded-full bg-slate-300" /><span>{article.date}</span><span className="size-1 rounded-full bg-slate-300" /><span className="inline-flex items-center gap-1"><Clock3 size={14} /> {article.readTime} مطالعه</span><span className="size-1 rounded-full bg-slate-300" /><span className="inline-flex items-center gap-1"><Eye size={14} /> {article.views.toLocaleString("fa-IR")} بازدید</span><span className="inline-flex items-center gap-1"><Heart size={14} /> {(article.likes + (liked ? 1 : 0)).toLocaleString("fa-IR")}</span>
</div>
</div>
</header>
<div className="mx-auto max-w-5xl px-4 sm:px-6">
<SafeImage src={article.image} alt={article.imageAlt} className="mt-10 aspect-[16/8] w-full rounded-[28px] object-cover shadow-[0_20px_60px_rgba(30,30,60,.12)]" />
</div>
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-12 sm:px-6 lg:grid-cols-[220px_minmax(0,1fr)_56px] lg:px-8">
<aside className="order-first">
<div className="sticky top-24 rounded-2xl border border-slate-200 bg-white p-5">
<strong className="text-sm font-black text-slate-900">فهرست مطالب</strong>
<nav className="mt-4 flex flex-col gap-3 border-r-2 border-[#e5e1ff] pr-3 text-xs font-bold leading-6 text-slate-500">
<a href="#reality" className="hover:text-[#5b50d6]">واقع‌بینانه برنامه‌ریزی کن</a>
<a href="#goals" className="hover:text-[#5b50d6]">هدف‌های کوچک انتخاب کن</a>
<a href="#review" className="hover:text-[#5b50d6]">هر هفته برنامه را مرور کن</a>
</nav>
</div>
</aside>
<div className="article-body min-w-0 text-[17px] leading-9 text-slate-700">
<p className="lead">همه ما حداقل یک‌بار برنامه‌ای نوشته‌ایم که روی کاغذ عالی به نظر می‌رسید، اما بعد از چند روز فراموش شد. مشکل از اراده ما نیست؛ بیشتر وقت‌ها برنامه با زندگی واقعی‌مان هماهنگ نیست.</p>
<h2 id="reality" className="scroll-mt-28">۱. اول واقعیت را ببین، بعد برنامه بنویس</h2>
<p>قبل از اینکه ساعت‌های روز را پر کنی، یک هفته معمولی خودت را مرور کن. زمان مدرسه، استراحت، رفت‌وآمد و کارهای شخصی را بنویس. برنامه خوب قرار نیست تمام روز تو را بگیرد؛ قرار است از زمان‌های آزاد به بهترین شکل استفاده کند.</p>
<div className="my-8 rounded-3xl border border-amber-200 bg-amber-50 p-6">
<div className="flex gap-3"><span className="grid size-10 shrink-0 place-items-center rounded-xl bg-[#ffd05b]"><Lightbulb size={20} /></span><div><strong className="font-black text-slate-900">نکته تدریسینو</strong><p className="mt-1 text-sm leading-7">فقط ۷۰ درصد زمان آزادت را برنامه‌ریزی کن. آن ۳۰ درصد باقی‌مانده برای اتفاق‌های پیش‌بینی‌نشده است.</p></div></div>
</div>
<h2 id="goals" className="scroll-mt-28">۲. هدف‌های کوچک و روشن انتخاب کن</h2>
<p>«امروز ریاضی بخوانم» یک هدف مبهم است. به‌جای آن بنویس: «تمرین‌های ۱ تا ۸ صفحه ۴۲ را حل کنم». هرچه هدف روشن‌تر باشد، شروع کردن آسان‌تر می‌شود.</p>
<ul>
<li>هر بازه مطالعه بین ۲۵ تا ۵۰ دقیقه باشد.</li>
<li>بین دو بازه، ۵ تا ۱۰ دقیقه استراحت کن.</li>
<li>درس‌های سخت و آسان را پشت سر هم قرار بده.</li>
<li>برای پایان هر روز، یک مرور کوتاه بگذار.</li>
</ul>
<blockquote><Quote size={27} /><p>برنامه خوب، برنامه‌ای نیست که فشرده‌تر باشد؛ برنامه‌ای است که بتوانی ادامه‌اش بدهی.</p></blockquote>
<h2 id="review" className="scroll-mt-28">۳. برنامه را هر هفته بهتر کن</h2>
<p>در پایان هفته از خودت بپرس کدام بخش‌ها انجام شدند و کجاها به زمان بیشتری نیاز داشتی. تغییر دادن برنامه نشانه شکست نیست؛ یعنی داری روش مناسب خودت را پیدا می‌کنی.</p>
<div className="mt-10 flex flex-wrap items-center justify-between gap-4 border-t border-slate-200 pt-6">
<div className="flex gap-2">{["برنامه‌ریزی", "مطالعه", "مدیریت زمان"].map((tag) => <span key={tag} className="rounded-full bg-slate-100 px-3 py-1.5 text-xs font-bold text-slate-500">#{tag}</span>)}</div>
<button onClick={() => setLiked((value) => !value)} className={`inline-flex items-center gap-2 rounded-xl border px-4 py-2 text-sm font-bold ${liked ? "border-rose-200 bg-rose-50 text-rose-600" : "border-slate-200"}`}><Heart size={17} fill={liked ? "currentColor" : "none"} /> {liked ? "پسندیده شد" : "پسندیدم"}</button>
</div>
</div>
<aside className="flex gap-2 lg:flex-col">
<button aria-label="اشتراک‌گذاری" className="grid size-11 place-items-center rounded-xl border border-slate-200 bg-white text-slate-500 hover:text-[#5b50d6]"><Share2 size={18} /></button>
<button aria-label="ذخیره مقاله" className="grid size-11 place-items-center rounded-xl border border-slate-200 bg-white text-slate-500 hover:text-[#5b50d6]"><BookMarked size={18} /></button>
</aside>
</div>
<section className="border-t border-slate-200 bg-white py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionTitle eyebrow="در همین موضوع" title="مقالات مرتبط" />
<div className="mt-8"><RelatedArticles articles={related.length ? related : articles.slice(1, 4)} /></div>
</div>
</section>
<section className="bg-[#f2f0ff] py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionTitle eyebrow="انتخاب تدریسینو" title="پیشنهاد می‌کنیم این‌ها را هم بخوانی" />
<div className="mt-8"><RecommendedArticles articles={recommended} /></div>
</div>
</section>
</article>
);
}
export function Grades() {
const features = [
{ icon: Brain, title: "روش مطالعه", text: "تکنیک‌های متناسب با درس‌ها و سن تو" },
{ icon: Target, title: "هدف‌گذاری", text: "مسیرهای کوچک و قابل انجام برای پیشرفت" },
{ icon: BookOpen, title: "آموزش مفهومی", text: "یادگیری ساده و قابل فهم موضوعات سخت" },
];
return (
<>
<section className="relative overflow-hidden bg-[#172038] py-16 text-white sm:py-20">
<div className="absolute inset-0 opacity-10 hero-grid-light" />
<div className="relative mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
<span className="mx-auto grid size-14 place-items-center rounded-2xl bg-[#ffd05b] text-slate-900"><GraduationCap size={29} /></span>
<h1 className="mt-6 text-3xl font-black sm:text-4xl">محتوای مناسب پایه خودت را پیدا کن</h1>
<p className="mx-auto mt-4 max-w-2xl leading-8 text-slate-300">از اولین سال‌های مدرسه تا انتخاب مسیر آینده، تدریسینو کنار توست.</p>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="grid gap-5 md:grid-cols-3">
{gradeItems.map((item, index) => (
<div key={item.title} className={`relative overflow-hidden rounded-[28px] border p-7 sm:p-8 ${item.color}`}>
<span className={`grid size-16 place-items-center rounded-[20px] text-xl font-black text-white shadow-md ${item.accent}`}>{item.icon}</span>
<h2 className="mt-6 text-2xl font-black text-slate-950">{item.title}</h2>
<p className="mt-1 text-sm font-bold text-slate-500">{item.range}</p>
<p className="mt-4 max-w-md text-sm leading-7 text-slate-600">{["یادگیری شیرین و پایه‌ای با داستان، بازی و تجربه‌های ساده برای ساختن یک شروع محکم.", "راهنمای عبور از تغییرات این دوره، شناخت بهتر درس‌ها و پیدا کردن روش مطالعه شخصی.", "محتوای تخصصی‌تر برای درس‌ها، امتحانات نهایی و تصمیم‌های مهم تحصیلی."][index]}</p>
<Link to={`/magazine?grade=${encodeURIComponent(item.title.replace("دوره ", ""))}`} className="mt-6 inline-flex items-center gap-2 rounded-xl bg-white px-4 py-2.5 text-sm font-black text-slate-800 shadow-sm">مشاهده مطالب <ArrowLeft size={16} /></Link>
<span className="absolute -bottom-10 -left-8 text-[130px] font-black text-slate-900/[.035]">{index + 1}</span>
</div>
))}
</div>
</section>
<section className="bg-white py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionTitle eyebrow="چه چیزی پیدا می‌کنی؟" title="همراهی برای هر قدم یادگیری" />
<div className="mt-9 grid gap-5 md:grid-cols-3">
{features.map(({ icon: Icon, title, text }) => <div key={title} className="rounded-3xl border border-slate-200 p-6"><span className="grid size-12 place-items-center rounded-2xl bg-[#eeecff] text-[#5b50d6]"><Icon size={23} /></span><h3 className="mt-5 font-black text-slate-900">{title}</h3><p className="mt-2 text-sm leading-7 text-slate-500">{text}</p></div>)}
</div>
</div>
</section>
</>
);
}
export function About() {
const values = [
{ icon: Eye, title: "ساده و روشن", text: "مفاهیم را پیچیده نمی‌کنیم؛ هر مطلب باید قابل فهم و قابل اجرا باشد." },
{ icon: Heart, title: "همدلانه", text: "دغدغه‌ها و تجربه واقعی دانش‌آموزان، نقطه شروع محتوای ماست." },
{ icon: Compass, title: "مستقل و کنجکاو", text: "کمک می‌کنیم هر دانش‌آموز راه یادگیری خودش را پیدا کند." },
{ icon: Users, title: "برای همه", text: "دانش خوب باید رایگان و در دسترس همه دانش‌آموزان باشد." },
];
return (
<>
<section className="mx-auto grid max-w-7xl items-center gap-12 px-4 py-16 sm:px-6 lg:grid-cols-2 lg:px-8 lg:py-24">
<div>
<span className="text-sm font-black text-[#5b50d6]">داستان تدریسینو</span>
<h1 className="mt-4 text-4xl font-black leading-[1.5] text-slate-950 sm:text-5xl">ما باور داریم هر کسی می‌تواند <span className="text-[#5b50d6]">یادگیرنده خوبی</span> باشد.</h1>
<p className="mt-6 leading-8 text-slate-600">تدریسینو از یک سؤال ساده شروع شد: چرا یادگیری، با همه جذابیتی که دارد، گاهی خسته‌کننده و دشوار به نظر می‌رسد؟ ما اینجا هستیم تا با محتوای روشن، کاربردی و صمیمی، جواب تازه‌ای به این سؤال بدهیم.</p>
</div>
<div className="relative">
<SafeImage src={articles[4].image} alt={articles[4].imageAlt} className="aspect-[4/3] w-full rounded-[32px] object-cover shadow-xl" />
<div className="absolute -bottom-6 right-6 rounded-2xl bg-[#ffd05b] p-5 shadow-lg"><strong className="block text-2xl font-black">از ۱۴۰۱</strong><span className="text-xs font-bold text-slate-700">کنار دانش‌آموزان</span></div>
</div>
</section>
<section className="border-y border-slate-200 bg-white py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionTitle eyebrow="ارزش‌های ما" title="چیزی که برایمان مهم است" />
<div className="mt-9 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{values.map(({ icon: Icon, title, text }) => <div key={title} className="rounded-3xl bg-[#fbfaf7] p-6"><span className="grid size-12 place-items-center rounded-2xl bg-[#eeecff] text-[#5b50d6]"><Icon size={23} /></span><h3 className="mt-5 font-black text-slate-900">{title}</h3><p className="mt-2 text-sm leading-7 text-slate-500">{text}</p></div>)}
</div>
</div>
</section>
<section className="mx-auto max-w-4xl px-4 py-20 text-center sm:px-6">
<span className="mx-auto grid size-14 place-items-center rounded-2xl bg-emerald-100 text-emerald-600"><Sparkles size={26} /></span>
<h2 className="mt-5 text-3xl font-black text-slate-950">بیایید یادگیری را دوباره کشف کنیم</h2>
<p className="mx-auto mt-4 max-w-2xl leading-8 text-slate-500">مهم نیست در کدام پایه هستی یا چه هدفی داری؛ در مجله تدریسینو همیشه چیزی برای شروع کردن پیدا می‌کنی.</p>
<Link to="/magazine" className="mt-7 inline-flex items-center gap-2 rounded-2xl bg-[#5b50d6] px-6 py-3.5 text-sm font-black text-white shadow-[0_5px_0_#4038a8]">مطالعه را شروع کن <ArrowLeft size={18} /></Link>
</section>
</>
);
}
export function NotFound() {
return <section className="mx-auto max-w-2xl px-4 py-28 text-center"><span className="text-7xl font-black text-[#d8d3ff]">۴۰۴</span><h1 className="mt-5 text-3xl font-black">این صفحه را پیدا نکردیم</h1><p className="mt-3 text-slate-500">شاید آدرس تغییر کرده یا صفحه به جای دیگری رفته باشد.</p><Link to="/" className="mt-7 inline-flex rounded-xl bg-[#5b50d6] px-5 py-3 text-sm font-black text-white">بازگشت به خانه</Link></section>;
}

24
src/app/routes.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { createBrowserRouter } from "react-router";
import { Layout } from "./Layout";
import HomePage from "@/pages/HomePage";
import MagazinePage from "@/pages/MagazinePage";
import ArticleDetailPage from "@/pages/ArticleDetailPage";
import GradesPage from "@/pages/GradesPage";
import AboutPage from "@/pages/AboutPage";
import NotFoundPage from "@/pages/NotFoundPage";
export const router = createBrowserRouter([
{
path: "/",
Component: Layout,
children: [
{ index: true, Component: HomePage },
{ path: "magazine", Component: MagazinePage },
{ path: "magazine/articles/:slug", Component: ArticleDetailPage },
{ path: "article/:slug", Component: ArticleDetailPage },
{ path: "grades", Component: GradesPage },
{ path: "about", Component: AboutPage },
{ path: "*", Component: NotFoundPage },
],
},
]);

View File

@@ -0,0 +1,27 @@
import { ArrowLeft, Clock3 } from "lucide-react";
import { Link } from "react-router";
import type { Article } from "@/api/mockData";
import SafeImage from "./SafeImage";
export default function ArticleCard({ article, featured = false }: { article: Article; featured?: boolean }) {
return (
<article className={`group flex h-full overflow-hidden rounded-[24px] border border-slate-200 bg-white transition duration-300 hover:-translate-y-1 hover:shadow-[0_18px_50px_rgba(42,36,91,.10)] ${featured ? "sm:grid sm:grid-cols-[1.05fr_.95fr]" : "flex-col"}`}>
<Link to={`/magazine/articles/${article.slug}`} className={`relative block shrink-0 overflow-hidden bg-[#eeecff] ${featured ? "min-h-[280px]" : "aspect-[16/10] w-full"}`}>
<SafeImage src={article.image} alt={article.imageAlt} className="h-full w-full object-cover transition duration-500 group-hover:scale-105" />
<span className={`absolute right-4 top-4 rounded-full px-3 py-1.5 text-xs font-black ${article.color}`}>{article.category}</span>
</Link>
<div className={`flex min-w-0 flex-1 flex-col ${featured ? "justify-center p-6 sm:p-8" : "p-5"}`}>
<div className="mb-3 flex flex-wrap items-center gap-2.5 text-xs font-bold text-slate-400">
<span>{article.grade}</span><span className="size-1 rounded-full bg-slate-300" /><span className="inline-flex items-center gap-1"><Clock3 size={13} /> {article.readTime}</span>
</div>
<h3 className={`${featured ? "text-2xl" : "line-clamp-2 min-h-[3.7rem] text-lg"} font-black leading-[1.65] text-slate-900 transition group-hover:text-[#5146c7]`}>
<Link to={`/magazine/articles/${article.slug}`}>{article.title}</Link>
</h3>
<p className={`mt-2 line-clamp-2 text-sm leading-7 text-slate-500 ${featured ? "" : "min-h-14"}`}>{article.excerpt}</p>
<Link to={`/magazine/articles/${article.slug}`} className="mt-auto inline-flex items-center gap-1.5 pt-5 text-sm font-black text-[#5b50d6]">
ادامه مطلب <ArrowLeft size={16} className="transition group-hover:-translate-x-1" />
</Link>
</div>
</article>
);
}

View File

@@ -0,0 +1,12 @@
import { Link } from "react-router";
import type { Article } from "@/api/mockData";
import ArticleGrid from "./ArticleGrid";
import SafeImage from "./SafeImage";
export function RelatedArticles({ articles }: { articles: Article[] }) {
return <ArticleGrid articles={articles.slice(0, 3)} />;
}
export function RecommendedArticles({ articles }: { articles: Article[] }) {
return <div className="grid gap-4 lg:grid-cols-3">{articles.slice(0, 3).map((item) => <Link key={item.slug} to={`/magazine/articles/${item.slug}`} className="group flex items-center gap-4 rounded-2xl bg-white p-3 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"><SafeImage src={item.image} alt={item.imageAlt} className="size-20 shrink-0 rounded-xl object-cover" /><span><small className="font-bold text-[#5b50d6]">{item.category}</small><strong className="mt-1 line-clamp-2 block text-sm leading-6 text-slate-900">{item.title}</strong></span></Link>)}</div>;
}

View File

@@ -0,0 +1,6 @@
import type { Article } from "@/api/mockData";
import ArticleCard from "./ArticleCard";
export default function ArticleGrid({ articles }: { articles: Article[] }) {
return <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">{articles.map((article) => <ArticleCard key={article.slug} article={article} />)}</div>;
}

View File

@@ -0,0 +1,11 @@
import { BookOpen } from "lucide-react";
import { Link } from "react-router";
export default function BrandLogo({ inverse = false }: { inverse?: boolean }) {
return (
<Link to="/" className="group flex items-center gap-2.5" aria-label="تدریسینو، صفحه اصلی">
<span className="grid size-10 rotate-2 place-items-center rounded-[13px] bg-[#5b50d6] text-white shadow-[0_7px_0_#dcd9ff] transition-transform group-hover:-rotate-2"><BookOpen size={22} strokeWidth={2.2} /></span>
<span><strong className={`block text-xl font-black leading-5 ${inverse ? "text-white" : "text-slate-900"}`}>تدریسینو</strong><span className="text-[10px] font-bold tracking-tight text-slate-400">مجله یادگیری بهتر</span></span>
</Link>
);
}

View File

@@ -0,0 +1,8 @@
import { ChevronLeft } from "lucide-react";
import { Link } from "react-router";
export type BreadcrumbItem = { label: string; to?: string };
export default function Breadcrumb({ items }: { items: BreadcrumbItem[] }) {
return <nav className="flex flex-wrap items-center justify-center gap-2 text-xs font-bold text-slate-400" aria-label="مسیر صفحه">{items.map((item, index) => <span key={item.label} className="inline-flex items-center gap-2">{item.to ? <Link to={item.to} className="hover:text-[#5b50d6]">{item.label}</Link> : <span>{item.label}</span>}{index < items.length - 1 && <ChevronLeft size={13} />}</span>)}</nav>;
}

View File

@@ -0,0 +1,21 @@
import { Search } from "lucide-react";
export function SearchBar({ value, onChange }: { value: string; onChange: (value: string) => void }) {
return <label className="flex max-w-2xl items-center gap-3 rounded-2xl border border-white bg-white px-4 py-3 shadow-sm focus-within:border-[#8e85eb]"><Search size={20} className="text-slate-400" /><input value={value} onChange={(event) => onChange(event.target.value)} placeholder="دنبال چه موضوعی می‌گردی؟" className="w-full bg-transparent text-sm outline-none" /></label>;
}
export function CategoryFilter({ items, value, onChange }: { items: string[]; value: string; onChange: (value: string) => void }) {
return <div className="no-scrollbar flex gap-2 overflow-x-auto pb-4">{items.map((item) => <button key={item} onClick={() => onChange(item)} className={`shrink-0 rounded-full px-4 py-2 text-sm font-bold transition ${value === item ? "bg-[#5b50d6] text-white" : "border border-slate-200 bg-white text-slate-600 hover:border-[#8e85eb]"}`}>{item}</button>)}</div>;
}
function SelectFilter({ label, value, items, onChange }: { label: string; value: string; items: { value: string; label: string }[]; onChange: (value: string) => void }) {
return <label><span className="mb-2 block text-xs font-black text-slate-500">{label}</span><select value={value} onChange={(event) => onChange(event.target.value)} className="w-full rounded-xl border border-slate-200 bg-[#fbfaf7] px-3 py-2.5 text-sm font-bold text-slate-700 outline-none focus:border-[#8e85eb]">{items.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></label>;
}
export function GradeFilter({ value, onChange }: { value: string; onChange: (value: string) => void }) {
return <SelectFilter label="پایه تحصیلی" value={value} onChange={onChange} items={["همه پایه‌ها", "ابتدایی", "متوسطه اول", "متوسطه دوم"].map((item) => ({ value: item, label: item }))} />;
}
export function SortFilter({ value, onChange }: { value: string; onChange: (value: string) => void }) {
return <SelectFilter label="مرتب‌سازی" value={value} onChange={onChange} items={[{ value: "newest", label: "جدیدترین" }, { value: "popular", label: "پربازدیدترین" }, { value: "shortest", label: "کوتاه‌ترین زمان مطالعه" }]} />;
}

17
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { AtSign, Send } from "lucide-react";
import { Link } from "react-router";
import BrandLogo from "./BrandLogo";
import { navItems } from "./Header";
export default function Footer() {
return (
<footer className="mt-20 bg-[#171a2b] text-white">
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 md:grid-cols-2 lg:grid-cols-[1.5fr_1fr_1fr] lg:px-8">
<div><BrandLogo inverse /><p className="mt-5 max-w-md text-sm leading-7 text-slate-400">تدریسینو یک مجله آموزشی رایگان برای دانش‌آموزانی است که می‌خواهند هوشمندانه‌تر یاد بگیرند، نه سخت‌تر.</p><div className="mt-5 flex gap-2"><a href="#" aria-label="اینستاگرام" className="grid size-10 place-items-center rounded-xl bg-white/8 text-slate-300 hover:bg-[#5b50d6] hover:text-white"><AtSign size={19} /></a><a href="#" aria-label="تلگرام" className="grid size-10 place-items-center rounded-xl bg-white/8 text-slate-300 hover:bg-[#5b50d6] hover:text-white"><Send size={18} /></a></div></div>
<div><h3 className="mb-4 font-black">دسترسی سریع</h3><div className="flex flex-col gap-3 text-sm text-slate-400">{navItems.map((item) => <Link key={item.to} to={item.to} className="hover:text-white">{item.label}</Link>)}</div></div>
<div><h3 className="mb-4 font-black">موضوعات محبوب</h3><div className="flex flex-col gap-3 text-sm text-slate-400"><Link to="/magazine?category=مهارت مطالعه" className="hover:text-white">مهارت‌های مطالعه</Link><Link to="/magazine?category=امتحانات" className="hover:text-white">امتحانات و کنکور</Link><Link to="/magazine?category=علوم" className="hover:text-white">علوم و فناوری</Link></div></div>
</div>
<div className="border-t border-white/8"><div className="mx-auto flex max-w-7xl flex-col gap-2 px-4 py-5 text-xs text-slate-500 sm:flex-row sm:items-center sm:justify-between sm:px-6 lg:px-8"><p>© ۱۴۰۴ تدریسینو؛ یادگیری برای همه.</p><p>با کنجکاوی ساخته شده برای دانش‌آموزان ایران</p></div></div>
</footer>
);
}

29
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { useState } from "react";
import { ArrowLeft, Menu, Search, X } from "lucide-react";
import { Link, NavLink } from "react-router";
import BrandLogo from "./BrandLogo";
export const navItems = [
{ to: "/", label: "خانه", end: true },
{ to: "/magazine", label: "مجله" },
{ to: "/grades", label: "پایه‌های تحصیلی" },
{ to: "/about", label: "درباره ما" },
];
export default function Header() {
const [menuOpen, setMenuOpen] = useState(false);
return (
<header className="sticky top-0 z-50 border-b border-slate-200/70 bg-[#fbfaf7]/90 backdrop-blur-xl">
<div className="mx-auto flex h-[76px] max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
<BrandLogo />
<nav className="hidden items-center gap-1 lg:flex" aria-label="منوی اصلی">{navItems.map((item) => <NavLink key={item.to} to={item.to} end={item.end} className={({ isActive }) => `rounded-xl px-4 py-2.5 text-sm font-bold transition-colors ${isActive ? "bg-[#eeecff] text-[#5146c7]" : "text-slate-600 hover:bg-white hover:text-slate-950"}`}>{item.label}</NavLink>)}</nav>
<div className="hidden items-center gap-2 lg:flex">
<Link to="/magazine" className="grid size-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 transition hover:border-[#5b50d6] hover:text-[#5b50d6]" aria-label="جستجو در مجله"><Search size={19} /></Link>
<Link to="/magazine" className="inline-flex items-center gap-2 rounded-xl bg-[#ffd05b] px-4 py-2.5 text-sm font-black text-slate-900 shadow-[0_4px_0_#dcae3f] transition hover:-translate-y-0.5">شروع مطالعه <ArrowLeft size={17} /></Link>
</div>
<button className="grid size-10 place-items-center rounded-xl border border-slate-200 bg-white lg:hidden" onClick={() => setMenuOpen((value) => !value)} aria-label={menuOpen ? "بستن منو" : "باز کردن منو"}>{menuOpen ? <X size={21} /> : <Menu size={21} />}</button>
</div>
{menuOpen && <nav className="border-t border-slate-200 bg-white px-4 py-3 lg:hidden"><div className="mx-auto flex max-w-7xl flex-col gap-1">{navItems.map((item) => <NavLink key={item.to} to={item.to} end={item.end} onClick={() => setMenuOpen(false)} className={({ isActive }) => `rounded-xl px-4 py-3 text-sm font-bold ${isActive ? "bg-[#eeecff] text-[#5146c7]" : "text-slate-700"}`}>{item.label}</NavLink>)}</div></nav>}
</header>
);
}

23
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { ArrowLeft, Check, GraduationCap, Lightbulb, Monitor, PenLine, Sparkles } from "lucide-react";
import { Link } from "react-router";
export default function Hero() {
return (
<section className="relative overflow-hidden border-b border-slate-200/70">
<div className="hero-grid absolute inset-0 opacity-50" /><div className="absolute -right-24 top-24 size-72 rounded-full bg-[#e5e1ff] blur-3xl" /><div className="absolute -left-20 bottom-0 size-72 rounded-full bg-[#fff0ba] blur-3xl" />
<div className="relative mx-auto grid max-w-7xl items-center gap-12 px-4 py-14 sm:px-6 md:py-20 lg:grid-cols-[1.02fr_.98fr] lg:px-8 lg:py-24">
<div>
<span className="inline-flex items-center gap-2 rounded-full border border-[#d8d3ff] bg-white px-3.5 py-2 text-xs font-black text-[#5b50d6] shadow-sm"><Sparkles size={15} className="text-amber-500" />یادگیری ساده‌تر، آینده روشن‌تر</span>
<h1 className="mt-6 max-w-2xl text-4xl font-black leading-[1.45] tracking-tight text-slate-950 sm:text-5xl lg:text-[58px]">مجله آموزشی <span className="relative mr-3 inline-block text-[#5b50d6]">تدریسینو<svg className="absolute -bottom-1 right-0 h-3 w-full text-[#ffd05b]" viewBox="0 0 190 12" fill="none" aria-hidden="true"><path d="M3 8.5C44 2 125 2 187 7" stroke="currentColor" strokeWidth="6" strokeLinecap="round"/></svg></span></h1>
<p className="mt-6 max-w-xl text-base leading-8 text-slate-600 sm:text-lg">مطالب آموزشی، نکات درسی و محتوای کاربردی برای مسیر بهتر یادگیری</p>
<div className="mt-8 flex flex-wrap gap-3"><Link to="/magazine" className="inline-flex items-center gap-2 rounded-2xl bg-[#5b50d6] px-6 py-3.5 text-sm font-black text-white shadow-[0_6px_0_#4038a8] transition hover:-translate-y-0.5">مطالعه مطالب <ArrowLeft size={18} /></Link><Link to="/grades" className="inline-flex items-center gap-2 rounded-2xl border border-slate-200 bg-white px-6 py-3.5 text-sm font-black text-slate-800 shadow-sm transition hover:border-[#5b50d6]">انتخاب پایه تحصیلی <GraduationCap size={19} /></Link></div>
<div className="mt-9 flex items-center gap-6 border-t border-slate-200 pt-6"><div><strong className="block text-xl font-black text-slate-900">+۱۲۰</strong><span className="text-xs text-slate-500">مقاله کاربردی</span></div><div className="h-9 w-px bg-slate-200" /><div><strong className="block text-xl font-black text-slate-900">۱۲ پایه</strong><span className="text-xs text-slate-500">محتوای متنوع</span></div><div className="h-9 w-px bg-slate-200" /><div><strong className="block text-xl font-black text-slate-900">رایگان</strong><span className="text-xs text-slate-500">برای همه</span></div></div>
</div>
<div className="relative mx-auto w-full max-w-[540px] lg:mx-0" role="img" aria-label="تصویرسازی دانش‌آموز در حال مطالعه با لپ‌تاپ و کتاب">
<div className="relative aspect-[1.12/1] overflow-hidden rounded-[40px] border-[7px] border-white bg-[#dedaff] shadow-[0_30px_80px_rgba(56,48,118,.20)]"><div className="absolute inset-x-0 bottom-0 h-[31%] bg-[#f6c957]" /><div className="absolute left-[9%] top-[10%] grid size-16 -rotate-6 place-items-center rounded-2xl bg-white text-[#5b50d6] shadow-lg"><span className="text-xl font-black">x²</span></div><div className="absolute right-[8%] top-[12%] grid size-14 rotate-6 place-items-center rounded-full bg-emerald-400 text-white shadow-lg"><Lightbulb size={27} /></div><div className="absolute left-[17%] top-[23%] h-[45%] w-[67%]"><div className="absolute left-1/2 top-0 size-[29%] -translate-x-1/2 rounded-full bg-[#f2b38c]" /><div className="absolute left-[39%] top-[12%] h-[12%] w-[24%] rounded-t-full bg-slate-800" /><div className="absolute bottom-0 left-1/2 h-[69%] w-[64%] -translate-x-1/2 rounded-t-[45%] bg-[#5b50d6]" /><div className="absolute bottom-[8%] left-[5%] h-[17%] w-[42%] -rotate-[20deg] rounded-full bg-[#f2b38c]" /><div className="absolute bottom-[8%] right-[5%] h-[17%] w-[42%] rotate-[20deg] rounded-full bg-[#f2b38c]" /></div><div className="absolute bottom-[17%] left-1/2 h-[27%] w-[58%] -translate-x-1/2 rounded-xl border-[7px] border-slate-700 bg-white shadow-xl"><div className="grid h-full place-items-center text-[#5b50d6]"><Monitor size={38} strokeWidth={1.8} /></div></div><div className="absolute bottom-[14%] left-[17%] h-3 w-[66%] rounded-full bg-slate-700" /><div className="absolute bottom-[7%] right-[5%] flex -rotate-6"><span className="h-5 w-20 rounded-sm bg-emerald-400" /><span className="h-5 w-20 rounded-sm bg-white" /></div></div>
<div className="float-card absolute -right-3 top-1/2 grid size-16 -rotate-6 place-items-center rounded-2xl bg-white text-[#5b50d6] shadow-xl sm:-right-8"><PenLine size={29} strokeWidth={2.2} /></div><div className="float-card-delayed absolute -bottom-5 -left-2 flex items-center gap-3 rounded-2xl bg-white px-4 py-3 shadow-xl sm:-left-8"><span className="grid size-10 place-items-center rounded-xl bg-emerald-100 text-emerald-600"><Check size={20} strokeWidth={3} /></span><span><strong className="block text-sm font-black">یادگیری امروز</strong><small className="text-slate-400">یک قدم رو به جلو</small></span></div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,6 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
export default function Pagination({ page, totalPages, onChange }: { page: number; totalPages: number; onChange: (page: number) => void }) {
if (totalPages <= 1) return null;
return <nav className="mt-10 flex items-center justify-center gap-2" aria-label="صفحه‌بندی"><button disabled={page === 1} onClick={() => onChange(page - 1)} className="grid size-10 place-items-center rounded-xl border border-slate-200 bg-white disabled:opacity-40"><ChevronRight size={18} /></button>{Array.from({ length: totalPages }, (_, index) => index + 1).map((item) => <button key={item} onClick={() => onChange(item)} className={`size-10 rounded-xl text-sm font-black ${page === item ? "bg-[#5b50d6] text-white" : "border border-slate-200 bg-white"}`}>{item.toLocaleString("fa-IR")}</button>)}<button disabled={page === totalPages} onClick={() => onChange(page + 1)} className="grid size-10 place-items-center rounded-xl border border-slate-200 bg-white disabled:opacity-40"><ChevronLeft size={18} /></button></nav>;
}

View File

@@ -0,0 +1,23 @@
import { useState } from "react";
import { BookOpen } from "lucide-react";
type SafeImageProps = {
src: string;
alt: string;
className: string;
eager?: boolean;
};
export default function SafeImage({ src, alt, className, eager = false }: SafeImageProps) {
const [failed, setFailed] = useState(false);
if (failed) {
return (
<div className={`grid place-items-center bg-gradient-to-br from-[#eeecff] to-[#fff3c9] text-[#5b50d6] ${className}`} role="img" aria-label={alt}>
<BookOpen size={26} strokeWidth={1.8} />
</div>
);
}
return <img src={src} alt={alt} className={className} loading={eager ? "eager" : "lazy"} onError={() => setFailed(true)} />;
}

25
src/hooks/useArticles.ts Normal file
View File

@@ -0,0 +1,25 @@
import { useEffect, useState } from "react";
import { getArticles, type ArticleQuery, type PaginatedArticles } from "@/api/articles";
const emptyResult: PaginatedArticles = { items: [], total: 0, page: 1, totalPages: 1 };
export function useArticles(query: ArticleQuery) {
const [data, setData] = useState(emptyResult);
const [loading, setLoading] = useState(true);
useEffect(() => {
let active = true;
setLoading(true);
getArticles(query).then((result) => {
if (active) {
setData(result);
setLoading(false);
}
});
return () => {
active = false;
};
}, [query.search, query.category, query.grade, query.sort, query.page, query.limit]);
return { ...data, loading };
}

View File

@@ -0,0 +1,11 @@
import { useEffect, useState } from "react";
export function usePagination(resetKeys: unknown[]) {
const [page, setPage] = useState(1);
useEffect(() => {
setPage(1);
}, resetKeys);
return { page, setPage };
}

140
src/index.css Normal file
View File

@@ -0,0 +1,140 @@
@import url("https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800;900&display=swap");
@import "tailwindcss";
:root {
font-family: "Vazirmatn", ui-sans-serif, system-ui, sans-serif;
color: #1e293b;
background: #fbfaf7;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
button,
input {
font: inherit;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
button {
cursor: pointer;
}
.hero-grid {
background-image:
linear-gradient(to right, rgba(91, 80, 214, 0.07) 1px, transparent 1px),
linear-gradient(to bottom, rgba(91, 80, 214, 0.07) 1px, transparent 1px);
background-size: 36px 36px;
mask-image: linear-gradient(to bottom, black, transparent 90%);
}
.hero-grid-light {
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.6) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.6) 1px, transparent 1px);
background-size: 40px 40px;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
scrollbar-width: none;
}
.article-body .lead {
border-right: 4px solid #5b50d6;
padding: 0.5rem 1.25rem 0.5rem 0;
font-size: 1.125rem;
font-weight: 600;
color: #334155;
}
.article-body h2 {
margin-top: 2.25rem;
margin-bottom: 0.75rem;
font-size: 1.5rem;
line-height: 1.6;
font-weight: 900;
color: #0f172a;
}
.article-body ul {
margin: 1.25rem 0;
padding-right: 1.5rem;
list-style: none;
}
.article-body li {
position: relative;
margin: 0.45rem 0;
}
.article-body li::before {
content: "";
position: absolute;
right: -1.3rem;
top: 0.85rem;
width: 0.45rem;
height: 0.45rem;
border-radius: 999px;
background: #5b50d6;
}
.article-body blockquote {
display: flex;
gap: 1rem;
margin: 2.25rem 0;
border-radius: 1.5rem;
background: #eeecff;
padding: 1.5rem;
color: #4338a8;
font-size: 1.1rem;
font-weight: 800;
}
@keyframes float {
0%, 100% { transform: translateY(0) rotate(-6deg); }
50% { transform: translateY(-10px) rotate(-3deg); }
}
@keyframes float-delayed {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.float-card {
animation: float 4s ease-in-out infinite;
}
.float-card-delayed {
animation: float-delayed 4.5s ease-in-out 0.8s infinite;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./app/App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

1
src/pages/AboutPage.tsx Normal file
View File

@@ -0,0 +1 @@
export { About as default } from "@/app/Pages";

View File

@@ -0,0 +1 @@
export { ArticleDetail as default } from "@/app/Pages";

1
src/pages/GradesPage.tsx Normal file
View File

@@ -0,0 +1 @@
export { Grades as default } from "@/app/Pages";

1
src/pages/HomePage.tsx Normal file
View File

@@ -0,0 +1 @@
export { Home as default } from "@/app/Pages";

View File

@@ -0,0 +1 @@
export { Magazine as default } from "@/app/Pages";

View File

@@ -0,0 +1 @@
export { NotFound as default } from "@/app/Pages";

15
src/pic.ts Normal file
View File

@@ -0,0 +1,15 @@
import smartStudyPlanImg from './pic/smartstudy1.jpg';
import mathWithoutStressImg from './pic/math.jpg';
import betterReadingImg from './pic/reading.jpg';
import scienceAtHomeImg from './pic/teacherstudent.jpg';
import examNightImg from './pic/examgreen.jpg';
import groupStudyImg from './pic/groupstudy.jpg';
export const pic = {
smartStudyPlan: smartStudyPlanImg,
mathWithoutStress: mathWithoutStressImg,
betterReading: betterReadingImg,
scienceAtHome: scienceAtHomeImg,
examNight: examNightImg,
groupStudy: groupStudyImg,
} as const;

BIN
src/pic/examblue.jpg LFS Normal file

Binary file not shown.

BIN
src/pic/examgreen.jpg LFS Normal file

Binary file not shown.

BIN
src/pic/groupstudy.jpg LFS Normal file

Binary file not shown.

BIN
src/pic/math.jpg LFS Normal file

Binary file not shown.

BIN
src/pic/reading.jpg LFS Normal file

Binary file not shown.

BIN
src/pic/smartstudy1.jpg LFS Normal file

Binary file not shown.

BIN
src/pic/teacherstudent.jpg LFS Normal file

Binary file not shown.

3
src/utils/format.ts Normal file
View File

@@ -0,0 +1,3 @@
export function toPersianNumber(value: number) {
return value.toLocaleString("fa-IR");
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["node"],
"strict": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}

388
vite.config.ts Normal file
View File

@@ -0,0 +1,388 @@
import { defineConfig, type HtmlTagDescriptor, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'node:path'
import siteConfiguration from './.figma/make/site.json'
// Vite config — https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// .figma/make/deploy-preview passes `--mode development` for cached-preview builds.
const emitSourcemaps = mode === 'development'
return {
base: process.env.FIGMA_PUBLIC_URL ? `${process.env.FIGMA_PUBLIC_URL}/` : '/',
build: {
sourcemap: emitSourcemaps ? 'inline' : false,
minify: !emitSourcemaps,
},
plugins: [
react(),
tailwindcss(),
figmaSiteConfiguration(siteConfiguration),
figmaErrorOverlayReplay(),
figmaReactRefreshBoundaryFallback(),
figmaMakeKitPlugin({ storiesGlob: '/src/**/*.stories.{ts,tsx,js,jsx}' }),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
host: process.env.FIGMA_DEV_SERVER_HOST || '0.0.0.0',
port: parseInt(process.env.PORT || '8443'),
strictPort: true,
watch: {
ignored: [
'**/.figma/**',
],
},
},
preview: {
host: process.env.FIGMA_DEV_SERVER_HOST || '0.0.0.0',
port: parseInt(process.env.PORT || '8443'),
},
}
})
type FigmaSiteConfiguration = {
title?: string
description?: string
language?: string
robots?: {
index?: boolean
}
icons?: {
icon?: string
}
openGraph?: {
image?: string
}
analytics?: {
googleAnalyticsId?: string
}
customScripts?: {
headStart?: string
headEnd?: string
bodyStart?: string
bodyEnd?: string
}
accessibility?: {
addBypassLinks?: boolean
}
}
/** Applies /.figma/make/site.json to the generated document shell. */
function figmaSiteConfiguration(config: FigmaSiteConfiguration): Plugin {
function sanitizeHtmlValue(value: string | undefined): string {
return value?.replace(/[^a-zA-Z0-9_-]/g, '') || ''
}
function escapeHtmlText(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
function replaceHtmlCommentSlot(html: string, slotName: string, content: string): string {
return html.replace(`<!-- ${slotName} -->`, content)
}
const title = config.title ?? "Figma Make App"
const description = config.description ?? ''
const favicon = config.icons?.icon ?? ''
const socialImage = config.openGraph?.image ?? ''
const language = sanitizeHtmlValue(config.language) || 'en'
const googleAnalyticsId = sanitizeHtmlValue(config.analytics?.googleAnalyticsId)
const headStart = config.customScripts?.headStart ?? ''
const headEnd = config.customScripts?.headEnd ?? ''
const bodyStart = config.customScripts?.bodyStart ?? ''
const bodyEnd = config.customScripts?.bodyEnd ?? ''
const robotsTxt = config.robots?.index === false ? 'User-agent: *\nDisallow: /\n' : ''
return {
name: 'figma-site-configuration',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (!robotsTxt || req.url?.split('?')[0] !== '/robots.txt') return next()
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end(robotsTxt)
})
},
generateBundle() {
if (!robotsTxt) return
this.emitFile({
type: 'asset',
fileName: 'robots.txt',
source: robotsTxt,
})
},
transformIndexHtml: {
order: 'pre',
handler(html) {
let result = html
result = replaceHtmlCommentSlot(result, 'figma:lang', language)
result = replaceHtmlCommentSlot(result, 'figma:title', escapeHtmlText(title))
result = replaceHtmlCommentSlot(result, 'figma:head-start', headStart)
result = replaceHtmlCommentSlot(result, 'figma:head-end', headEnd)
result = replaceHtmlCommentSlot(result, 'figma:body-start', bodyStart)
result = replaceHtmlCommentSlot(result, 'figma:body-end', bodyEnd)
const tags: HtmlTagDescriptor[] = []
if (description) {
tags.push({ tag: 'meta', attrs: { name: 'description', content: description }, injectTo: 'head' })
}
if (config.robots?.index === false) {
tags.push({ tag: 'meta', attrs: { name: 'robots', content: 'noindex, nofollow' }, injectTo: 'head' })
}
if (favicon) {
tags.push({ tag: 'link', attrs: { rel: 'icon', href: favicon }, injectTo: 'head' })
}
if (title) {
tags.push({ tag: 'meta', attrs: { property: 'og:title', content: title }, injectTo: 'head' })
}
if (description) {
tags.push({ tag: 'meta', attrs: { property: 'og:description', content: description }, injectTo: 'head' })
}
if (socialImage) {
tags.push(
{ tag: 'meta', attrs: { property: 'og:image', content: socialImage }, injectTo: 'head' },
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' }, injectTo: 'head' },
{ tag: 'meta', attrs: { name: 'twitter:image', content: socialImage }, injectTo: 'head' },
)
}
if (googleAnalyticsId) {
tags.push(
{
tag: 'script',
attrs: {
async: true,
src: `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`,
},
injectTo: 'head',
},
{
tag: 'script',
children: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', ${JSON.stringify(googleAnalyticsId)});
`,
injectTo: 'head',
},
)
}
if (config.accessibility?.addBypassLinks) {
tags.push(
{
tag: 'style',
children: `
.figma-bypass-link {
position: fixed;
top: 8px;
left: 8px;
z-index: 2147483647;
transform: translateY(-150%);
border-radius: 6px;
background: #111827;
color: #fff;
padding: 8px 12px;
font: 600 14px/1.2 system-ui, sans-serif;
text-decoration: none;
}
.figma-bypass-link:focus {
transform: translateY(0);
}
`,
injectTo: 'head',
},
{
tag: 'a',
attrs: { class: 'figma-bypass-link', href: '#root' },
children: 'Skip to content',
injectTo: 'body-prepend',
},
)
}
return {
html: result,
tags,
}
},
},
}
}
/**
* Replay the most recent build error to clients that connect after
* it was first broadcast. Vite buffers an error payload only while
* no clients are connected and clears the buffer on the first
* reconnect (see `bufferedMessage` in `createWebSocketServer`), so
* if the preview iframe reloads after Vite already delivered an
* error to a live socket, the new socket misses the payload and
* the overlay stays hidden even though the build is still broken.
* We intercept `ws.send` to remember the latest error and replay
* it on every new connection; the cache clears on a successful
* `update` or `full-reload` so a stale overlay can't survive a
* fixed build.
*/
function figmaErrorOverlayReplay(): Plugin {
return {
name: 'figma-error-overlay-replay',
apply: 'serve',
configureServer(server) {
let lastError: object | null = null
const origSend = server.ws.send.bind(server.ws) as (...args: any[]) => void
server.ws.send = ((...args: any[]) => {
const payload = args[0]
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
const type = (payload as { type?: string }).type
if (type === 'error') {
lastError = payload as object
} else if (type === 'update' || type === 'full-reload') {
lastError = null
}
}
return origSend(...args)
}) as typeof server.ws.send
server.ws.on('connection', (socket) => {
if (lastError !== null) {
socket.send(JSON.stringify(lastError))
}
})
},
}
}
/**
* Reload when a module that previously defined a React Refresh boundary stops
* defining one. This happens when an agent moves a component into a new file
* and replaces the old module with a re-export:
*
* export { default } from './app/App'
*
* Vite otherwise accepts the update using the previous module's HMR boundary,
* but the re-export-only transform no longer registers a replacement for the
* mounted component family. React reports a successful refresh while leaving
* the old tree mounted until the page is reloaded.
*/
function figmaReactRefreshBoundaryFallback(): Plugin {
type ModuleNode = import('vite').ModuleNode
const hadRefreshBoundary = new Map<string, boolean>()
const lostRefreshBoundaries = new Set<string>()
let sendFullReload: (() => void) | null = null
return {
name: 'figma-react-refresh-boundary-fallback',
apply: 'serve',
enforce: 'post',
configureServer(server) {
sendFullReload = () => server.ws.send({ type: 'full-reload', path: '*' })
},
handleHotUpdate({ modules, server, timestamp }) {
if (lostRefreshBoundaries.size === 0) return
const visited = new Set<ModuleNode>()
const pending = [...modules]
while (pending.length > 0) {
const current = pending.pop()
if (!current || visited.has(current)) continue
visited.add(current)
const moduleId = current.id?.split('?')[0]
if (moduleId && lostRefreshBoundaries.has(moduleId)) {
const invalidated = new Set<ModuleNode>()
for (const updatedModule of modules) {
server.moduleGraph.invalidateModule(updatedModule, invalidated, timestamp, true)
}
sendFullReload?.()
return []
}
pending.push(...current.importers)
}
},
transform(code, id) {
if (!/\.[jt]sx?(?:\?|$)/.test(id) || id.includes('/node_modules/')) return null
const moduleId = id.split('?')[0] ?? id
const hasRefreshBoundary = code.includes('registerExportsForReactRefresh')
const previousHadRefreshBoundary = hadRefreshBoundary.get(moduleId)
hadRefreshBoundary.set(moduleId, hasRefreshBoundary)
if (hasRefreshBoundary) lostRefreshBoundaries.delete(moduleId)
if (previousHadRefreshBoundary && !hasRefreshBoundary) {
lostRefreshBoundaries.add(moduleId)
queueMicrotask(() => sendFullReload?.())
}
return null
},
}
}
/**
* Serves a blank render-target page at /.figma/make/kit.html that
* the Figma preview script drives directly. The page exposes a
* registry of every file matching `storiesGlob` on
* window.__FIGMA__.stories so the design surface can dynamically
* import + mount each entry into its own grid view.
*
* Dev-only: `apply: 'serve'` gates the plugin to `vite dev`. Prod
* builds (`vite build`) skip it entirely so the route doesn't leak
* into shipped bundles.
*/
function figmaMakeKitPlugin(options: { storiesGlob: string | string[] }): Plugin {
const storiesGlob = Array.isArray(options.storiesGlob) ? options.storiesGlob : [options.storiesGlob]
const ROUTE = '/.figma/make/kit.html'
const VIRTUAL_ID = 'virtual:figma-stories'
const RESOLVED_ID = '\0' + VIRTUAL_ID
const STORIES_MODULE = `export const stories = import.meta.glob(${JSON.stringify(storiesGlob)})`
const HTML_BOOTSTRAP = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="figma-make-kit-root"></div>
<script type="module">
import { stories } from 'virtual:figma-stories'
window.__FIGMA__ = Object.assign(window.__FIGMA__ ?? {}, { stories })
window.dispatchEvent(new CustomEvent('figma.ready'))
</script>
</body>
</html>`
return {
name: 'figma-make-kit',
apply: 'serve',
resolveId(id) {
if (id === VIRTUAL_ID) return RESOLVED_ID
return null
},
load(id) {
if (id !== RESOLVED_ID) return null
return STORIES_MODULE
},
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const url = req.url || ''
if (url.split('?')[0] !== ROUTE) return next()
try {
res.setHeader('Content-Type', 'text/html')
res.end(await server.transformIndexHtml(url, HTML_BOOTSTRAP))
} catch (err) {
next(err as Error)
}
})
},
}
}