From a133dc43bf8715dfe967d719dae2431b77a5fbf6 Mon Sep 17 00:00:00 2001 From: mhnmaleki-web Date: Thu, 8 Oct 2026 19:51:09 +0330 Subject: [PATCH] Initial commit --- .figma/make/analyze-routes | 4 + .figma/make/deploy | 4 + .figma/make/deploy-preview | 5 + .figma/make/dev | 4 + .figma/make/dev.json | 22 + .figma/make/format | 4 + .figma/make/install | 4 + .figma/make/langserver | 3 + .figma/make/site.json | 10 + .gitattributes | 146 ++++ .gitignore | 21 + .mise.toml | 3 + AGENTS.md | 41 ++ CLAUDE.md | 1 + index.html | 16 + package.json | 29 + pnpm-lock.yaml | 976 ++++++++++++++++++++++++++ src/api/articles.ts | 50 ++ src/api/categories.ts | 11 + src/api/client.ts | 31 + src/api/comments.ts | 13 + src/api/grades.ts | 10 + src/api/mockData.ts | 109 +++ src/app/App.tsx | 6 + src/app/Layout.tsx | 26 + src/app/Pages.tsx | 380 ++++++++++ src/app/routes.tsx | 24 + src/components/ArticleCard.tsx | 27 + src/components/ArticleCollections.tsx | 12 + src/components/ArticleGrid.tsx | 6 + src/components/BrandLogo.tsx | 11 + src/components/Breadcrumb.tsx | 8 + src/components/Filters.tsx | 21 + src/components/Footer.tsx | 17 + src/components/Header.tsx | 29 + src/components/Hero.tsx | 23 + src/components/Pagination.tsx | 6 + src/components/SafeImage.tsx | 23 + src/hooks/useArticles.ts | 25 + src/hooks/usePagination.ts | 11 + src/index.css | 140 ++++ src/main.tsx | 10 + src/pages/AboutPage.tsx | 1 + src/pages/ArticleDetailPage.tsx | 1 + src/pages/GradesPage.tsx | 1 + src/pages/HomePage.tsx | 1 + src/pages/MagazinePage.tsx | 1 + src/pages/NotFoundPage.tsx | 1 + src/pic.ts | 15 + src/pic/examblue.jpg | 3 + src/pic/examgreen.jpg | 3 + src/pic/groupstudy.jpg | 3 + src/pic/math.jpg | 3 + src/pic/reading.jpg | 3 + src/pic/smartstudy1.jpg | 3 + src/pic/teacherstudent.jpg | 3 + src/utils/format.ts | 3 + src/vite-env.d.ts | 1 + tsconfig.json | 23 + vite.config.ts | 388 ++++++++++ 60 files changed, 2779 insertions(+) create mode 100644 .figma/make/analyze-routes create mode 100644 .figma/make/deploy create mode 100644 .figma/make/deploy-preview create mode 100644 .figma/make/dev create mode 100644 .figma/make/dev.json create mode 100644 .figma/make/format create mode 100644 .figma/make/install create mode 100644 .figma/make/langserver create mode 100644 .figma/make/site.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .mise.toml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/api/articles.ts create mode 100644 src/api/categories.ts create mode 100644 src/api/client.ts create mode 100644 src/api/comments.ts create mode 100644 src/api/grades.ts create mode 100644 src/api/mockData.ts create mode 100644 src/app/App.tsx create mode 100644 src/app/Layout.tsx create mode 100644 src/app/Pages.tsx create mode 100644 src/app/routes.tsx create mode 100644 src/components/ArticleCard.tsx create mode 100644 src/components/ArticleCollections.tsx create mode 100644 src/components/ArticleGrid.tsx create mode 100644 src/components/BrandLogo.tsx create mode 100644 src/components/Breadcrumb.tsx create mode 100644 src/components/Filters.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/Hero.tsx create mode 100644 src/components/Pagination.tsx create mode 100644 src/components/SafeImage.tsx create mode 100644 src/hooks/useArticles.ts create mode 100644 src/hooks/usePagination.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/pages/AboutPage.tsx create mode 100644 src/pages/ArticleDetailPage.tsx create mode 100644 src/pages/GradesPage.tsx create mode 100644 src/pages/HomePage.tsx create mode 100644 src/pages/MagazinePage.tsx create mode 100644 src/pages/NotFoundPage.tsx create mode 100644 src/pic.ts create mode 100644 src/pic/examblue.jpg create mode 100644 src/pic/examgreen.jpg create mode 100644 src/pic/groupstudy.jpg create mode 100644 src/pic/math.jpg create mode 100644 src/pic/reading.jpg create mode 100644 src/pic/smartstudy1.jpg create mode 100644 src/pic/teacherstudent.jpg create mode 100644 src/utils/format.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.figma/make/analyze-routes b/.figma/make/analyze-routes new file mode 100644 index 0000000..70c7579 --- /dev/null +++ b/.figma/make/analyze-routes @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +exec figma-analyze routes "$@" diff --git a/.figma/make/deploy b/.figma/make/deploy new file mode 100644 index 0000000..2ab04c0 --- /dev/null +++ b/.figma/make/deploy @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +pnpm run build +figma make deploy --build-dir dist diff --git a/.figma/make/deploy-preview b/.figma/make/deploy-preview new file mode 100644 index 0000000..3b3bce3 --- /dev/null +++ b/.figma/make/deploy-preview @@ -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 diff --git a/.figma/make/dev b/.figma/make/dev new file mode 100644 index 0000000..95497ad --- /dev/null +++ b/.figma/make/dev @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm run dev diff --git a/.figma/make/dev.json b/.figma/make/dev.json new file mode 100644 index 0000000..85b08d0 --- /dev/null +++ b/.figma/make/dev.json @@ -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": [] +} diff --git a/.figma/make/format b/.figma/make/format new file mode 100644 index 0000000..6a6e91a --- /dev/null +++ b/.figma/make/format @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm run format -- "$@" diff --git a/.figma/make/install b/.figma/make/install new file mode 100644 index 0000000..700bc98 --- /dev/null +++ b/.figma/make/install @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm install --prefer-offline --no-frozen-lockfile diff --git a/.figma/make/langserver b/.figma/make/langserver new file mode 100644 index 0000000..7e10e46 --- /dev/null +++ b/.figma/make/langserver @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +exec pnpm dlx --package=@vtsls/language-server@^0.3.0 vtsls --stdio diff --git a/.figma/make/site.json b/.figma/make/site.json new file mode 100644 index 0000000..b4b7c60 --- /dev/null +++ b/.figma/make/site.json @@ -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 + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6ed7361 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0cb07a --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..4ea25bd --- /dev/null +++ b/.mise.toml @@ -0,0 +1,3 @@ +[tools] +node = "22" +"npm:pnpm" = "10.34.3" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..753cd5f --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/index.html b/index.html new file mode 100644 index 0000000..842acf7 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + <!-- figma:title --> + + + + +
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..dff38de --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0d2618d --- /dev/null +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/api/articles.ts b/src/api/articles.ts new file mode 100644 index 0000000..24d00aa --- /dev/null +++ b/src/api/articles.ts @@ -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 { + 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
{ + return articles.find((article) => article.slug === slug); +} + +export async function getArticlesFromApi(query: ArticleQuery = {}) { + return apiClient("articles", { params: query }); +} diff --git a/src/api/categories.ts b/src/api/categories.ts new file mode 100644 index 0000000..dea50f7 --- /dev/null +++ b/src/api/categories.ts @@ -0,0 +1,11 @@ +import { apiClient } from "./client"; + +export const mockCategories = ["همه", "مهارت مطالعه", "ریاضی", "علوم", "یادگیری", "امتحانات"]; + +export function getCategories() { + return Promise.resolve(mockCategories); +} + +export function getCategoriesFromApi() { + return apiClient("categories"); +} diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..faa37df --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,31 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +type RequestOptions = RequestInit & { + params?: Record; +}; + +export async function apiClient(path: string, options: RequestOptions = {}): Promise { + 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; +} diff --git a/src/api/comments.ts b/src/api/comments.ts new file mode 100644 index 0000000..53e6a17 --- /dev/null +++ b/src/api/comments.ts @@ -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(`articles/${articleSlug}/comments`); +} diff --git a/src/api/grades.ts b/src/api/grades.ts new file mode 100644 index 0000000..0e6b914 --- /dev/null +++ b/src/api/grades.ts @@ -0,0 +1,10 @@ +import { gradeItems } from "./mockData"; +import { apiClient } from "./client"; + +export function getGrades() { + return Promise.resolve(gradeItems); +} + +export function getGradesFromApi() { + return apiClient("grades"); +} diff --git a/src/api/mockData.ts b/src/api/mockData.ts new file mode 100644 index 0000000..bd0462f --- /dev/null +++ b/src/api/mockData.ts @@ -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" }, +]; diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..af3badc --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,6 @@ +import { RouterProvider } from "react-router"; +import { router } from "./routes"; + +export default function App() { + return ; +} diff --git a/src/app/Layout.tsx b/src/app/Layout.tsx new file mode 100644 index 0000000..9224c47 --- /dev/null +++ b/src/app/Layout.tsx @@ -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 ( +
+
+ +
+ +
+ +
+
+ ); +} diff --git a/src/app/Pages.tsx b/src/app/Pages.tsx new file mode 100644 index 0000000..aeb2c27 --- /dev/null +++ b/src/app/Pages.tsx @@ -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 ( +
+ + + {eyebrow} + +

{title}

+ {text &&

{text}

} +
+ ); +} + +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 ( + <> + + +
+
+ + همه مقاله‌ها +
+
+ +
+ + +
+
+
+ +
+
+ +
+ {categories.map(({ title, count, icon: Icon, color }) => ( + + +

{title}

+

{count}

+ + ))} +
+
+
+ +
+
+ + مشاهده همه +
+
+ {articles.slice(3, 6).map((article) => )} +
+
+ +
+
+
+ +
+ {articles.slice(0, 4).map((article, index) => ( + + {(index + 1).toLocaleString("fa-IR")} + + + {article.title} + {article.readTime} + + + + ))} +
+
+
+
+ پیشنهاد تدریسینو +

{articles[2].title}

+

{articles[2].excerpt}

+
+ +
+ مطالعه مقاله +
+
+
+ +
+
+ +
+ {gradeItems.map((item) => ( + + {item.icon} +

{item.title}

+

{item.range}

+ + + ))} +
+
+
+ +
+
+
+
+
+ +

یک نکته تازه، هر هفته

+

به خبرنامه تدریسینو بپیوند و بهترین مقاله‌های آموزشی را در ایمیلت بخوان.

+
e.preventDefault()}> + + +
+
+
+
+ + ); +} + +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 ( + <> +
+
+ مجله تدریسینو +

کشف کن، بخوان، یاد بگیر

+

مجموعه‌ای از مقاله‌های کاربردی برای درس خواندن بهتر، رشد مهارت‌ها و پیدا کردن مسیر خودت.

+
+
+
+
+
+ +
+ + setSort(value as typeof sort)} /> +
+
+
+

{category === "همه" ? "همه مقاله‌ها" : category}

+ {total.toLocaleString("fa-IR")} مطلب +
+ {loading ? ( +
{[1, 2, 3].map((item) =>
)}
+ ) : items.length ? ( +
+ ) : ( +
+ +

مطلبی با این مشخصات پیدا نشد.

+ +
+ )} +
+ + ); +} + +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.category}
+

{article.title}

+

{article.excerpt}

+
+ تـتیم تحریریه تدریسینو{article.date} {article.readTime} مطالعه {article.views.toLocaleString("fa-IR")} بازدید {(article.likes + (liked ? 1 : 0)).toLocaleString("fa-IR")} +
+
+
+
+ +
+
+ +
+

همه ما حداقل یک‌بار برنامه‌ای نوشته‌ایم که روی کاغذ عالی به نظر می‌رسید، اما بعد از چند روز فراموش شد. مشکل از اراده ما نیست؛ بیشتر وقت‌ها برنامه با زندگی واقعی‌مان هماهنگ نیست.

+

۱. اول واقعیت را ببین، بعد برنامه بنویس

+

قبل از اینکه ساعت‌های روز را پر کنی، یک هفته معمولی خودت را مرور کن. زمان مدرسه، استراحت، رفت‌وآمد و کارهای شخصی را بنویس. برنامه خوب قرار نیست تمام روز تو را بگیرد؛ قرار است از زمان‌های آزاد به بهترین شکل استفاده کند.

+
+
نکته تدریسینو

فقط ۷۰ درصد زمان آزادت را برنامه‌ریزی کن. آن ۳۰ درصد باقی‌مانده برای اتفاق‌های پیش‌بینی‌نشده است.

+
+

۲. هدف‌های کوچک و روشن انتخاب کن

+

«امروز ریاضی بخوانم» یک هدف مبهم است. به‌جای آن بنویس: «تمرین‌های ۱ تا ۸ صفحه ۴۲ را حل کنم». هرچه هدف روشن‌تر باشد، شروع کردن آسان‌تر می‌شود.

+
    +
  • هر بازه مطالعه بین ۲۵ تا ۵۰ دقیقه باشد.
  • +
  • بین دو بازه، ۵ تا ۱۰ دقیقه استراحت کن.
  • +
  • درس‌های سخت و آسان را پشت سر هم قرار بده.
  • +
  • برای پایان هر روز، یک مرور کوتاه بگذار.
  • +
+

برنامه خوب، برنامه‌ای نیست که فشرده‌تر باشد؛ برنامه‌ای است که بتوانی ادامه‌اش بدهی.

+

۳. برنامه را هر هفته بهتر کن

+

در پایان هفته از خودت بپرس کدام بخش‌ها انجام شدند و کجاها به زمان بیشتری نیاز داشتی. تغییر دادن برنامه نشانه شکست نیست؛ یعنی داری روش مناسب خودت را پیدا می‌کنی.

+
+
{["برنامه‌ریزی", "مطالعه", "مدیریت زمان"].map((tag) => #{tag})}
+ +
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+
+ ); +} + +export function Grades() { + const features = [ + { icon: Brain, title: "روش مطالعه", text: "تکنیک‌های متناسب با درس‌ها و سن تو" }, + { icon: Target, title: "هدف‌گذاری", text: "مسیرهای کوچک و قابل انجام برای پیشرفت" }, + { icon: BookOpen, title: "آموزش مفهومی", text: "یادگیری ساده و قابل فهم موضوعات سخت" }, + ]; + return ( + <> +
+
+
+ +

محتوای مناسب پایه خودت را پیدا کن

+

از اولین سال‌های مدرسه تا انتخاب مسیر آینده، تدریسینو کنار توست.

+
+
+
+
+ {gradeItems.map((item, index) => ( +
+ {item.icon} +

{item.title}

+

{item.range}

+

{["یادگیری شیرین و پایه‌ای با داستان، بازی و تجربه‌های ساده برای ساختن یک شروع محکم.", "راهنمای عبور از تغییرات این دوره، شناخت بهتر درس‌ها و پیدا کردن روش مطالعه شخصی.", "محتوای تخصصی‌تر برای درس‌ها، امتحانات نهایی و تصمیم‌های مهم تحصیلی."][index]}

+ مشاهده مطالب + {index + 1} +
+ ))} +
+
+
+
+ +
+ {features.map(({ icon: Icon, title, text }) =>

{title}

{text}

)} +
+
+
+ + ); +} + +export function About() { + const values = [ + { icon: Eye, title: "ساده و روشن", text: "مفاهیم را پیچیده نمی‌کنیم؛ هر مطلب باید قابل فهم و قابل اجرا باشد." }, + { icon: Heart, title: "همدلانه", text: "دغدغه‌ها و تجربه واقعی دانش‌آموزان، نقطه شروع محتوای ماست." }, + { icon: Compass, title: "مستقل و کنجکاو", text: "کمک می‌کنیم هر دانش‌آموز راه یادگیری خودش را پیدا کند." }, + { icon: Users, title: "برای همه", text: "دانش خوب باید رایگان و در دسترس همه دانش‌آموزان باشد." }, + ]; + return ( + <> +
+
+ داستان تدریسینو +

ما باور داریم هر کسی می‌تواند یادگیرنده خوبی باشد.

+

تدریسینو از یک سؤال ساده شروع شد: چرا یادگیری، با همه جذابیتی که دارد، گاهی خسته‌کننده و دشوار به نظر می‌رسد؟ ما اینجا هستیم تا با محتوای روشن، کاربردی و صمیمی، جواب تازه‌ای به این سؤال بدهیم.

+
+
+ +
از ۱۴۰۱کنار دانش‌آموزان
+
+
+
+
+ +
+ {values.map(({ icon: Icon, title, text }) =>

{title}

{text}

)} +
+
+
+
+ +

بیایید یادگیری را دوباره کشف کنیم

+

مهم نیست در کدام پایه هستی یا چه هدفی داری؛ در مجله تدریسینو همیشه چیزی برای شروع کردن پیدا می‌کنی.

+ مطالعه را شروع کن +
+ + ); +} + +export function NotFound() { + return
۴۰۴

این صفحه را پیدا نکردیم

شاید آدرس تغییر کرده یا صفحه به جای دیگری رفته باشد.

بازگشت به خانه
; +} diff --git a/src/app/routes.tsx b/src/app/routes.tsx new file mode 100644 index 0000000..c6e14d7 --- /dev/null +++ b/src/app/routes.tsx @@ -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 }, + ], + }, +]); diff --git a/src/components/ArticleCard.tsx b/src/components/ArticleCard.tsx new file mode 100644 index 0000000..53115ce --- /dev/null +++ b/src/components/ArticleCard.tsx @@ -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.category} + +
+
+ {article.grade} {article.readTime} +
+

+ {article.title} +

+

{article.excerpt}

+ + ادامه مطلب + +
+
+ ); +} diff --git a/src/components/ArticleCollections.tsx b/src/components/ArticleCollections.tsx new file mode 100644 index 0000000..00ed576 --- /dev/null +++ b/src/components/ArticleCollections.tsx @@ -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 ; +} + +export function RecommendedArticles({ articles }: { articles: Article[] }) { + return
{articles.slice(0, 3).map((item) => {item.category}{item.title})}
; +} diff --git a/src/components/ArticleGrid.tsx b/src/components/ArticleGrid.tsx new file mode 100644 index 0000000..772c7e4 --- /dev/null +++ b/src/components/ArticleGrid.tsx @@ -0,0 +1,6 @@ +import type { Article } from "@/api/mockData"; +import ArticleCard from "./ArticleCard"; + +export default function ArticleGrid({ articles }: { articles: Article[] }) { + return
{articles.map((article) => )}
; +} diff --git a/src/components/BrandLogo.tsx b/src/components/BrandLogo.tsx new file mode 100644 index 0000000..b20faee --- /dev/null +++ b/src/components/BrandLogo.tsx @@ -0,0 +1,11 @@ +import { BookOpen } from "lucide-react"; +import { Link } from "react-router"; + +export default function BrandLogo({ inverse = false }: { inverse?: boolean }) { + return ( + + + تدریسینومجله یادگیری بهتر + + ); +} diff --git a/src/components/Breadcrumb.tsx b/src/components/Breadcrumb.tsx new file mode 100644 index 0000000..d305841 --- /dev/null +++ b/src/components/Breadcrumb.tsx @@ -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 ; +} diff --git a/src/components/Filters.tsx b/src/components/Filters.tsx new file mode 100644 index 0000000..5b26686 --- /dev/null +++ b/src/components/Filters.tsx @@ -0,0 +1,21 @@ +import { Search } from "lucide-react"; + +export function SearchBar({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ; +} + +export function CategoryFilter({ items, value, onChange }: { items: string[]; value: string; onChange: (value: string) => void }) { + return
{items.map((item) => )}
; +} + +function SelectFilter({ label, value, items, onChange }: { label: string; value: string; items: { value: string; label: string }[]; onChange: (value: string) => void }) { + return ; +} + +export function GradeFilter({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ({ value: item, label: item }))} />; +} + +export function SortFilter({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ; +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..16051fc --- /dev/null +++ b/src/components/Footer.tsx @@ -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 ( +
+
+

تدریسینو یک مجله آموزشی رایگان برای دانش‌آموزانی است که می‌خواهند هوشمندانه‌تر یاد بگیرند، نه سخت‌تر.

+

دسترسی سریع

{navItems.map((item) => {item.label})}
+

موضوعات محبوب

مهارت‌های مطالعهامتحانات و کنکورعلوم و فناوری
+
+

© ۱۴۰۴ تدریسینو؛ یادگیری برای همه.

با کنجکاوی ساخته شده برای دانش‌آموزان ایران

+
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..b189528 --- /dev/null +++ b/src/components/Header.tsx @@ -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 ( +
+
+ + +
+ + شروع مطالعه +
+ +
+ {menuOpen && } +
+ ); +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx new file mode 100644 index 0000000..19341e6 --- /dev/null +++ b/src/components/Hero.tsx @@ -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 ( +
+
+
+
+ یادگیری ساده‌تر، آینده روشن‌تر +

مجله آموزشی تدریسینو

+

مطالب آموزشی، نکات درسی و محتوای کاربردی برای مسیر بهتر یادگیری

+
مطالعه مطالب انتخاب پایه تحصیلی
+
+۱۲۰مقاله کاربردی
۱۲ پایهمحتوای متنوع
رایگانبرای همه
+
+
+
x²
+
یادگیری امروزیک قدم رو به جلو
+
+
+
+ ); +} diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..5fd5fa9 --- /dev/null +++ b/src/components/Pagination.tsx @@ -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 ; +} diff --git a/src/components/SafeImage.tsx b/src/components/SafeImage.tsx new file mode 100644 index 0000000..aa672ee --- /dev/null +++ b/src/components/SafeImage.tsx @@ -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 ( +
+ +
+ ); + } + + return {alt} setFailed(true)} />; +} diff --git a/src/hooks/useArticles.ts b/src/hooks/useArticles.ts new file mode 100644 index 0000000..b6535c8 --- /dev/null +++ b/src/hooks/useArticles.ts @@ -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 }; +} diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts new file mode 100644 index 0000000..eca0228 --- /dev/null +++ b/src/hooks/usePagination.ts @@ -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 }; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..ce23ecf --- /dev/null +++ b/src/index.css @@ -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; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..f07e697 --- /dev/null +++ b/src/main.tsx @@ -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( + + + , +); diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx new file mode 100644 index 0000000..9a12e1c --- /dev/null +++ b/src/pages/AboutPage.tsx @@ -0,0 +1 @@ +export { About as default } from "@/app/Pages"; diff --git a/src/pages/ArticleDetailPage.tsx b/src/pages/ArticleDetailPage.tsx new file mode 100644 index 0000000..5959f4c --- /dev/null +++ b/src/pages/ArticleDetailPage.tsx @@ -0,0 +1 @@ +export { ArticleDetail as default } from "@/app/Pages"; diff --git a/src/pages/GradesPage.tsx b/src/pages/GradesPage.tsx new file mode 100644 index 0000000..fb5decb --- /dev/null +++ b/src/pages/GradesPage.tsx @@ -0,0 +1 @@ +export { Grades as default } from "@/app/Pages"; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 0000000..65e992f --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1 @@ +export { Home as default } from "@/app/Pages"; diff --git a/src/pages/MagazinePage.tsx b/src/pages/MagazinePage.tsx new file mode 100644 index 0000000..4491062 --- /dev/null +++ b/src/pages/MagazinePage.tsx @@ -0,0 +1 @@ +export { Magazine as default } from "@/app/Pages"; diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..aff4cca --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1 @@ +export { NotFound as default } from "@/app/Pages"; diff --git a/src/pic.ts b/src/pic.ts new file mode 100644 index 0000000..a28f24b --- /dev/null +++ b/src/pic.ts @@ -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; \ No newline at end of file diff --git a/src/pic/examblue.jpg b/src/pic/examblue.jpg new file mode 100644 index 0000000..5531a61 --- /dev/null +++ b/src/pic/examblue.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d58387860cdfcb0816e75c331e1b357bb488531f148aea83d9853a183590149 +size 51623 diff --git a/src/pic/examgreen.jpg b/src/pic/examgreen.jpg new file mode 100644 index 0000000..179a676 --- /dev/null +++ b/src/pic/examgreen.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32067bab553a9fcfb8928fbea2ec3ad3d818d1e2cc68df944e151cb04a1fd709 +size 45499 diff --git a/src/pic/groupstudy.jpg b/src/pic/groupstudy.jpg new file mode 100644 index 0000000..619f258 --- /dev/null +++ b/src/pic/groupstudy.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77cd618e503556a457b9f51605be1a88fd6b87a64492977ae065a8e0680761b2 +size 41096 diff --git a/src/pic/math.jpg b/src/pic/math.jpg new file mode 100644 index 0000000..fe854f1 --- /dev/null +++ b/src/pic/math.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b23fcfeb1399bb2bf830d96ae6875f5fcf9b1c418d44d108bbde16cb469e0ab0 +size 63778 diff --git a/src/pic/reading.jpg b/src/pic/reading.jpg new file mode 100644 index 0000000..cf14970 --- /dev/null +++ b/src/pic/reading.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee4e71fd33a71885c5d028c91c6d8f464e053fa46a31d7525e6f2739ba87143c +size 28277 diff --git a/src/pic/smartstudy1.jpg b/src/pic/smartstudy1.jpg new file mode 100644 index 0000000..50a1e47 --- /dev/null +++ b/src/pic/smartstudy1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f841a6dd51f5ea8980ee2b7fc765c7e17cfb2aeb6cf7750d29eddf7586517d43 +size 41058 diff --git a/src/pic/teacherstudent.jpg b/src/pic/teacherstudent.jpg new file mode 100644 index 0000000..53b6a8a --- /dev/null +++ b/src/pic/teacherstudent.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fffbd9efa48de26a23f94ae4d91c48e5683105067fbbaf8b6ea9cf946294b632 +size 55314 diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..2a08299 --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,3 @@ +export function toPersianNumber(value: number) { + return value.toLocaleString("fa-IR"); +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ce78eb3 --- /dev/null +++ b/tsconfig.json @@ -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"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..d02f4ce --- /dev/null +++ b/vite.config.ts @@ -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, '&').replace(//g, '>') + } + function replaceHtmlCommentSlot(html: string, slotName: string, content: string): string { + return html.replace(``, 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() + const lostRefreshBoundaries = new Set() + 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() + 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() + 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 = ` + + + + + + +
+ + +` + + 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) + } + }) + }, + } +}