From 1e68a848798a8706661a7a8993821b2ae6f8bb41 Mon Sep 17 00:00:00 2001 From: Behrad Date: Tue, 8 Sep 2026 01:40:00 +0330 Subject: [PATCH] feat: update UI to glassmorphic design and copper theme --- .env.example | 23 + .gitignore | 31 + README.md | 192 + backend/.env.example | 29 + backend/Dockerfile | 16 + backend/README.md | 189 + backend/app/__init__.py | 0 backend/app/api/__init__.py | 0 backend/app/api/admin.py | 354 ++ backend/app/api/forms.py | 42 + backend/app/api/public.py | 149 + backend/app/auth.py | 52 + backend/app/config.py | 36 + backend/app/database.py | 20 + backend/app/main.py | 47 + backend/app/migrations.py | 71 + backend/app/models.py | 375 ++ backend/app/rate_limit.py | 49 + backend/app/schemas.py | 588 ++ backend/app/seed.py | 94 + backend/app/seed_data.py | 458 ++ backend/app/seo_pages.py | 239 + backend/requirements-dev.txt | 4 + backend/requirements.txt | 13 + backend/tests/conftest.py | 93 + backend/tests/test_api.py | 182 + docker-compose.prod.yml | 57 + docker-compose.yml | 158 + frontend/.eslintrc.cjs | 18 + frontend/.gitignore | 24 + frontend/Caddyfile | 12 + frontend/Dockerfile | 10 + frontend/README.md | 33 + frontend/index.html | 21 + frontend/package-lock.json | 5203 +++++++++++++++++ frontend/package.json | 43 + frontend/postcss.config.js | 6 + frontend/src/App.tsx | 140 + frontend/src/admin/AdminBar.tsx | 190 + frontend/src/admin/AdminContext.tsx | 162 + frontend/src/admin/AdminLoginPage.tsx | 85 + frontend/src/admin/ArticleWritePage.tsx | 375 ++ frontend/src/admin/Editable.tsx | 290 + frontend/src/admin/EntityDrawer.tsx | 339 ++ frontend/src/admin/adminClient.ts | 97 + frontend/src/admin/entities.ts | 203 + frontend/src/api.tsx | 285 + frontend/src/components/ArticleCard.tsx | 52 + frontend/src/components/Button.tsx | 110 + frontend/src/components/ContactSection.tsx | 277 + frontend/src/components/FloatingContact.tsx | 30 + frontend/src/components/Footer.tsx | 152 + frontend/src/components/Header.tsx | 152 + frontend/src/components/Lightbox.tsx | 100 + frontend/src/components/Navbar.tsx | 0 frontend/src/components/PageHero.tsx | 89 + frontend/src/components/Preloader.tsx | 81 + frontend/src/components/ProjectCard.tsx | 57 + frontend/src/components/Reveal.tsx | 23 + frontend/src/components/SectionHeading.tsx | 58 + frontend/src/components/ServiceIcon.tsx | 83 + .../src/components/admin/ArticleEditor.tsx | 267 + frontend/src/components/admin/ImageField.tsx | 81 + frontend/src/components/admin/InlineEdit.tsx | 87 + .../src/components/admin/SeoChecklist.tsx | 208 + frontend/src/components/home/Articles.tsx | 67 + frontend/src/components/home/BeforeAfter.tsx | 126 + frontend/src/components/home/Faq.tsx | 81 + .../src/components/home/FeaturedProjects.tsx | 125 + frontend/src/components/home/Gallery.tsx | 123 + frontend/src/components/home/Hero.tsx | 151 + frontend/src/components/home/Newsletter.tsx | 161 + frontend/src/components/home/Services.tsx | 56 + frontend/src/components/home/Stats.tsx | 70 + frontend/src/components/home/Story.tsx | 121 + frontend/src/components/home/Team.tsx | 41 + frontend/src/components/home/Testimonials.tsx | 80 + frontend/src/components/home/Timeline.tsx | 92 + frontend/src/components/home/WhyUs.tsx | 72 + frontend/src/config.ts | 3 + frontend/src/data/site.ts | 43 + frontend/src/index.css | 141 + frontend/src/index.tsx | 9 + frontend/src/pages/About.tsx | 24 + frontend/src/pages/ArticleDetail.tsx | 149 + frontend/src/pages/Blog.tsx | 132 + frontend/src/pages/ContactPage.tsx | 39 + frontend/src/pages/FaqPage.tsx | 16 + frontend/src/pages/Home.tsx | 38 + frontend/src/pages/Investment.tsx | 57 + frontend/src/pages/ProjectDetail.tsx | 173 + frontend/src/pages/Projects.tsx | 88 + frontend/src/pages/ServicesPage.tsx | 45 + frontend/src/seo.ts | 94 + frontend/src/vite-env.d.ts | 1 + frontend/tailwind.config.js | 31 + frontend/tsconfig.json | 25 + frontend/tsconfig.node.json | 11 + frontend/vite.config.ts | 20 + scripts/backup.sh | 44 + 100 files changed, 15553 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/admin.py create mode 100644 backend/app/api/forms.py create mode 100644 backend/app/api/public.py create mode 100644 backend/app/auth.py create mode 100644 backend/app/config.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/migrations.py create mode 100644 backend/app/models.py create mode 100644 backend/app/rate_limit.py create mode 100644 backend/app/schemas.py create mode 100644 backend/app/seed.py create mode 100644 backend/app/seed_data.py create mode 100644 backend/app/seo_pages.py create mode 100644 backend/requirements-dev.txt create mode 100644 backend/requirements.txt create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_api.py create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/.gitignore create mode 100644 frontend/Caddyfile create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/admin/AdminBar.tsx create mode 100644 frontend/src/admin/AdminContext.tsx create mode 100644 frontend/src/admin/AdminLoginPage.tsx create mode 100644 frontend/src/admin/ArticleWritePage.tsx create mode 100644 frontend/src/admin/Editable.tsx create mode 100644 frontend/src/admin/EntityDrawer.tsx create mode 100644 frontend/src/admin/adminClient.ts create mode 100644 frontend/src/admin/entities.ts create mode 100644 frontend/src/api.tsx create mode 100644 frontend/src/components/ArticleCard.tsx create mode 100644 frontend/src/components/Button.tsx create mode 100644 frontend/src/components/ContactSection.tsx create mode 100644 frontend/src/components/FloatingContact.tsx create mode 100644 frontend/src/components/Footer.tsx create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/components/Lightbox.tsx create mode 100644 frontend/src/components/Navbar.tsx create mode 100644 frontend/src/components/PageHero.tsx create mode 100644 frontend/src/components/Preloader.tsx create mode 100644 frontend/src/components/ProjectCard.tsx create mode 100644 frontend/src/components/Reveal.tsx create mode 100644 frontend/src/components/SectionHeading.tsx create mode 100644 frontend/src/components/ServiceIcon.tsx create mode 100644 frontend/src/components/admin/ArticleEditor.tsx create mode 100644 frontend/src/components/admin/ImageField.tsx create mode 100644 frontend/src/components/admin/InlineEdit.tsx create mode 100644 frontend/src/components/admin/SeoChecklist.tsx create mode 100644 frontend/src/components/home/Articles.tsx create mode 100644 frontend/src/components/home/BeforeAfter.tsx create mode 100644 frontend/src/components/home/Faq.tsx create mode 100644 frontend/src/components/home/FeaturedProjects.tsx create mode 100644 frontend/src/components/home/Gallery.tsx create mode 100644 frontend/src/components/home/Hero.tsx create mode 100644 frontend/src/components/home/Newsletter.tsx create mode 100644 frontend/src/components/home/Services.tsx create mode 100644 frontend/src/components/home/Stats.tsx create mode 100644 frontend/src/components/home/Story.tsx create mode 100644 frontend/src/components/home/Team.tsx create mode 100644 frontend/src/components/home/Testimonials.tsx create mode 100644 frontend/src/components/home/Timeline.tsx create mode 100644 frontend/src/components/home/WhyUs.tsx create mode 100644 frontend/src/config.ts create mode 100644 frontend/src/data/site.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/pages/About.tsx create mode 100644 frontend/src/pages/ArticleDetail.tsx create mode 100644 frontend/src/pages/Blog.tsx create mode 100644 frontend/src/pages/ContactPage.tsx create mode 100644 frontend/src/pages/FaqPage.tsx create mode 100644 frontend/src/pages/Home.tsx create mode 100644 frontend/src/pages/Investment.tsx create mode 100644 frontend/src/pages/ProjectDetail.tsx create mode 100644 frontend/src/pages/Projects.tsx create mode 100644 frontend/src/pages/ServicesPage.tsx create mode 100644 frontend/src/seo.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 scripts/backup.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0369333 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# کپی کنید: cp .env.example .env — و همه مقادیر change-me را عوض کنید. +# docker compose به‌طور خودکار این فایل را می‌خواند؛ بدون آن استک بالا نمی‌آید. + +# --- رمزهای اصلی (اجباری) --- +# تولید کلید تصادفی: python -c "import secrets; print(secrets.token_urlsafe(48))" +SECRET_KEY=change-me-to-a-long-random-string +POSTGRES_PASSWORD=change-me-db-password +MINIO_ROOT_USER=aramesh +MINIO_ROOT_PASSWORD=change-me-minio-password +ADMIN_USERNAME=admin +ADMIN_PASSWORD=change-me-admin-password + +# --- اختیاری --- +# پورت ورود Traefik در توسعه (پیش‌فرض 5173) +TRAEFIK_HTTP_PORT=5173 +# نشانی عمومی سایت برای sitemap و آدرس مطلق OG — در تولید مثل https://aramesh.example +SITE_URL= +# آدرس‌های مجاز CORS (لیست JSON) — پیش‌فرض فقط لوکال‌هاست +# CORS_ORIGINS=["http://localhost:5173"] + +# --- فقط برای docker-compose.prod.yml (حالت تولید با TLS) --- +# DOMAIN=aramesh.example +# ACME_EMAIL=admin@aramesh.example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da36e75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ +env/ + +# Environment +.env + +# پشتیبان‌گیری محلی +backups/ + +# Node / Vite +node_modules/ +dist/ +.vite/ + +# IDE / OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +# Database +*.sqlite3 +*.db + +# تصاویر محلی — از پنل/کنسول MinIO مدیریت می‌شوند +frontend/public/images/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a77754 --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# گروه ساختمانی آرامش — پروژه کامل (فرانت‌اند + بک‌اند) + +مونوریپوی سایت گروه ساختمانی آرامش شامل: + +- **`frontend/`** — سایت React + Vite + Tailwind (تمام متن‌ها و تصاویر از API خوانده می‌شود) +- **`backend/`** — بک‌اند FastAPI + SQLAlchemy 2 + PostgreSQL + JWT + MinIO + +## معماری + +``` +Traefik (پورت ۵۱۷۳ — تنها نقطه ورود) +├── / → Caddy (فایل‌های استاتیک SPA) +├── /api → FastAPI (بک‌اند) +└── /media → MinIO (باکت aramesh-media — تصاویر آپلودی) +``` + +- **همه محتوای متنی سایت در PostgreSQL است** — هیچ متنی در کد فرانت‌اند هاردکد نشده؛ + با یک درخواست `GET /api/site` بارگذاری و از پنل مدیریت ویرایش می‌شود +- **تصاویر آپلودی در MinIO** (Object Storage سازگار با S3) ذخیره می‌شوند و با مسیر نسبی + `/media/...` برگردانده می‌شوند — بک‌اند ذخیره‌سازی بدون تغییر محتوای دیتابیس قابل تعویض است +- مسیریابی همه‌چیز روی **Traefik** است (با Docker labels) — nginx در پروژه وجود ندارد + +## ساختار + +``` +├── frontend/ # سایت React +│ ├── src/ # کد منبع (api.tsx = لایه اتصال به بک‌اند) +│ ├── Dockerfile # بیلد Node + سرو استاتیک با Caddy +│ └── Caddyfile # فقط سرو فایل‌های استاتیک (مسیریابی روی Traefik است) +├── backend/ # بک‌اند FastAPI +│ ├── app/ # کد منبع (آپلود تصویر: app/api/admin.py — سئوی سرور: app/seo_pages.py) +│ ├── tests/ # تست‌های API (pytest) +│ ├── requirements.txt +│ ├── Dockerfile +│ └── .env.example +├── scripts/backup.sh # پشتیبان‌گیری دیتابیس و تصاویر +├── docker-compose.yml # اجرای کامل با یک دستور +└── docker-compose.prod.yml # لایه تولید: TLS با Let's Encrypt +``` + +## اجرا با Docker (ساده‌ترین راه) + +```bash +cp .env.example .env # مقادیر change-me را عوض کنید — بدون .env استک بالا نمی‌آید +docker compose up --build +``` + +- سایت: http://localhost:5173 (از طریق Traefik) +- مستندات API: http://localhost:8001/docs (با `docker-compose.override.yml` محلی) +- کنسول MinIO: http://localhost:9001 — کاربر/رمز از متغیرهای `MINIO_ROOT_*` در compose +- دیتابیس PostgreSQL و آبجکت‌استور MinIO هر دو در کانتینر جدا با volume دائمی +- بک‌اند هنگام شروع، خودش جداول را می‌سازد و **فقط جدول‌های خالی** را seed می‌کند — + ارتقای نسخه داده‌های فعلی را از بین نمی‌برد +- در اولین اجرا، سرویس `minio-setup` باکت `aramesh-media` را می‌سازد و خواندن عمومی را فعال می‌کند + +## انتشار واقعی (TLS) + +برای بالا آوردن با HTTPS روی دامنه واقعی، لایه `docker-compose.prod.yml` را هم سوار کنید: + +```bash +# در .env: DOMAIN=example.com و ACME_EMAIL=you@example.com +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +``` + +- Traefik گواهی Let's Encrypt می‌گیرد و تمدید می‌کند؛ همه درخواست‌های HTTP به HTTPS هدایت می‌شوند +- در این حالت `SITE_URL=https://example.com` را هم در `.env` بگذارید تا sitemap و آدرس‌های OG مطلق شوند + +## اجرای محلی (توسعه) + +### بک‌اند + +```bash +cd backend +python -m venv .venv +.venv\Scripts\activate # ویندوز — در لینوکس: source .venv/bin/activate +pip install -r requirements.txt + +# فایل .env از روی .env.example بسازید و DATABASE_URL را تنظیم کنید +python -m app.seed # ساخت جداول + محتوای اولیه +uvicorn app.main:app --reload +``` + +### فرانت‌اند + +```bash +cd frontend +npm install +npm run dev +``` + +در حالت توسعه، Vite درخواست‌های `/api` و `/media` را به Traefik (پورت ۵۱۷۳) پروکسی می‌کند؛ +پس استک Docker باید در حال اجرا باشد. آدرس بک‌اند در `frontend/src/config.ts` از +`VITE_API_URL` خوانده می‌شود — پیش‌فرض خالی یعنی درخواست‌های هم‌مبدأ. + +## امکانات بک‌اند + +- **API عمومی**: برند، منو، هیرو، خدمات، پروژه‌ها، مقالات، نظرات، سوالات متداول، تیم، + تایم‌لاین، آمار، دفاتر، مدل‌های سرمایه‌گذاری و... — همه در دیتابیس +- **فرم تماس** `POST /api/contact` و **خبرنامه** `POST /api/newsletter` +- **پنل مدیریت** با JWT: ورود `POST /api/auth/login` (پیش‌فرض: `admin` / `aramesh123`) + و سپس CRUD کامل همه محتواها در `/api/admin/...` +- **تغییر رمز** `POST /api/auth/change-password` — از نوار مدیریت پایین سایت (دکمه «رمز») هم در دسترس است +- **محدودسازی ورود**: بعد از ۵ ورود ناموفق در ۱۵ دقیقه از یک IP، درخواست با 429 رد می‌شود +- **آپلود تصویر** `POST /api/admin/upload` (فقط توکن ادمین، حداکثر ۱۰MB) — فایل در MinIO + ذخیره و URL نسبی `/media/...` برمی‌گردد +- **ویرایش جزئی** `PATCH /api/admin/{resource}/{id}` — فقط فیلدهای ارسال‌شده عوض می‌شوند +- **ترتیب‌بندی** `POST /api/admin/{resource}/reorder` — با `{"ids": [...]}` +- همه محتوا در یک درخواست: `GET /api/site` (مقاله‌های پیش‌نویس در آن نمی‌آیند) +- **تست‌ها**: `cd backend && pip install -r requirements-dev.txt && pytest tests` + +> در محیط واقعی حتماً `SECRET_KEY`، `ADMIN_PASSWORD` و `MINIO_ROOT_PASSWORD` را تغییر دهید +> (از `.env`) و بعد از اولین ورود، رمز را از نوار مدیریت عوض کنید. + +## ویرایش زنده سایت (دوقلوی دیجیتال) + +ادمین‌ها نیازی به داشبورد جدا ندارند — خود سایت ویرایشگر است: + +1. به `/admin/login` بروید و وارد شوید +2. نوار شناور پایین صفحه ظاهر می‌شود؛ **حالت ویرایش** را روشن کنید +3. روی هر متن کلیک کنید و ویرایش کنید — **خروج از کادر، بلافاصله ذخیره می‌شود** + (Enter ذخیره، Esc لغو)؛ وضعیت ذخیره در نوار پایین دیده می‌شود +4. آیتم‌های مجموعه‌ها (خدمات، پروژه‌ها، مقالات، نظرات، تیم و...) با هاور، دکمه‌های + ویرایش / حذف / جابه‌جایی ترتیب نشان می‌دهند؛ دکمه «افزودن» هم انتهای هر لیست هست +5. تصویرها از همان کشوها به MinIO آپلود می‌شوند + +- در حالت ویرایش، لینک‌ها کلیک نمی‌شوند تا ویرایش متن ممکن باشد +- بازدیدکننده‌های عادی هیچ اثری از این لایه نمی‌بینند (بدون توکن، همه درخواست‌های + مدیریتی `401` برمی‌گردند) +- تمام تغییرها بلافاصله در PostgreSQL ثبت می‌شوند — با Ctrl+F5 هم می‌مانند + +## وبلاگ — نوشتن با ویرایشگر WYSIWYG + +مقالات با ویرایشگر غنی **TipTap** نوشته می‌شوند (همان تجربه Medium): + +- در حالت ویرایش، روی هر مقاله در صفحه وبلاگ دکمه ویرایش را بزنید — کشوی نوشتن باز می‌شود +- تیتر (H2/H3)، بولد/ایتالیک/زیرخط، لیست نقطه‌ای و شماره‌ای، نقل‌قول، لینک و واگرد +- **درج تصویر داخل متن**: دکمه تصویر، یا چسباندن/کشیدن تصویر داخل نوشته — فایل مستقیم + در MinIO ذخیره و در متن قرار می‌گیرد +- راست‌به‌چپ کامل با فونت و تم سایت +- ذخیره خودکار: هر مکث کوتاه در تایپ، تغییرها را ذخیره می‌کند؛ بستن کشو هم ذخیره می‌کند + +**پیش‌نویس و انتشار**: مقاله تازه همیشه **پیش‌نویس** شروع می‌شود — در سایت عمومی دیده +نمی‌شود و تا انتشار، تغییرهای خودکار آن چیزی را خراب نمی‌کند. با دکمه «انتشار در سایت» +عمومی می‌شود؛ دوباره می‌توان آن را به پیش‌نویس برگرداند. پیش‌نویس‌ها فقط برای ادمین +(در صفحه وبلاگ، با برچسب «پیش‌نویس») دیده می‌شوند و در `GET /api/site` هرگز نمی‌آیند. + +**فرمت ذخیره**: بدنه مقاله یک سند ساختاریافته TipTap (JSON) در PostgreSQL است — نه HTML خام. +صفحه مقاله آن را با رندر استاتیک TipTap به HTML تبدیل می‌کند، پس فقط تگ‌های شناخته‌شده +(پاراگراف، تیتر، لیست، نقل‌قول، تصویر، لینک) تولید می‌شوند. مقالات قدیمی (لیست پاراگراف) +در اولین اجرا به‌صورت خودکار به این فرمت تبدیل می‌شوند. + +## سئو + +- **بررسی سئو در صفحه نوشتن**: فهرست سبز/نارنجی/قرمز به سبک یوست ولی سازگار با فارسی — + طول عنوان، کلمه کلیدی هدف (در عنوان/پاراگراف اول/تیترها)، طول چکیده، تعداد واژه، + تیتربندی H2، متن جایگزین تصاویر، پیوندها و نشانی (slug) — کلمه کلیدی هدف در دیتابیس + ذخیره می‌شود (فیلد `seo_keyphrase` مقاله) +- **سئوی هر صفحه**: در حالت ویرایش، دکمه سئو روی هیرو هر صفحه (و کنار هیرو صفحه اصلی) — + عنوان تب و توضیح متای هر صفحه از دیتابیس خوانده و اعمال می‌شود؛ اگر ردیف سئوی صفحه‌ای + هنوز ساخته نشده باشد، دکمه + همان‌جا آن را می‌سازد +- صفحه مقاله و پروژه به‌طور خودکار عنوان و توضیح متا را از محتوای خودشان می‌گیرند +- **تگ‌های OG و توییتر و canonical** برای همه صفحات از `src/seo.ts` ست می‌شود و + مقاله‌ها اسکیمای JSON-LD نوع Article می‌گیرند +- **robots.txt و sitemap.xml** از بک‌اند سرو می‌شوند و مقاله/پروژه‌های منتشرشده را + شامل می‌شوند (پیش‌نویس‌ها نمی‌آیند) — برای آدرس مطلق، `SITE_URL` را در `.env` بگذارید +- **متا برای خزنده‌های بدون جاوااسکریپت** (تلگرام، واتس‌اپ و...): بک‌اند صفحات + `/blog`، `/blog/{slug}`، `/projects` و `/projects/{slug}` را با همان `index.html` + فرانت‌اند سرو می‌کند و پیش از ارسال، title / description / OG / JSON-LD را از + دیتابیس داخل آن تزریق می‌کند (`app/seo_pages.py`) — بازدیدکننده‌های عادی همان SPA + همیشگی را می‌بینند + +## پشتیبان‌گیری + +همه محتوای سایت در PostgreSQL و تصاویر در MinIO است — هر دو را با یک دستور پشتیبان بگیرید: + +```bash +bash scripts/backup.sh # در backups/ — نگهداری پیش‌فرض ۱۴ روز +``` + +- خروجی: `backups/db-<زمان>.sql.gz` (فشرده) و `backups/images-<زمان>/` (آینه باکت) +- زمان‌بندی با cron یا Task Scheduler: نمونه دستور داخل خود اسکریپت هست +- بازیابی دیتابیس: `gunzip -c backups/db-<زمان>.sql.gz | docker compose exec -T db psql -U aramesh arameshplaza` + +## نکات امنیتی + +- مقادیر محرمانه در `.env` ریشه هستند (در `.gitignore`) و در `docker-compose.yml` + با سینتکس `${VAR:?}` اجباری شده‌اند — بدون `.env` استک بالا نمی‌آید +- رمز عبور ادمین با bcrypt هش می‌شود و از نوار مدیریت پایین سایت (دکمه «رمز») قابل تعویض است +- ورود با محدودیت تلاش محافظت می‌شود: ۵ ورود ناموفق در ۱۵ دقیقه از هر IP → خطای 429 +- توکن‌های JWT امضا دارند و مدت اعتبارشان قابل تنظیم است +- تصاویر باکت MinIO عمومی (فقط خواندن) هستند؛ آپلود فقط با توکن ادمین ممکن است +- مقاله‌های پیش‌نویس در هیچ انتهای عمومی نمی‌آیند و در HTML تزریق‌شده سئو هم افشا نمی‌شوند diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..1429894 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,29 @@ +# کپی کنید و نام آن را بگذارید ‎.env‎ — مقادیر را با تنظیمات خودتان جایگزین کنید + +# رشته اتصال به PostgreSQL (پسورد، نام کاربری و نام دیتابیس را تنظیم کنید) +DATABASE_URL=postgresql+psycopg://postgres:YOUR_PASSWORD@localhost:5432/arameshplaza_api + +# کلید امضای توکن JWT — حتماً یک مقدار تصادفی طولانی بگذارید +SECRET_KEY=change-me-to-a-long-random-string + +# مدت اعتبار توکن مدیریت بر حسب دقیقه (پیش‌فرض: ۲۴ ساعت) +ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +# آدرس‌های مجاز برای CORS (لیست JSON — آدرس فرانت‌اند خود را اضافه کنید) +CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173"] + +# مشخصات ادمین پیش‌فرض که هنگام seed ساخته می‌شود +ADMIN_USERNAME=admin +ADMIN_PASSWORD=aramesh123 + +# Object Storage (MinIO) برای آپلود تصاویر پنل مدیریت +S3_ENDPOINT=http://localhost:9000 +S3_ACCESS_KEY=aramesh +S3_SECRET_KEY=change-me-minio-secret +S3_BUCKET=aramesh-media + +# پیشوند عمومی مسیر تصاویر — پروکسی Reverse باید آن را به باکت برساند +MEDIA_PUBLIC_BASE=/media + +# حداکثر حجم آپلود تصویر (مگابایت) +MEDIA_MAX_UPLOAD_MB=10 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b060621 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.13-slim + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +EXPOSE 8000 + +# ساخت جداول و seed (بدون حذف داده‌های موجود) و سپس اجرای سرور +CMD ["sh", "-c", "python -m app.seed && uvicorn app.main:app --host 0.0.0.0 --port 8000"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..7f6b098 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,189 @@ +# بک‌اند گروه ساختمانی آرامش (Aramesh Building Group API) + +بک‌اند سایت گروه ساختمانی آرامش — ساخته‌شده با **FastAPI** + **PostgreSQL** + **SQLAlchemy 2** + **JWT** + **MinIO** (ذخیره تصاویر) + **Traefik** (مسیریابی). + +## امکانات + +- **API عمومی محتوا**: برند، منو، هیرو، داستان، تیتر بخش‌ها، هیرو صفحات، خدمات، پروژه‌ها، مقالات، نظرات مشتریان، سوالات متداول، تیم، تایم‌لاین، آمار، چرا آرامش، قبل/بعد، مدل‌های سرمایه‌گذاری، مراحل کار، دفاتر، گالری، فرم تماس، خبرنامه و فوتر — **تمام متن‌های سایت در دیتابیس است** +- **فرم تماس**: ثبت پیام مشتریان از سایت (`POST /api/contact`) +- **خبرنامه**: عضویت و مدیریت مشترکین (`POST /api/newsletter`) +- **پنل مدیریت**: ورود با نام کاربری/رمز عبور و دریافت توکن JWT، سپس CRUD کامل همه محتواها، مدیریت پیام‌های تماس و مشترکین +- **آپلود تصویر**: `POST /api/admin/upload` — فایل در MinIO ذخیره و URL نسبی `/media/...` برمی‌گردد +- **Seed خودکار**: همه محتوای فعلی سایت با یک دستور در دیتابیس قرار می‌گیرد (فقط جدول‌های خالی پر می‌شوند — ارتقا بدون از دست رفتن داده) +- مستندات تعاملی Swagger در مسیر `/docs` + +## راه‌اندازی + +### ۱. پیش‌نیازها + +- Python 3.11 یا جدیدتر +- PostgreSQL در حال اجرا (پورت پیش‌فرض 5432) + +### ۲. نصب وابستگی‌ها + +```bash +cd arameshplaza-backend +python -m venv .venv + +# ویندوز +.venv\Scripts\activate +# لینوکس / مک +source .venv/bin/activate + +pip install -r requirements.txt +``` + +### ۳. تنظیمات + +فایل `.env.example` را به نام `.env` کپی کنید و مقادیر را تنظیم کنید: + +```bash +cp .env.example .env # لینوکس / مک +copy .env.example .env # ویندوز +``` + +مهم‌ترین تنظیم `DATABASE_URL` است: + +``` +DATABASE_URL=postgresql+psycopg://postgres:YOUR_PASSWORD@localhost:5432/arameshplaza_api +``` + +### ۴. ساخت دیتابیس و Seed + +```bash +# ساخت دیتابیس (یک‌بار): +psql -U postgres -c "CREATE DATABASE arameshplaza_api;" + +# ساخت جداول + وارد کردن محتوای سایت + ساخت ادمین پیش‌فرض: +python -m app.seed + +# بازنشانی کامل داده‌ها در صورت نیاز: +python -m app.seed --force +``` + +### ۵. اجرا + +```bash +uvicorn app.main:app --reload +``` + +سپس: +- مستندات API: http://localhost:8000/docs +- بررسی سلامت: http://localhost:8000/api/health + +## ورود به پنل مدیریت + +``` +POST /api/auth/login +{ "username": "admin", "password": "aramesh123" } +``` + +پاسخ شامل `access_token` است. توکن را در هدر همه درخواست‌های مدیریتی قرار دهید: + +``` +Authorization: Bearer +``` + +> نام کاربری و رمز عبور پیش‌فرض از تنظیمات `.env` خوانده می‌شود — حتماً در محیط تولید تغییرشان دهید. + +تغییر رمز (با توکن): + +``` +POST /api/auth/change-password +{ "current_password": "...", "new_password": "..." } +``` + +## فهرست انتهاپوینت‌ها + +### عمومی (بدون نیاز به توکن) + +| متد | مسیر | توضیح | +|---|---|---| +| GET | `/api/health` | وضعیت سرور | +| GET | `/api/brand` | اطلاعات برند و تماس | +| GET | `/api/services` | خدمات | +| GET | `/api/projects` | همه پروژه‌ها | +| GET | `/api/projects/{slug}` | جزئیات یک پروژه | +| GET | `/api/articles` | مقاله‌های منتشرشده (پیش‌نویس نمی‌آید) | +| GET | `/api/articles/{slug}` | جزئیات یک مقاله منتشرشده | +| GET | `/api/testimonials` | نظرات مشتریان | +| GET | `/api/faqs` | سوالات متداول | +| GET | `/api/team` | اعضای تیم | +| GET | `/api/timeline` | تایم‌لاین شرکت | +| GET | `/api/stats` | آمار | +| GET | `/api/why-us` | چرا گروه ساختمانی آرامش | +| GET | `/api/before-after` | بخش قبل/بعد | +| GET | `/api/site` | **همه محتوا در یک درخواست** | +| POST | `/api/contact` | ثبت پیام تماس | +| POST | `/api/newsletter` | عضویت در خبرنامه | +| POST | `/api/auth/login` | ورود ادمین و دریافت توکن — بعد از ۵ تلاش ناموفق در ۱۵ دقیقه، 429 | +| GET | `/robots.txt` | قوانین خزنده‌ها | +| GET | `/sitemap.xml` | نقشه سایت — مقاله/پروژه‌های منتشرشده | +| GET | `/blog/{slug}` | HTML صفحه مقاله با متای سئوی تزریق‌شده | +| GET | `/projects/{slug}` | HTML صفحه پروژه با متای سئوی تزریق‌شده | + +### مدیریت (نیاز به توکن Bearer) + +برای هر یک از این مسیرها، CRUD کامل در دسترس است: +`brand`، `services`، `projects`، `articles`، `testimonials`، `faqs`، `team`، `timeline`، `stats`، `why-us`، `before-after` + +| متد | مسیر | توضیح | +|---|---|---| +| GET/POST | `/api/admin/{resource}` | لیست / ساخت | +| GET/PUT/DELETE | `/api/admin/{resource}/{id}` | دریافت / ویرایش کامل / حذف | +| PATCH | `/api/admin/{resource}/{id}` | **ویرایش جزئی** — فقط فیلدهای ارسال‌شده به‌روزرسانی می‌شوند | +| POST | `/api/admin/{resource}/reorder` | ذخیره ترتیب: `{"ids": [3, 1, 2, ...]}` | +| GET | `/api/admin/messages` | پیام‌های تماس (با `?unread_only=true` فقط خوانده‌نشده‌ها) | +| PATCH | `/api/admin/messages/{id}` | تغییر وضعیت خوانده‌شدن | +| DELETE | `/api/admin/messages/{id}` | حذف پیام | +| GET | `/api/admin/subscribers` | مشترکین خبرنامه | +| DELETE | `/api/admin/subscribers/{id}` | حذف مشترک | +| POST | `/api/admin/upload` | آپلود تصویر (multipart، حداکثر ۱۰MB — jpg/png/webp/gif/avif) → `{"url": "/media/..."}` | + +### بدنه مقاله — سند TipTap + +`body` مقاله یک سند ساختاریافته TipTap (ProseMirror JSON) است: `{"type": "doc", "content": [...]}`. +- فقط ساختار اعتبارسنجی می‌شود (`type: "doc"` + `content` لیست)؛ امنیت رندر بر عهده + رندر استاتیک TipTap در فرانت‌اند است که فقط تگ‌های شناخته‌شده تولید می‌کند +- ردیف‌های قدیمی (لیست پاراگراف) با مهاجرت idempotent `migrate_article_bodies` + در startup/seed خودکار به سند تبدیل می‌شوند +- seed هم بدنه‌های لیستی را قبل از درج تبدیل می‌کند + +| POST/PATCH | `/api/admin/seo-meta` | عنوان و توضیح متای صفحات — `page_key`: home / about / services / projects / investment / blog / faq / contact | + +منابعی که `reorder` دارند (ستون `position`): `services`، `projects`، `articles`، `testimonials`، `faqs`، `team`، `timeline`، `stats`، `why-us`، `investment-models`، `process-steps`، `offices`، `gallery`، `nav`. ستون با مهاجرت سبک idempotent هنگام startup/seed به دیتابیس‌های موجود اضافه می‌شود. + +### تصاویر و MinIO + +فایل‌های آپلودشده در باکت `aramesh-media` روی MinIO ذخیره می‌شوند و با مسیر نسبی `/media/<سال>/<ماه>/<فایل>` برگردانده می‌شوند. Traefik مسیر `/media` را به MinIO می‌برد، پس URLها مستقل از بک‌اند ذخیره‌سازی هستند — بعداً برای مهاجرت به R2/S3 فقط تنظیمات را عوض کنید، نه محتوای دیتابیس را. + +- کنسول MinIO: `http://localhost:9001` (کاربر/رمز از `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` در compose) +- تصاویر عمومی و قابل نمایش‌اند؛ آپلود فقط با توکن ادمین انجام می‌شود. + +## اتصال به فرانت‌اند + +در پروژه React (پوشه `ArameshPlaza`)، فایل `src/config.ts` آدرس بک‌اند را نگه می‌دارد: + +```ts +export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8000' +``` + +در محیط تولید مقدار `VITE_API_URL` را روی آدرس عمومی API تنظیم کنید و مطمئن شوید آدرس سایت در `CORS_ORIGINS` فایل `.env` بک‌اند قرار دارد. + +## ساختار پروژه + +``` +app/ +├── main.py # نقطه ورود FastAPI، CORS، ثبت روترها +├── config.py # تنظیمات از .env +├── database.py # اتصال PostgreSQL و نشست‌ها +├── models.py # مدل‌های SQLAlchemy +├── schemas.py # اسکیماهای Pydantic (ورودی/خروجی) +├── auth.py # هش رمز عبور و JWT +├── seed.py # اسکریپت seed +├── seed_data.py # داده‌های اولیه سایت +└── api/ + ├── public.py # انتهای عمومی محتوا + ├── forms.py # فرم تماس و خبرنامه + └── admin.py # ورود و پنل مدیریت +``` diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py new file mode 100644 index 0000000..eedbea4 --- /dev/null +++ b/backend/app/api/admin.py @@ -0,0 +1,354 @@ +"""انتهای پنل مدیریت — همه مسیرها نیازمند توکن JWT معتبر هستند.""" + +import uuid +from datetime import datetime, timezone + +import boto3 +from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status +from sqlalchemy import func +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session + +from .. import models, schemas +from ..auth import create_access_token, get_current_admin, hash_password, verify_password +from ..config import settings +from ..database import get_db +from ..rate_limit import clear as clear_rate_limit +from ..rate_limit import ensure_allowed, record_failure +from ..schemas import ( + ArticleIn, + ArticleOut, + ArticlePatch, + BeforeAfterIn, + BeforeAfterOut, + BeforeAfterPatch, + BrandIn, + BrandOut, + BrandPatch, + ChangePasswordIn, + ContactSectionIn, + ContactSectionOut, + ContactSectionPatch, + ContactMessageOut, + FaqIn, + FaqOut, + FaqPatch, + FooterIn, + FooterOut, + FooterPatch, + GalleryImageIn, + GalleryImageOut, + GalleryImagePatch, + HeroIn, + HeroOut, + HeroPatch, + InvestmentModelIn, + InvestmentModelOut, + InvestmentModelPatch, + LoginIn, + MessageReadIn, + NavItemIn, + NavItemOut, + NavItemPatch, + NewsletterSectionIn, + NewsletterOut, + NewsletterSectionPatch, + OfficeIn, + OfficeOut, + OfficePatch, + PageHeroIn, + PageHeroOut, + PageHeroPatch, + ProcessStepIn, + ProcessStepOut, + ProcessStepPatch, + ProjectIn, + ProjectOut, + ProjectPatch, + ReorderIn, + SeoMetaIn, + SeoMetaOut, + SeoMetaPatch, + SectionHeadingIn, + SectionHeadingOut, + SectionHeadingPatch, + ServiceIn, + ServiceOut, + ServicePatch, + StatIn, + StatOut, + StatPatch, + StatusLabelIn, + StatusLabelOut, + StatusLabelPatch, + StoryIn, + StoryOut, + StoryPatch, + SubscriberOut, + TeamMemberIn, + TeamMemberOut, + TeamMemberPatch, + TestimonialIn, + TestimonialOut, + TestimonialPatch, + TimelineItemIn, + TimelineItemOut, + TimelineItemPatch, + TokenOut, + WhyUsItemIn, + WhyUsItemOut, + WhyUsItemPatch, +) + +# روتر عمومی برای ورود — بدون نیاز به توکن +auth_router = APIRouter() + +# روتر محافظت‌شده — همه مسیرهای آن فقط با توکن معتبر قابل دسترسی هستند +router = APIRouter(dependencies=[Depends(get_current_admin)]) + + +@auth_router.post("/auth/login", response_model=TokenOut) +def login(payload: LoginIn, request: Request, db: Session = Depends(get_db)): + ensure_allowed(request) + user = db.query(models.AdminUser).filter(models.AdminUser.username == payload.username).first() + if user is None or not verify_password(payload.password, user.password_hash): + record_failure(request) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="نام کاربری یا رمز عبور اشتباه است", + ) + clear_rate_limit(request) + return TokenOut(access_token=create_access_token(user.username)) + + +@auth_router.post("/auth/change-password") +def change_password( + payload: ChangePasswordIn, + admin: models.AdminUser = Depends(get_current_admin), + db: Session = Depends(get_db), +): + """تغییر رمز ادمین واردشده — رمز فعلی باید درست باشد.""" + if not verify_password(payload.current_password, admin.password_hash): + raise HTTPException(status_code=400, detail="رمز عبور فعلی اشتباه است") + admin.password_hash = hash_password(payload.new_password) + db.commit() + return {"ok": True} + + +def register_crud(path: str, model, out_schema, create_schema, patch_schema=None) -> None: + """ثبت مسیرهای استاندارد CRUD (لیست، ساخت، دریافت، ویرایش کامل، ویرایش جزئی، + حذف و ترتیب‌بندی) روی روتر مدیریت.""" + + @router.get(f"/{path}", response_model=list[out_schema]) + def list_items(db: Session = Depends(get_db)): + query = db.query(model) + if hasattr(model, "position"): + query = query.order_by(model.position, model.id) + else: + query = query.order_by(model.id) + return query.all() + + @router.post(f"/{path}", response_model=out_schema, status_code=201) + def create_item(payload: create_schema, db: Session = Depends(get_db)): + item = model(**payload.model_dump()) + if hasattr(model, "position"): + # آیتم جدید در انتهای لیست قرار می‌گیرد + item.position = (db.query(func.max(model.position)).scalar() or 0) + 1 + db.add(item) + try: + db.commit() + except IntegrityError: + db.rollback() + raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)") + db.refresh(item) + return item + + @router.get(f"/{path}/{{item_id}}", response_model=out_schema) + def get_item(item_id: int, db: Session = Depends(get_db)): + item = db.get(model, item_id) + if item is None: + raise HTTPException(status_code=404, detail="رکورد یافت نشد") + return item + + @router.put(f"/{path}/{{item_id}}", response_model=out_schema) + def update_item(item_id: int, payload: create_schema, db: Session = Depends(get_db)): + item = db.get(model, item_id) + if item is None: + raise HTTPException(status_code=404, detail="رکورد یافت نشد") + for key, value in payload.model_dump().items(): + setattr(item, key, value) + try: + db.commit() + except IntegrityError: + db.rollback() + raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)") + db.refresh(item) + return item + + if patch_schema is not None: + + @router.patch(f"/{path}/{{item_id}}", response_model=out_schema) + def patch_item(item_id: int, payload: patch_schema, db: Session = Depends(get_db)): + """ویرایش جزئی — فقط فیلدهای ارسال‌شده به‌روزرسانی می‌شوند (برای ویرایش زنده).""" + item = db.get(model, item_id) + if item is None: + raise HTTPException(status_code=404, detail="رکورد یافت نشد") + for key, value in payload.model_dump(exclude_unset=True).items(): + setattr(item, key, value) + try: + db.commit() + except IntegrityError: + db.rollback() + raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)") + db.refresh(item) + return item + + @router.post(f"/{path}/reorder", response_model=list[out_schema]) + def reorder_items(payload: ReorderIn, db: Session = Depends(get_db)): + """ذخیره ترتیب جدید آیتم‌ها بر اساس لیست idهای ارسالی.""" + if not hasattr(model, "position"): + raise HTTPException(status_code=400, detail="این منبع از ترتیب‌بندی پشتیبانی نمی‌کند") + items = db.query(model).filter(model.id.in_(payload.ids)).all() + if len(items) != len(payload.ids): + raise HTTPException(status_code=404, detail="برخی رکوردها یافت نشدند") + by_id = {item.id: item for item in items} + for pos, item_id in enumerate(payload.ids): + by_id[item_id].position = pos + db.commit() + return db.query(model).order_by(model.position, model.id).all() + + @router.delete(f"/{path}/{{item_id}}", status_code=204) + def delete_item(item_id: int, db: Session = Depends(get_db)): + item = db.get(model, item_id) + if item is None: + raise HTTPException(status_code=404, detail="رکورد یافت نشد") + db.delete(item) + db.commit() + + +# CRUD محتوا +register_crud("brand", models.Brand, BrandOut, BrandIn, BrandPatch) +register_crud("nav", models.NavItem, NavItemOut, NavItemIn, NavItemPatch) +register_crud("hero", models.Hero, HeroOut, HeroIn, HeroPatch) +register_crud("story", models.StorySection, StoryOut, StoryIn, StoryPatch) +register_crud("section-headings", models.SectionHeadingContent, SectionHeadingOut, SectionHeadingIn, SectionHeadingPatch) +register_crud("page-heroes", models.PageHeroContent, PageHeroOut, PageHeroIn, PageHeroPatch) +register_crud("services", models.Service, ServiceOut, ServiceIn, ServicePatch) +register_crud("projects", models.Project, ProjectOut, ProjectIn, ProjectPatch) +register_crud("articles", models.Article, ArticleOut, ArticleIn, ArticlePatch) +register_crud("testimonials", models.Testimonial, TestimonialOut, TestimonialIn, TestimonialPatch) +register_crud("faqs", models.Faq, FaqOut, FaqIn, FaqPatch) +register_crud("team", models.TeamMember, TeamMemberOut, TeamMemberIn, TeamMemberPatch) +register_crud("timeline", models.TimelineItem, TimelineItemOut, TimelineItemIn, TimelineItemPatch) +register_crud("stats", models.Stat, StatOut, StatIn, StatPatch) +register_crud("why-us", models.WhyUsItem, WhyUsItemOut, WhyUsItemIn, WhyUsItemPatch) +register_crud("before-after", models.BeforeAfter, BeforeAfterOut, BeforeAfterIn, BeforeAfterPatch) +register_crud("investment-models", models.InvestmentModel, InvestmentModelOut, InvestmentModelIn, InvestmentModelPatch) +register_crud("process-steps", models.ProcessStep, ProcessStepOut, ProcessStepIn, ProcessStepPatch) +register_crud("offices", models.Office, OfficeOut, OfficeIn, OfficePatch) +register_crud("gallery", models.GalleryImage, GalleryImageOut, GalleryImageIn, GalleryImagePatch) +register_crud("status-labels", models.StatusLabel, StatusLabelOut, StatusLabelIn, StatusLabelPatch) +register_crud("contact-section", models.ContactSectionContent, ContactSectionOut, ContactSectionIn, ContactSectionPatch) +register_crud("newsletter", models.NewsletterSectionContent, NewsletterOut, NewsletterSectionIn, NewsletterSectionPatch) +register_crud("footer", models.FooterContent, FooterOut, FooterIn, FooterPatch) +register_crud("seo-meta", models.SeoMeta, SeoMetaOut, SeoMetaIn, SeoMetaPatch) + + +# آپلود تصویر به Object Storage (MinIO) — URL نسبی برمی‌گرداند تا بک‌اند ذخیره‌سازی قابل تعویض باشد +ALLOWED_IMAGE_TYPES = { + "image/jpeg": "jpg", + "image/png": "png", + "image/webp": "webp", + "image/gif": "gif", + "image/avif": "avif", +} + + +@router.post("/upload") +async def upload_media( + file: UploadFile = File(...), + _admin: models.AdminUser = Depends(get_current_admin), +): + content_type = (file.content_type or "").lower() + ext = ALLOWED_IMAGE_TYPES.get(content_type) + if ext is None: + raise HTTPException( + status_code=415, + detail="فرمت فایل پشتیبانی نمی‌شود (مجاز: jpg، png، webp، gif، avif)", + ) + + max_bytes = settings.media_max_upload_mb * 1024 * 1024 + data = await file.read() + if len(data) == 0: + raise HTTPException(status_code=400, detail="فایل خالی است") + if len(data) > max_bytes: + raise HTTPException( + status_code=413, + detail=f"حجم فایل بیش از {settings.media_max_upload_mb} مگابایت است", + ) + + now = datetime.now(timezone.utc) + key = f"{now:%Y/%m}/{uuid.uuid4().hex}.{ext}" + + s3 = boto3.client( + "s3", + endpoint_url=settings.s3_endpoint, + aws_access_key_id=settings.s3_access_key, + aws_secret_access_key=settings.s3_secret_key, + region_name="us-east-1", + ) + try: + s3.put_object(Bucket=settings.s3_bucket, Key=key, Body=data, ContentType=content_type) + except Exception: + raise HTTPException(status_code=502, detail="ذخیره فایل در Object Storage ناموفق بود") + + return { + "url": f"{settings.media_public_base}/{key}", + "key": key, + "size": len(data), + "content_type": content_type, + } + + +# پیام‌های تماس +@router.get("/messages", response_model=list[ContactMessageOut]) +def list_messages(unread_only: bool = False, db: Session = Depends(get_db)): + query = db.query(models.ContactMessage) + if unread_only: + query = query.filter(models.ContactMessage.is_read.is_(False)) + return query.order_by(models.ContactMessage.created_at.desc()).all() + + +@router.patch("/messages/{message_id}", response_model=ContactMessageOut) +def update_message(message_id: int, payload: MessageReadIn, db: Session = Depends(get_db)): + message = db.get(models.ContactMessage, message_id) + if message is None: + raise HTTPException(status_code=404, detail="پیام یافت نشد") + message.is_read = payload.is_read + db.commit() + db.refresh(message) + return message + + +@router.delete("/messages/{message_id}", status_code=204) +def delete_message(message_id: int, db: Session = Depends(get_db)): + message = db.get(models.ContactMessage, message_id) + if message is None: + raise HTTPException(status_code=404, detail="پیام یافت نشد") + db.delete(message) + db.commit() + + +# مشترکین خبرنامه +@router.get("/subscribers", response_model=list[SubscriberOut]) +def list_subscribers(db: Session = Depends(get_db)): + return db.query(models.Subscriber).order_by(models.Subscriber.created_at.desc()).all() + + +@router.delete("/subscribers/{subscriber_id}", status_code=204) +def delete_subscriber(subscriber_id: int, db: Session = Depends(get_db)): + subscriber = db.get(models.Subscriber, subscriber_id) + if subscriber is None: + raise HTTPException(status_code=404, detail="مشترک یافت نشد") + db.delete(subscriber) + db.commit() diff --git a/backend/app/api/forms.py b/backend/app/api/forms.py new file mode 100644 index 0000000..9692b40 --- /dev/null +++ b/backend/app/api/forms.py @@ -0,0 +1,42 @@ +"""انتهای فرم‌ها — ثبت پیام تماس و عضویت در خبرنامه.""" + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from ..database import get_db +from .. import models +from ..schemas import ContactIn, ContactMessageOut, NewsletterIn, SubscriberOut + +router = APIRouter() + + +@router.post("/contact", response_model=ContactMessageOut, status_code=201) +def submit_contact(payload: ContactIn, db: Session = Depends(get_db)): + message = models.ContactMessage( + name=payload.name.strip(), + phone=payload.phone.strip(), + note=payload.note.strip(), + ) + db.add(message) + db.commit() + db.refresh(message) + return message + + +@router.post("/newsletter", response_model=SubscriberOut, status_code=201) +def subscribe(payload: NewsletterIn, db: Session = Depends(get_db)): + email = str(payload.email).lower() + subscriber = db.query(models.Subscriber).filter(models.Subscriber.email == email).first() + + if subscriber is not None: + if not subscriber.is_active: + subscriber.is_active = True + db.commit() + db.refresh(subscriber) + return subscriber + + subscriber = models.Subscriber(email=email) + db.add(subscriber) + db.commit() + db.refresh(subscriber) + return subscriber diff --git a/backend/app/api/public.py b/backend/app/api/public.py new file mode 100644 index 0000000..25a312d --- /dev/null +++ b/backend/app/api/public.py @@ -0,0 +1,149 @@ +"""انتهای عمومی — خواندن محتوای سایت بدون نیاز به احراز هویت.""" + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from ..database import get_db +from .. import models +from ..schemas import ( + ArticleOut, + BeforeAfterOut, + BrandOut, + FaqOut, + ProjectOut, + ServiceOut, + StatOut, + TeamMemberOut, + TestimonialOut, + TimelineItemOut, + WhyUsItemOut, +) + +router = APIRouter() + + +@router.get("/health") +def health(): + return {"status": "ok", "app": "Aramesh Building Group API"} + + +@router.get("/brand", response_model=BrandOut) +def get_brand(db: Session = Depends(get_db)): + brand = db.query(models.Brand).first() + if brand is None: + raise HTTPException(status_code=404, detail="اطلاعات برند یافت نشد") + return brand + + +@router.get("/services", response_model=list[ServiceOut]) +def get_services(db: Session = Depends(get_db)): + return db.query(models.Service).order_by(models.Service.position, models.Service.id).all() + + +@router.get("/projects", response_model=list[ProjectOut]) +def get_projects(db: Session = Depends(get_db)): + return db.query(models.Project).order_by(models.Project.position, models.Project.id).all() + + +@router.get("/projects/{slug}", response_model=ProjectOut) +def get_project(slug: str, db: Session = Depends(get_db)): + project = db.query(models.Project).filter(models.Project.slug == slug).first() + if project is None: + raise HTTPException(status_code=404, detail="پروژه یافت نشد") + return project + + +@router.get("/articles", response_model=list[ArticleOut]) +def get_articles(db: Session = Depends(get_db)): + return ( + db.query(models.Article) + .filter(models.Article.status == "published") + .order_by(models.Article.position, models.Article.id) + .all() + ) + + +@router.get("/articles/{slug}", response_model=ArticleOut) +def get_article(slug: str, db: Session = Depends(get_db)): + article = ( + db.query(models.Article) + .filter(models.Article.slug == slug, models.Article.status == "published") + .first() + ) + if article is None: + raise HTTPException(status_code=404, detail="مقاله یافت نشد") + return article + + +@router.get("/testimonials", response_model=list[TestimonialOut]) +def get_testimonials(db: Session = Depends(get_db)): + return db.query(models.Testimonial).order_by(models.Testimonial.position, models.Testimonial.id).all() + + +@router.get("/faqs", response_model=list[FaqOut]) +def get_faqs(db: Session = Depends(get_db)): + return db.query(models.Faq).order_by(models.Faq.position, models.Faq.id).all() + + +@router.get("/team", response_model=list[TeamMemberOut]) +def get_team(db: Session = Depends(get_db)): + return db.query(models.TeamMember).order_by(models.TeamMember.position, models.TeamMember.id).all() + + +@router.get("/timeline", response_model=list[TimelineItemOut]) +def get_timeline(db: Session = Depends(get_db)): + return db.query(models.TimelineItem).order_by(models.TimelineItem.position, models.TimelineItem.id).all() + + +@router.get("/stats", response_model=list[StatOut]) +def get_stats(db: Session = Depends(get_db)): + return db.query(models.Stat).order_by(models.Stat.position, models.Stat.id).all() + + +@router.get("/why-us", response_model=list[WhyUsItemOut]) +def get_why_us(db: Session = Depends(get_db)): + return db.query(models.WhyUsItem).order_by(models.WhyUsItem.position, models.WhyUsItem.id).all() + + +@router.get("/before-after", response_model=BeforeAfterOut) +def get_before_after(db: Session = Depends(get_db)): + item = db.query(models.BeforeAfter).first() + if item is None: + raise HTTPException(status_code=404, detail="بخش قبل/بعد یافت نشد") + return item + + +@router.get("/site") +def get_site(db: Session = Depends(get_db)): + """همه محتوای سایت در یک درخواست — مناسب برای بارگذاری اولیه فرانت‌اند.""" + return { + "brand": db.query(models.Brand).first(), + "nav": db.query(models.NavItem).order_by(models.NavItem.position, models.NavItem.id).all(), + "hero": db.query(models.Hero).first(), + "story": db.query(models.StorySection).first(), + "section_headings": db.query(models.SectionHeadingContent).order_by(models.SectionHeadingContent.id).all(), + "page_heroes": db.query(models.PageHeroContent).order_by(models.PageHeroContent.id).all(), + "services": db.query(models.Service).order_by(models.Service.position, models.Service.id).all(), + "projects": db.query(models.Project).order_by(models.Project.position, models.Project.id).all(), + # فقط مقاله‌های منتشرشده — پیش‌نویس‌ها از انتهای مدیریتی خوانده می‌شوند + "articles": db.query(models.Article) + .filter(models.Article.status == "published") + .order_by(models.Article.position, models.Article.id) + .all(), + "testimonials": db.query(models.Testimonial).order_by(models.Testimonial.position, models.Testimonial.id).all(), + "faqs": db.query(models.Faq).order_by(models.Faq.position, models.Faq.id).all(), + "team": db.query(models.TeamMember).order_by(models.TeamMember.position, models.TeamMember.id).all(), + "timeline": db.query(models.TimelineItem).order_by(models.TimelineItem.position, models.TimelineItem.id).all(), + "stats": db.query(models.Stat).order_by(models.Stat.position, models.Stat.id).all(), + "why_us": db.query(models.WhyUsItem).order_by(models.WhyUsItem.position, models.WhyUsItem.id).all(), + "before_after": db.query(models.BeforeAfter).first(), + "investment_models": db.query(models.InvestmentModel).order_by(models.InvestmentModel.position, models.InvestmentModel.id).all(), + "process_steps": db.query(models.ProcessStep).order_by(models.ProcessStep.position, models.ProcessStep.id).all(), + "offices": db.query(models.Office).order_by(models.Office.position, models.Office.id).all(), + "gallery": db.query(models.GalleryImage).order_by(models.GalleryImage.position, models.GalleryImage.id).all(), + "status_labels": db.query(models.StatusLabel).order_by(models.StatusLabel.id).all(), + "contact_section": db.query(models.ContactSectionContent).first(), + "newsletter": db.query(models.NewsletterSectionContent).first(), + "footer": db.query(models.FooterContent).first(), + "seo_meta": db.query(models.SeoMeta).order_by(models.SeoMeta.id).all(), + } diff --git a/backend/app/auth.py b/backend/app/auth.py new file mode 100644 index 0000000..0532e31 --- /dev/null +++ b/backend/app/auth.py @@ -0,0 +1,52 @@ +from datetime import datetime, timedelta, timezone + +import bcrypt +import jwt +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from .config import settings +from .database import get_db +from .models import AdminUser + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + + +def hash_password(password: str) -> str: + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + + +def verify_password(password: str, hashed: str) -> bool: + return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8")) + + +def create_access_token(username: str) -> str: + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) + payload = {"sub": username, "exp": expire} + return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm) + + +def get_current_admin( + token: str = Depends(oauth2_scheme), + db: Session = Depends(get_db), +) -> AdminUser: + """وابستگی محافظت‌شده — فقط با توکن JWT معتبر دسترسی می‌دهد.""" + credentials_error = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="احراز هویت نامعتبر است", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) + except jwt.PyJWTError: + raise credentials_error + + username = payload.get("sub") + if not username: + raise credentials_error + + user = db.query(AdminUser).filter(AdminUser.username == username).first() + if user is None: + raise credentials_error + return user diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..2d97aeb --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,36 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """تنظیمات برنامه — از متغیرهای محیطی یا فایل .env خوانده می‌شود.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/arameshplaza_api" + secret_key: str = "dev-secret-change-me-in-production" + algorithm: str = "HS256" + access_token_expire_minutes: int = 1440 + cors_origins: list[str] = ["*"] + admin_username: str = "admin" + admin_password: str = "aramesh123" + + # Object Storage (MinIO / هر S3 سازگار) برای آپلود تصاویر + s3_endpoint: str = "http://localhost:9000" + s3_access_key: str = "aramesh" + s3_secret_key: str = "aramesh-minio-secret" + s3_bucket: str = "aramesh-media" + # پیشوند عمومی مسیر تصاویر — Traefik آن را به باکت پروکسی می‌کند + media_public_base: str = "/media" + media_max_upload_mb: int = 10 + + # نشانی کامل سایت (مثل https://aramesh.example) — برای sitemap و آدرس مطلق OG + site_url: str = "" + # مسیر بیلد فرانت‌اند (شامل index.html) — برای سرو HTML با متای سئو در صفحات جزئیات + frontend_dist: str = "../frontend/dist" + + +settings = Settings() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..e1eee4c --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase, sessionmaker + +from .config import settings + +engine = create_engine(settings.database_url, pool_pre_ping=True) +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + + +class Base(DeclarativeBase): + pass + + +def get_db(): + """وابستگی FastAPI برای تزریق نشست دیتابیس.""" + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..e04fea7 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,47 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from . import models # noqa: F401 — ثبت مدل‌ها پیش از ساخت جداول +from .api import admin, forms, public +from .config import settings +from .database import Base, engine +from .migrations import run_all as run_migrations +from .seo_pages import router as seo_router + +Base.metadata.create_all(bind=engine) +run_migrations() + +app = FastAPI( + title="Aramesh Building Group API", + description=( + "بک‌اند سایت گروه ساختمانی آرامش — API محتوای سایت، فرم تماس و خبرنامه، " + "و پنل مدیریت با احراز هویت JWT." + ), + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(public.router, prefix="/api", tags=["عمومی"]) +app.include_router(forms.router, prefix="/api", tags=["فرم‌ها"]) +app.include_router(admin.auth_router, prefix="/api", tags=["احراز هویت"]) +app.include_router(admin.router, prefix="/api/admin", tags=["پنل مدیریت"]) +# robots.txt، sitemap.xml و HTML صفحات وبلاگ/پروژه با متای سئو — در ریشه دامنه +app.include_router(seo_router, tags=["سئو"]) + + +@app.get("/") +def root(): + return { + "app": "Aramesh Building Group API", + "docs": "/docs", + "health": "/api/health", + } diff --git a/backend/app/migrations.py b/backend/app/migrations.py new file mode 100644 index 0000000..3e4cd48 --- /dev/null +++ b/backend/app/migrations.py @@ -0,0 +1,71 @@ +"""مهاجرت‌های سبک دیتابیس — DDL و تبدیل داده‌های قدیمی؛ همه idempotent. + +هم main.py (startup) و هم seed.py این‌ها را صدا می‌زنند تا هر مسیر اجرایی +دیتابیس موجود را بدون از دست رفتن داده ارتقا دهد. +""" + +from sqlalchemy import text + +from .database import SessionLocal, engine + +# جدول‌های مجموعه‌ای که ستون position دارند +POSITION_TABLES = [ + "services", "projects", "articles", "testimonials", "faqs", "team", + "timeline", "stats", "why_us", "investment_models", "process_steps", + "offices", "gallery_images", "nav_items", +] + + +def run_ddl_migrations() -> None: + """ستون‌های جدید — برای PostgreSQL با ADD COLUMN IF NOT EXISTS.""" + if engine.dialect.name != "postgresql": + return + with engine.begin() as conn: + for table in POSITION_TABLES: + conn.execute( + text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS position INTEGER NOT NULL DEFAULT 0") + ) + # مقالات موجود منتشر شده‌اند؛ فقط مقاله‌های تازه می‌توانند پیش‌نویس باشند + conn.execute( + text("ALTER TABLE articles ADD COLUMN IF NOT EXISTS status VARCHAR(16) NOT NULL DEFAULT 'published'") + ) + conn.execute( + text("ALTER TABLE articles ADD COLUMN IF NOT EXISTS seo_keyphrase VARCHAR(128) NOT NULL DEFAULT ''") + ) + + +def paragraphs_to_doc(paragraphs: list) -> dict: + """تبدیل لیست پاراگراف‌های متنی (فرمت قدیمی بدنه مقاله) به سند TipTap.""" + content = [] + for para in paragraphs: + if isinstance(para, str) and para.strip(): + content.append({ + "type": "paragraph", + "content": [{"type": "text", "text": para}], + }) + if not content: + content = [{"type": "paragraph"}] + return {"type": "doc", "content": content} + + +def migrate_article_bodies() -> None: + """تبدیل بدنه مقالات از لیست پاراگراف به سند JSON — idempotent.""" + from . import models # lazy — جلوگیری از import حلقه‌ای + + db = SessionLocal() + try: + changed = 0 + for article in db.query(models.Article).all(): + if isinstance(article.body, list): + article.body = paragraphs_to_doc(article.body) + changed += 1 + db.commit() + if changed: + print(f"مقالات تبدیل‌شده به فرمت سند: {changed}") + finally: + db.close() + + +def run_all() -> None: + run_ddl_migrations() + migrate_article_bodies() diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..f5929a7 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,375 @@ +from datetime import datetime, timezone + +from sqlalchemy import JSON, Boolean, DateTime, Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from .database import Base + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +class AdminUser(Base): + __tablename__ = "admin_users" + + id: Mapped[int] = mapped_column(primary_key=True) + username: Mapped[str] = mapped_column(String(64), unique=True, index=True) + password_hash: Mapped[str] = mapped_column(String(255)) + + +class Brand(Base): + __tablename__ = "brand" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(128), default="") + logo: Mapped[str] = mapped_column(Text, default="") + hero: Mapped[str] = mapped_column(Text, default="") + contact_bg: Mapped[str] = mapped_column(Text, default="") + phone: Mapped[str] = mapped_column(String(64), default="") + phone_href: Mapped[str] = mapped_column(String(128), default="") + email: Mapped[str] = mapped_column(String(128), default="") + address: Mapped[str] = mapped_column(Text, default="") + + +class Service(Base): + __tablename__ = "services" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(256)) + desc: Mapped[str] = mapped_column(Text, default="") + icon: Mapped[str] = mapped_column(String(32), default="draft") + + +class Project(Base): + __tablename__ = "projects" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + slug: Mapped[str] = mapped_column(String(128), unique=True, index=True) + title: Mapped[str] = mapped_column(String(256)) + location: Mapped[str] = mapped_column(String(128), default="") + year: Mapped[str] = mapped_column(String(64), default="") + status: Mapped[str] = mapped_column(String(16), default="done") # done | progress + image: Mapped[str] = mapped_column(Text, default="") + gallery: Mapped[list] = mapped_column(JSON, default=list) + area: Mapped[str] = mapped_column(String(64), default="") + floors: Mapped[str] = mapped_column(String(64), default="") + units: Mapped[str] = mapped_column(String(64), default="") + client: Mapped[str] = mapped_column(String(128), default="") + summary: Mapped[str] = mapped_column(Text, default="") + body: Mapped[list] = mapped_column(JSON, default=list) + + +class Article(Base): + __tablename__ = "articles" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + slug: Mapped[str] = mapped_column(String(128), unique=True, index=True) + title: Mapped[str] = mapped_column(String(256)) + date: Mapped[str] = mapped_column(String(64), default="") + category: Mapped[str] = mapped_column(String(64), default="") + image: Mapped[str] = mapped_column(Text, default="") + read: Mapped[str] = mapped_column(String(32), default="") + excerpt: Mapped[str] = mapped_column(Text, default="") + body: Mapped[list] = mapped_column(JSON, default=list) + # draft | published — پیش‌نویس فقط برای ادمین دیده می‌شود + status: Mapped[str] = mapped_column(String(16), default="published") + # کلمه کلیدی هدف چک‌لیست سئو — راهنمای نوشتن، در سایت نمایش داده نمی‌شود + seo_keyphrase: Mapped[str] = mapped_column(String(128), default="") + + +class Testimonial(Base): + __tablename__ = "testimonials" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(128)) + role: Mapped[str] = mapped_column(String(128), default="") + quote: Mapped[str] = mapped_column(Text, default="") + + +class Faq(Base): + __tablename__ = "faqs" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + question: Mapped[str] = mapped_column(Text) + answer: Mapped[str] = mapped_column(Text, default="") + + +class TeamMember(Base): + __tablename__ = "team" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(128)) + role: Mapped[str] = mapped_column(String(128), default="") + bio: Mapped[str] = mapped_column(Text, default="") + + +class TimelineItem(Base): + __tablename__ = "timeline" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + year: Mapped[str] = mapped_column(String(32)) + title: Mapped[str] = mapped_column(String(256)) + desc: Mapped[str] = mapped_column(Text, default="") + image: Mapped[str] = mapped_column(Text, default="") + + +class Stat(Base): + __tablename__ = "stats" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + value: Mapped[int] = mapped_column(Integer) + suffix: Mapped[str] = mapped_column(String(16), default="") + label: Mapped[str] = mapped_column(String(128)) + + +class WhyUsItem(Base): + __tablename__ = "why_us" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(256)) + desc: Mapped[str] = mapped_column(Text, default="") + + +class BeforeAfter(Base): + __tablename__ = "before_after" + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(256), default="") + lead: Mapped[str] = mapped_column(Text, default="") + before: Mapped[str] = mapped_column(Text, default="") + after: Mapped[str] = mapped_column(Text, default="") + before_label: Mapped[str] = mapped_column(String(128), default="") + after_label: Mapped[str] = mapped_column(String(128), default="") + + +class NavItem(Base): + __tablename__ = "nav_items" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + label: Mapped[str] = mapped_column(String(64)) + href: Mapped[str] = mapped_column(String(128)) + + +class Hero(Base): + """محتوای هیرو صفحه اصلی — رکورد یگانه.""" + + __tablename__ = "hero" + + id: Mapped[int] = mapped_column(primary_key=True) + kicker: Mapped[str] = mapped_column(Text, default="") + title: Mapped[str] = mapped_column(Text, default="") + title_highlight: Mapped[str] = mapped_column(String(128), default="") + subtitle: Mapped[str] = mapped_column(Text, default="") + badge_value: Mapped[str] = mapped_column(String(32), default="") + badge_label: Mapped[str] = mapped_column(Text, default="") + cta_primary_label: Mapped[str] = mapped_column(String(128), default="") + cta_primary_href: Mapped[str] = mapped_column(String(128), default="") + cta_secondary_label: Mapped[str] = mapped_column(String(128), default="") + cta_secondary_href: Mapped[str] = mapped_column(String(128), default="") + scroll_label: Mapped[str] = mapped_column(String(64), default="") + + +class StorySection(Base): + """بخش «درباره ما» در خانه و صفحه درباره — رکورد یگانه.""" + + __tablename__ = "story" + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(256), default="") + paragraphs: Mapped[list] = mapped_column(JSON, default=list) + quote: Mapped[str] = mapped_column(Text, default="") + quote_author: Mapped[str] = mapped_column(Text, default="") + stat1_value: Mapped[str] = mapped_column(String(32), default="") + stat1_label: Mapped[str] = mapped_column(String(128), default="") + stat2_value: Mapped[str] = mapped_column(String(32), default="") + stat2_label: Mapped[str] = mapped_column(String(128), default="") + link_label: Mapped[str] = mapped_column(String(128), default="") + link_href: Mapped[str] = mapped_column(String(128), default="") + bento: Mapped[list] = mapped_column(JSON, default=list) # [{src, alt}] + + +class SectionHeadingContent(Base): + """تیتر و توضیح بخش‌ها — فرانت‌اند بر اساس key آن را پیدا می‌کند.""" + + __tablename__ = "section_headings" + + id: Mapped[int] = mapped_column(primary_key=True) + key: Mapped[str] = mapped_column(String(64), unique=True, index=True) + label: Mapped[str] = mapped_column(String(64), default="") + title: Mapped[str] = mapped_column(String(256), default="") + lead: Mapped[str] = mapped_column(Text, default="") + image: Mapped[str] = mapped_column(Text, default="") + cta_label: Mapped[str] = mapped_column(String(128), default="") + cta_href: Mapped[str] = mapped_column(String(128), default="") + + +class PageHeroContent(Base): + """هیرو بالای هر صفحه — key: about | services | projects | investment | blog | faq | contact.""" + + __tablename__ = "page_heroes" + + id: Mapped[int] = mapped_column(primary_key=True) + key: Mapped[str] = mapped_column(String(32), unique=True, index=True) + title: Mapped[str] = mapped_column(String(256), default="") + lead: Mapped[str] = mapped_column(Text, default="") + image: Mapped[str] = mapped_column(Text, default="") + + +class InvestmentModel(Base): + __tablename__ = "investment_models" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(256)) + desc: Mapped[str] = mapped_column(Text, default="") + points: Mapped[list] = mapped_column(JSON, default=list) + + +class ProcessStep(Base): + __tablename__ = "process_steps" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + step: Mapped[str] = mapped_column(String(16), default="") + title: Mapped[str] = mapped_column(String(256)) + desc: Mapped[str] = mapped_column(Text, default="") + + +class Office(Base): + __tablename__ = "offices" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + city: Mapped[str] = mapped_column(String(64)) + address: Mapped[str] = mapped_column(Text, default="") + hours: Mapped[str] = mapped_column(String(128), default="") + + +class GalleryImage(Base): + __tablename__ = "gallery_images" + + position: Mapped[int] = mapped_column(Integer, default=0) + + id: Mapped[int] = mapped_column(primary_key=True) + src: Mapped[str] = mapped_column(Text, default="") + alt: Mapped[str] = mapped_column(Text, default="") + + +class StatusLabel(Base): + """برچسب فارسی وضعیت پروژه — key: done | progress.""" + + __tablename__ = "status_labels" + + id: Mapped[int] = mapped_column(primary_key=True) + key: Mapped[str] = mapped_column(String(16), unique=True, index=True) + label: Mapped[str] = mapped_column(String(64)) + + +class ContactSectionContent(Base): + """بخش فرم تماس — رکورد یگانه.""" + + __tablename__ = "contact_section" + + id: Mapped[int] = mapped_column(primary_key=True) + label: Mapped[str] = mapped_column(String(64), default="") + title: Mapped[str] = mapped_column(String(256), default="") + lead: Mapped[str] = mapped_column(Text, default="") + address_label: Mapped[str] = mapped_column(String(64), default="") + phone_label: Mapped[str] = mapped_column(String(64), default="") + email_label: Mapped[str] = mapped_column(String(64), default="") + name_label: Mapped[str] = mapped_column(String(64), default="") + name_placeholder: Mapped[str] = mapped_column(String(128), default="") + phone_field_label: Mapped[str] = mapped_column(String(64), default="") + phone_placeholder: Mapped[str] = mapped_column(String(64), default="") + note_label: Mapped[str] = mapped_column(String(64), default="") + note_optional_label: Mapped[str] = mapped_column(String(64), default="") + note_placeholder: Mapped[str] = mapped_column(String(256), default="") + submit_label: Mapped[str] = mapped_column(String(128), default="") + sending_label: Mapped[str] = mapped_column(String(64), default="") + success_title: Mapped[str] = mapped_column(String(256), default="") + success_text: Mapped[str] = mapped_column(Text, default="") + success_button_label: Mapped[str] = mapped_column(String(128), default="") + + +class NewsletterSectionContent(Base): + """بخش خبرنامه — رکورد یگانه.""" + + __tablename__ = "newsletter" + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(256), default="") + lead: Mapped[str] = mapped_column(Text, default="") + placeholder: Mapped[str] = mapped_column(String(128), default="") + button_label: Mapped[str] = mapped_column(String(64), default="") + sending_label: Mapped[str] = mapped_column(String(64), default="") + success_text: Mapped[str] = mapped_column(String(256), default="") + + +class FooterContent(Base): + """متن‌های فوتر — رکورد یگانه.""" + + __tablename__ = "footer" + + id: Mapped[int] = mapped_column(primary_key=True) + about_text: Mapped[str] = mapped_column(Text, default="") + pages_heading: Mapped[str] = mapped_column(String(64), default="") + services_heading: Mapped[str] = mapped_column(String(64), default="") + contact_heading: Mapped[str] = mapped_column(String(64), default="") + copyright: Mapped[str] = mapped_column(String(256), default="") + tagline: Mapped[str] = mapped_column(String(256), default="") + socials: Mapped[list] = mapped_column(JSON, default=list) # [{label, href, icon}] + + +class SeoMeta(Base): + """عنوان و توضیح سئوی هر صفحه — page_key: home / about / services / ...""" + + __tablename__ = "seo_meta" + + id: Mapped[int] = mapped_column(primary_key=True) + page_key: Mapped[str] = mapped_column(String(32), unique=True, index=True) + title: Mapped[str] = mapped_column(String(256), default="") + description: Mapped[str] = mapped_column(Text, default="") + + +class ContactMessage(Base): + __tablename__ = "contact_messages" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(128)) + phone: Mapped[str] = mapped_column(String(32)) + note: Mapped[str] = mapped_column(Text, default="") + is_read: Mapped[bool] = mapped_column(Boolean, default=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) + + +class Subscriber(Base): + __tablename__ = "subscribers" + + id: Mapped[int] = mapped_column(primary_key=True) + email: Mapped[str] = mapped_column(String(256), unique=True, index=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/backend/app/rate_limit.py b/backend/app/rate_limit.py new file mode 100644 index 0000000..ec35dc3 --- /dev/null +++ b/backend/app/rate_limit.py @@ -0,0 +1,49 @@ +"""محدودسازی تلاش ورود — جلوگیری از حمله brute-force روی /api/auth/login. + +پیش‌فرض: حداکثر ۵ ورود ناموفق در هر ۱۵ دقیقه برای هر IP؛ ورود موفق شمارنده را پاک می‌کند. +شمارنده در حافظه همین پروسه است — با ری‌استارت بک‌اند پاک می‌شود و همین کافی است. +""" + +import time + +from fastapi import HTTPException, Request, status + +MAX_FAILED_ATTEMPTS = 5 +WINDOW_SECONDS = 15 * 60 + +_failed: dict[str, list[float]] = {} + + +def client_ip(request: Request) -> str: + forwarded = request.headers.get("x-forwarded-for") + if forwarded: + return forwarded.split(",")[0].strip() + return request.client.host if request.client else "unknown" + + +def _prune(key: str, now: float) -> list[float]: + recent = [t for t in _failed.get(key, []) if now - t < WINDOW_SECONDS] + _failed[key] = recent + return recent + + +def ensure_allowed(request: Request) -> None: + """اگر این IP سهمیه تلاش ناموفقش پر شده باشد، درخواست با 429 رد می‌شود.""" + key = client_ip(request) + recent = _prune(key, time.monotonic()) + if len(recent) >= MAX_FAILED_ATTEMPTS: + retry_after = max(1, int(WINDOW_SECONDS - (time.monotonic() - recent[0]))) + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail="تلاش‌های ناموفق زیاد است — بعداً دوباره امتحان کنید", + headers={"Retry-After": str(retry_after)}, + ) + + +def record_failure(request: Request) -> None: + _failed.setdefault(client_ip(request), []).append(time.monotonic()) + + +def clear(request: Request) -> None: + """با ورود موفق، تاریخچه تلاش‌های ناموفق IP پاک می‌شود.""" + _failed.pop(client_ip(request), None) diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000..0e5811e --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,588 @@ +from datetime import datetime +from typing import Literal, Optional + +from pydantic import BaseModel, ConfigDict, EmailStr, Field, create_model, field_validator + +# ---------- پایه ---------- + + +class ORMModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + +# ---------- خروجی‌ها ---------- + + +class BrandOut(ORMModel): + id: int + name: str + logo: str + hero: str + contact_bg: str + phone: str + phone_href: str + email: str + address: str + + +class ServiceOut(ORMModel): + id: int + title: str + desc: str + icon: str + + +class ProjectOut(ORMModel): + id: int + slug: str + title: str + location: str + year: str + status: str + image: str + gallery: list[str] + area: str + floors: str + units: str + client: str + summary: str + body: list[str] + + +class ArticleOut(ORMModel): + id: int + slug: str + title: str + date: str + category: str + image: str + read: str + excerpt: str + body: dict + status: str + seo_keyphrase: str + + +class TestimonialOut(ORMModel): + id: int + name: str + role: str + quote: str + + +class FaqOut(ORMModel): + id: int + question: str + answer: str + + +class TeamMemberOut(ORMModel): + id: int + name: str + role: str + bio: str + + +class TimelineItemOut(ORMModel): + id: int + year: str + title: str + desc: str + image: str + + +class StatOut(ORMModel): + id: int + value: int + suffix: str + label: str + + +class WhyUsItemOut(ORMModel): + id: int + title: str + desc: str + + +class BeforeAfterOut(ORMModel): + id: int + title: str + lead: str + before: str + after: str + before_label: str + after_label: str + + +class NavItemOut(ORMModel): + id: int + label: str + href: str + + +class HeroOut(ORMModel): + id: int + kicker: str + title: str + title_highlight: str + subtitle: str + badge_value: str + badge_label: str + cta_primary_label: str + cta_primary_href: str + cta_secondary_label: str + cta_secondary_href: str + scroll_label: str + + +class StoryOut(ORMModel): + id: int + title: str + paragraphs: list[str] + quote: str + quote_author: str + stat1_value: str + stat1_label: str + stat2_value: str + stat2_label: str + link_label: str + link_href: str + bento: list[dict] + + +class SectionHeadingOut(ORMModel): + id: int + key: str + label: str + title: str + lead: str + image: str + cta_label: str + cta_href: str + + +class PageHeroOut(ORMModel): + id: int + key: str + title: str + lead: str + image: str + + +class InvestmentModelOut(ORMModel): + id: int + title: str + desc: str + points: list[str] + + +class ProcessStepOut(ORMModel): + id: int + step: str + title: str + desc: str + + +class OfficeOut(ORMModel): + id: int + city: str + address: str + hours: str + + +class GalleryImageOut(ORMModel): + id: int + src: str + alt: str + + +class StatusLabelOut(ORMModel): + id: int + key: str + label: str + + +class ContactSectionOut(ORMModel): + id: int + label: str + title: str + lead: str + address_label: str + phone_label: str + email_label: str + name_label: str + name_placeholder: str + phone_field_label: str + phone_placeholder: str + note_label: str + note_optional_label: str + note_placeholder: str + submit_label: str + sending_label: str + success_title: str + success_text: str + success_button_label: str + + +class NewsletterOut(ORMModel): + id: int + title: str + lead: str + placeholder: str + button_label: str + sending_label: str + success_text: str + + +class FooterOut(ORMModel): + id: int + about_text: str + pages_heading: str + services_heading: str + contact_heading: str + copyright: str + tagline: str + socials: list[dict] + + +class SeoMetaOut(ORMModel): + id: int + page_key: str + title: str + description: str + + +class ContactMessageOut(ORMModel): + id: int + name: str + phone: str + note: str + is_read: bool + created_at: datetime + + +class SubscriberOut(ORMModel): + id: int + email: str + is_active: bool + created_at: datetime + + +# ---------- ورودی فرم‌ها ---------- + + +class ContactIn(BaseModel): + name: str = Field(min_length=3, max_length=128) + phone: str = Field(pattern=r"^[\d۰-۹+\-\s]{8,15}$") + note: str = Field(default="", max_length=2000) + + +class NewsletterIn(BaseModel): + email: EmailStr + + +# ---------- ورودی‌های پنل مدیریت ---------- + + +class LoginIn(BaseModel): + username: str + password: str + + +class TokenOut(BaseModel): + access_token: str + token_type: str = "bearer" + + +class ChangePasswordIn(BaseModel): + current_password: str + new_password: str = Field(min_length=8, max_length=128) + + +class BrandIn(BaseModel): + name: str = "" + logo: str = "" + hero: str = "" + contact_bg: str = "" + phone: str = "" + phone_href: str = "" + email: str = "" + address: str = "" + + +class ServiceIn(BaseModel): + title: str = Field(min_length=1, max_length=256) + desc: str = "" + icon: str = "draft" + + +class ProjectIn(BaseModel): + slug: str = Field(min_length=1, max_length=128) + title: str = Field(min_length=1, max_length=256) + location: str = "" + year: str = "" + status: Literal["done", "progress"] = "done" + image: str = "" + gallery: list[str] = [] + area: str = "" + floors: str = "" + units: str = "" + client: str = "" + summary: str = "" + body: list[str] = [] + + +EMPTY_DOC = {"type": "doc", "content": [{"type": "paragraph"}]} + + +def validate_doc_body(value: object) -> dict: + """بدنه مقاله باید یک سند TipTap (ProseMirror) معتبر از نظر ساختار باشد.""" + if not value: + return dict(EMPTY_DOC) + if not isinstance(value, dict) or value.get("type") != "doc" or not isinstance(value.get("content"), list): + raise ValueError("body must be a TipTap doc: {type: 'doc', content: [...]}") + return value + + +class ArticleIn(BaseModel): + slug: str = Field(min_length=1, max_length=128) + title: str = Field(min_length=1, max_length=256) + date: str = "" + category: str = "" + image: str = "" + read: str = "" + excerpt: str = "" + body: dict = Field(default_factory=lambda: dict(EMPTY_DOC)) + status: Literal["draft", "published"] = "draft" + seo_keyphrase: str = Field(default="", max_length=128) + _validate_body = field_validator("body")(validate_doc_body) + + +class TestimonialIn(BaseModel): + name: str = Field(min_length=1, max_length=128) + role: str = "" + quote: str = "" + + +class FaqIn(BaseModel): + question: str = Field(min_length=1) + answer: str = "" + + +class TeamMemberIn(BaseModel): + name: str = Field(min_length=1, max_length=128) + role: str = "" + bio: str = "" + + +class TimelineItemIn(BaseModel): + year: str = Field(min_length=1, max_length=32) + title: str = Field(min_length=1, max_length=256) + desc: str = "" + image: str = "" + + +class StatIn(BaseModel): + value: int + suffix: str = "" + label: str = Field(min_length=1, max_length=128) + + +class WhyUsItemIn(BaseModel): + title: str = Field(min_length=1, max_length=256) + desc: str = "" + + +class BeforeAfterIn(BaseModel): + title: str = "" + lead: str = "" + before: str = "" + after: str = "" + before_label: str = "" + after_label: str = "" + + +class NavItemIn(BaseModel): + label: str = Field(min_length=1, max_length=64) + href: str = Field(min_length=1, max_length=128) + + +class HeroIn(BaseModel): + kicker: str = "" + title: str = "" + title_highlight: str = "" + subtitle: str = "" + badge_value: str = "" + badge_label: str = "" + cta_primary_label: str = "" + cta_primary_href: str = "" + cta_secondary_label: str = "" + cta_secondary_href: str = "" + scroll_label: str = "" + + +class StoryIn(BaseModel): + title: str = "" + paragraphs: list[str] = [] + quote: str = "" + quote_author: str = "" + stat1_value: str = "" + stat1_label: str = "" + stat2_value: str = "" + stat2_label: str = "" + link_label: str = "" + link_href: str = "" + bento: list[dict] = [] + + +class SectionHeadingIn(BaseModel): + key: str = Field(min_length=1, max_length=64) + label: str = "" + title: str = "" + lead: str = "" + image: str = "" + cta_label: str = "" + cta_href: str = "" + + +class PageHeroIn(BaseModel): + key: str = Field(min_length=1, max_length=32) + title: str = "" + lead: str = "" + image: str = "" + + +class InvestmentModelIn(BaseModel): + title: str = Field(min_length=1, max_length=256) + desc: str = "" + points: list[str] = [] + + +class ProcessStepIn(BaseModel): + step: str = "" + title: str = Field(min_length=1, max_length=256) + desc: str = "" + + +class OfficeIn(BaseModel): + city: str = Field(min_length=1, max_length=64) + address: str = "" + hours: str = "" + + +class GalleryImageIn(BaseModel): + src: str = "" + alt: str = "" + + +class StatusLabelIn(BaseModel): + key: str = Field(min_length=1, max_length=16) + label: str = Field(min_length=1, max_length=64) + + +class ContactSectionIn(BaseModel): + label: str = "" + title: str = "" + lead: str = "" + address_label: str = "" + phone_label: str = "" + email_label: str = "" + name_label: str = "" + name_placeholder: str = "" + phone_field_label: str = "" + phone_placeholder: str = "" + note_label: str = "" + note_optional_label: str = "" + note_placeholder: str = "" + submit_label: str = "" + sending_label: str = "" + success_title: str = "" + success_text: str = "" + success_button_label: str = "" + + +class NewsletterSectionIn(BaseModel): + title: str = "" + lead: str = "" + placeholder: str = "" + button_label: str = "" + sending_label: str = "" + success_text: str = "" + + +class FooterIn(BaseModel): + about_text: str = "" + pages_heading: str = "" + services_heading: str = "" + contact_heading: str = "" + copyright: str = "" + tagline: str = "" + socials: list[dict] = [] + + +class SeoMetaIn(BaseModel): + page_key: str = Field(min_length=1, max_length=32) + title: str = "" + description: str = "" + + +class SeoMetaPatch(BaseModel): + page_key: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + + +class MessageReadIn(BaseModel): + is_read: bool = True + + +# ---------- اسکیماهای PATCH (ویرایش جزئی — همه فیلدها اختیاری) ---------- + + +def _make_patch(name: str, model_cls: type[BaseModel]) -> type[BaseModel]: + """از اسکیمای ورودی، اسکیمای PATCH می‌سازد؛ فیلدهای ارسال‌نشده دست‌نخورده می‌مانند.""" + fields = {fname: (Optional[f.annotation], None) for fname, f in model_cls.model_fields.items()} + return create_model(name, **fields) + + +BrandPatch = _make_patch("BrandPatch", BrandIn) +ServicePatch = _make_patch("ServicePatch", ServiceIn) +ProjectPatch = _make_patch("ProjectPatch", ProjectIn) +class ArticlePatch(BaseModel): + slug: Optional[str] = None + title: Optional[str] = None + date: Optional[str] = None + category: Optional[str] = None + image: Optional[str] = None + read: Optional[str] = None + excerpt: Optional[str] = None + body: Optional[dict] = None + status: Optional[Literal["draft", "published"]] = None + seo_keyphrase: Optional[str] = None + _validate_body = field_validator("body")(validate_doc_body) +TestimonialPatch = _make_patch("TestimonialPatch", TestimonialIn) +FaqPatch = _make_patch("FaqPatch", FaqIn) +TeamMemberPatch = _make_patch("TeamMemberPatch", TeamMemberIn) +TimelineItemPatch = _make_patch("TimelineItemPatch", TimelineItemIn) +StatPatch = _make_patch("StatPatch", StatIn) +WhyUsItemPatch = _make_patch("WhyUsItemPatch", WhyUsItemIn) +BeforeAfterPatch = _make_patch("BeforeAfterPatch", BeforeAfterIn) +NavItemPatch = _make_patch("NavItemPatch", NavItemIn) +HeroPatch = _make_patch("HeroPatch", HeroIn) +StoryPatch = _make_patch("StoryPatch", StoryIn) +SectionHeadingPatch = _make_patch("SectionHeadingPatch", SectionHeadingIn) +PageHeroPatch = _make_patch("PageHeroPatch", PageHeroIn) +InvestmentModelPatch = _make_patch("InvestmentModelPatch", InvestmentModelIn) +ProcessStepPatch = _make_patch("ProcessStepPatch", ProcessStepIn) +OfficePatch = _make_patch("OfficePatch", OfficeIn) +GalleryImagePatch = _make_patch("GalleryImagePatch", GalleryImageIn) +StatusLabelPatch = _make_patch("StatusLabelPatch", StatusLabelIn) +ContactSectionPatch = _make_patch("ContactSectionPatch", ContactSectionIn) +NewsletterSectionPatch = _make_patch("NewsletterSectionPatch", NewsletterSectionIn) +FooterPatch = _make_patch("FooterPatch", FooterIn) + + +class ReorderIn(BaseModel): + """ترتیب جدید آیتم‌های یک مجموعه بر اساس id.""" + + ids: list[int] diff --git a/backend/app/seed.py b/backend/app/seed.py new file mode 100644 index 0000000..07b091e --- /dev/null +++ b/backend/app/seed.py @@ -0,0 +1,94 @@ +"""اسکریپت seed دیتابیس. + +اجرا: + python -m app.seed # فقط جدول‌های خالی پر می‌شوند + python -m app.seed --force # حذف کامل داده‌ها و ساخت مجدد + +پر کردن هر جدول مستقل است: روی یک دیتابیس موجود، فقط جدول‌های جدید یا خالی +seed می‌شوند و داده‌های قبلی دست‌نخورده می‌مانند. +""" + +import argparse +from typing import Any + +from . import models, seed_data +from .auth import hash_password +from .config import settings +from .database import Base, SessionLocal, engine +from .migrations import paragraphs_to_doc, run_all as run_migrations + +# (مدل، داده‌های اولیه، برچسب گزارش) +CONTENT: list[tuple[Any, list[dict], str]] = [ + (models.Brand, [seed_data.BRAND], "برند"), + (models.Hero, [seed_data.HERO], "هیرو صفحه اصلی"), + (models.StorySection, [seed_data.STORY], "داستان ما"), + (models.SectionHeadingContent, seed_data.SECTION_HEADINGS, "تیتر بخش‌ها"), + (models.PageHeroContent, seed_data.PAGE_HEROES, "هیرو صفحات"), + (models.NavItem, seed_data.NAV_ITEMS, "منوی ناوبری"), + (models.InvestmentModel, seed_data.INVESTMENT_MODELS, "مدل‌های سرمایه‌گذاری"), + (models.ProcessStep, seed_data.PROCESS_STEPS, "مراحل فرآیند کار"), + (models.Office, seed_data.OFFICES, "دفاتر"), + (models.GalleryImage, seed_data.GALLERY_IMAGES, "گالری تصاویر"), + (models.StatusLabel, seed_data.STATUS_LABELS, "برچسب وضعیت پروژه"), + (models.ContactSectionContent, [seed_data.CONTACT_SECTION], "بخش تماس"), + (models.NewsletterSectionContent, [seed_data.NEWSLETTER], "خبرنامه"), + (models.FooterContent, [seed_data.FOOTER], "فوتر"), + (models.Service, seed_data.SERVICES, "خدمات"), + (models.Stat, seed_data.STATS, "آمار"), + (models.Project, seed_data.PROJECTS, "پروژه‌ها"), + (models.Article, seed_data.ARTICLES, "مقالات"), + (models.Testimonial, seed_data.TESTIMONIALS, "نظرات"), + (models.Faq, seed_data.FAQS, "سوالات متداول"), + (models.TeamMember, seed_data.TEAM, "تیم"), + (models.TimelineItem, seed_data.TIMELINE, "تایم‌لاین"), + (models.WhyUsItem, seed_data.WHY, "چرا آرامش"), + (models.BeforeAfter, [seed_data.BEFORE_AFTER], "قبل/بعد"), + (models.SeoMeta, seed_data.SEO_META, "سئوی صفحات"), +] + + +def seed(force: bool = False) -> None: + db = SessionLocal() + try: + if force: + Base.metadata.drop_all(bind=engine) + print("جداول قبلی حذف شدند.") + + Base.metadata.create_all(bind=engine) + run_migrations() + + for model, rows, label in CONTENT: + if db.query(model).count() > 0: + continue + for idx, row in enumerate(rows): + payload = dict(row) + if hasattr(model, "position") and "position" not in payload: + payload["position"] = idx + # بدنه مقالات در seed به‌صورت لیست پاراگراف است — به سند تبدیل می‌شود + if model is models.Article and isinstance(payload.get("body"), list): + payload["body"] = paragraphs_to_doc(payload["body"]) + db.add(model(**payload)) + + if db.query(models.AdminUser).count() == 0: + db.add( + models.AdminUser( + username=settings.admin_username, + password_hash=hash_password(settings.admin_password), + ) + ) + print(f"کاربر ادمین ساخته شد: {settings.admin_username}") + + db.commit() + + for model, _, label in CONTENT: + print(f" {label}: {db.query(model).count()} رکورد") + print("✔ seed با موفقیت انجام شد.") + finally: + db.close() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Seed کردن دیتابیس گروه ساختمانی آرامش") + parser.add_argument("--force", action="store_true", help="حذف همه جداول و ساخت مجدد") + args = parser.parse_args() + seed(force=args.force) diff --git a/backend/app/seed_data.py b/backend/app/seed_data.py new file mode 100644 index 0000000..803bf8d --- /dev/null +++ b/backend/app/seed_data.py @@ -0,0 +1,458 @@ +"""داده‌های اولیه سایت گروه ساختمانی آرامش — برگرفته از فایل src/data/site.ts فرانت‌اند.""" + +BRAND = { + "name": "گروه ساختمانی آرامش", + "logo": "https://i.postimg.cc/XYNrsHsM/Chat-GPT-Image-Aug-6-2026-04-34-46-PM.png", + "hero": "https://i.postimg.cc/J0bKgQVg/Gemini-Generated-Image-mdh969mdh969mdh9.png", + "contact_bg": "https://i.postimg.cc/BbccNQHr/Gemini-Generated-Image-dafc2hdafc2hdafc.png", + "phone": "۰۲۱ ۸۸ ۷۴ ۲۲ ۱۰", + "phone_href": "tel:+982188742210", + "email": "info@arameshbuildinggroup.com", + "address": "تهران، خیابان ولیعصر، برج آرامش، طبقه هجدهم", +} + +IMAGES = [ + "https://i.postimg.cc/wTQ0QFkG/Gemini-Generated-Image-1tjszd1tjszd1tjs.png", + "https://i.postimg.cc/1zfBPS1G/Gemini-Generated-Image-at69yhat69yhat69.png", + "https://i.postimg.cc/BbccNQHr/Gemini-Generated-Image-dafc2hdafc2hdafc.png", + "https://i.postimg.cc/4xFvW0HX/Gemini-Generated-Image-n7mhonn7mhonn7mh.png", + "https://i.postimg.cc/N036cr4s/Gemini-Generated-Image-ytbhvvytbhvvytbh.png", +] + +SERVICES = [ + {"title": "طراحی معماری", "desc": "خلق فرم‌های معاصر با نگاه ایرانی؛ از کانسپت اولیه تا نقشه‌های اجرایی و جزئیات نما.", "icon": "draft"}, + {"title": "ساخت برج‌های مسکونی", "desc": "اجرای برج‌های بلندمرتبه با استانداردهای سازه‌ای بین‌المللی و کیفیت تحویل بی‌نقص.", "icon": "tower"}, + {"title": "مجتمع‌های تجاری", "desc": "طراحی و ساخت مراکز تجاری با مطالعه دقیق جریان مخاطب و بازدهی بلندمدت سرمایه.", "icon": "mall"}, + {"title": "ویلاسازی", "desc": "ویلاهای اختصاصی هم‌نشین با طبیعت؛ محرمانگی، نور طبیعی و مصالح ماندگار.", "icon": "villa"}, + {"title": "بازسازی", "desc": "بازآفرینی ساختمان‌های موجود با ارتقای سازه، نما و تجربه زندگی در فضا.", "icon": "renew"}, + {"title": "مدیریت پروژه", "desc": "کنترل هم‌زمان زمان، هزینه و کیفیت با گزارش‌دهی شفاف در تمام مراحل ساخت.", "icon": "manage"}, + {"title": "سرمایه‌گذاری ساختمانی", "desc": "ساختارهای مشارکت شفاف، ارزیابی ریسک و طراحی سبد ملکی متناسب با سرمایه شما.", "icon": "invest"}, + {"title": "محوطه‌سازی و لنداسکیپ", "desc": "طراحی حیاط، بام سبز و مسیرهای دسترسی؛ ادامه معماری در فضای باز پروژه.", "icon": "landscape"}, +] + +STATS = [ + {"value": 21, "suffix": "+", "label": "سال تجربه ساخت"}, + {"value": 47, "suffix": "", "label": "پروژه تحویل‌شده"}, + {"value": 860, "suffix": "هزار", "label": "مترمربع زیربنا"}, + {"value": 1200, "suffix": "+", "label": "خانواده ساکن"}, +] + +PROJECTS = [ + { + "slug": "aramesh-elahieh", + "title": "برج آرامش الهیه", + "location": "تهران، الهیه", + "year": "۱۴۰۲", + "status": "done", + "image": IMAGES[0], + "gallery": [IMAGES[0], IMAGES[3], IMAGES[1]], + "area": "۲۴٬۰۰۰ متر مربع", + "floors": "۱۸ طبقه", + "units": "۵۶ واحد", + "client": "سرمایه‌گذاری خصوصی", + "summary": "یک برج مسکونی لوکس با نمای سنگ تراورتن و پوسته دوپوسته‌ای که مصرف انرژی را تا ۳۵ درصد کاهش داده است.", + "body": [ + "برج آرامش الهیه روی زمینی با شیب طبیعی شمالی طراحی شد؛ همین شیب فرصت داد تا لابی، استخر و فضاهای مشترک در سه تراز مختلف بنشینند و همه واحدها دید بی‌واسطه به شهر داشته باشند.", + "پوسته بیرونی ترکیبی از سنگ تراورتن روشن و پنجره‌های قدی با شیشه سه‌جداره است. سایه‌بان‌های عمودی مسی‌رنگ، نور مستقیم تابستان را می‌شکنند و در زمستان اجازه ورود کامل نور را می‌دهند.", + "اجرای پروژه در سی‌ودو ماه به پایان رسید و تمام واحدها پیش از تحویل کلید به فروش رسیدند.", + ], + }, + { + "slug": "parseh-mall", + "title": "مجتمع تجاری پارسه", + "location": "اصفهان، مرکز شهر", + "year": "۱۴۰۱", + "status": "done", + "image": IMAGES[1], + "gallery": [IMAGES[1], IMAGES[2], IMAGES[4]], + "area": "۳۸٬۵۰۰ متر مربع", + "floors": "۷ طبقه", + "units": "۱۴۰ واحد تجاری", + "client": "گروه سرمایه‌گذاری پارسه", + "summary": "مرکز تجاری با آتریوم مرکزی و نورگیر سقفی که جریان مخاطب را به‌صورت طبیعی در همه ترازها توزیع می‌کند.", + "body": [ + "مطالعه رفتار مخاطب پیش از طراحی نشان داد کوتاه‌ترین مسیر عمودی، کلید موفقیت تجاری این مجموعه است؛ بنابراین آتریوم مرکزی با پله‌برقی‌های قطری در قلب پلان قرار گرفت.", + "نورگیر سقفی با شیشه لو-ای، روشنایی روز را تا تراز منفی دو می‌رساند و مصرف روشنایی مصنوعی را به‌طور محسوس کاهش می‌دهد.", + "مجموعه دو ماه پیش از موعد قراردادی به بهره‌برداری رسید.", + ], + }, + { + "slug": "daryakenar-villas", + "title": "ویلاهای دریاکنار", + "location": "مازندران، نوشهر", + "year": "۱۴۰۰", + "status": "done", + "image": IMAGES[2], + "gallery": [IMAGES[2], IMAGES[4], IMAGES[0]], + "area": "۹٬۲۰۰ متر مربع", + "floors": "۲ طبقه", + "units": "۱۲ ویلا", + "client": "مجموعه خصوصی دریاکنار", + "summary": "دوازده ویلای اختصاصی با چیدمان شانه‌ای که برای هر واحد، دید مستقیم به دریا و محرمانگی کامل فراهم می‌کند.", + "body": [ + "چیدمان شانه‌ای ویلاها نتیجه ده‌ها بررسی زاویه دید بود؛ هیچ ویلایی در دید ویلای دیگر نیست و همه به دریا باز می‌شوند.", + "مصالح، پاسخی به اقلیم مرطوب شمال است: چوب ترموود، بتن اکسپوز و پوشش‌های ضد نمک برای فلزات نما.", + "محوطه با گیاهان بومی طراحی شد تا نیاز آبیاری در طول سال حداقل باشد.", + ], + }, + { + "slug": "niavaran-twins", + "title": "برج دوقلوی نیاوران", + "location": "تهران، نیاوران", + "year": "۱۳۹۹", + "status": "done", + "image": IMAGES[3], + "gallery": [IMAGES[3], IMAGES[0], IMAGES[2]], + "area": "۳۱٬۷۰۰ متر مربع", + "floors": "۲۰ طبقه", + "units": "۸۰ واحد", + "client": "مشارکت با مالکان زمین", + "summary": "دو برج همسان با پادست مشترک که فضاهای رفاهی و پارکینگ‌ها را در یک تراز واحد ادغام می‌کند.", + "body": [ + "دو برج روی یک پادست مشترک نشسته‌اند؛ این تصمیم اجازه داد فضاهای رفاهی، سالن اجتماعات و پارکینگ‌ها بین دو ساختمان مشترک شوند و سطح اشغال باغ کاهش یابد.", + "سازه با سیستم دیوار برشی و دال پس‌کشیده اجرا شد تا دهانه‌های داخلی آزاد و پلان‌ها قابل شخصی‌سازی باشند.", + ], + }, + { + "slug": "kish-garden-tower", + "title": "باغ‌برج کیش", + "location": "کیش، مروارید", + "year": "۱۴۰۵ (پیش‌بینی)", + "status": "progress", + "image": IMAGES[4], + "gallery": [IMAGES[4], IMAGES[1], IMAGES[3]], + "area": "۱۸٬۴۰۰ متر مربع", + "floors": "۱۴ طبقه", + "units": "۶۲ واحد اقامتی", + "client": "صندوق پروژه‌محور آرامش", + "summary": "باغ‌برجی اقامتی با تراس‌های سبز پیوسته که در حال ساخت است؛ پیشرفت فیزیکی پروژه ۴۸ درصد.", + "body": [ + "تراس‌های سبز پیوسته، پوسته‌ای زنده برای ساختمان می‌سازند که در اقلیم گرم جزیره، دمای سطح نما را به‌طور محسوس پایین می‌آورد.", + "در حال حاضر سازه تا تراز طبقه نهم اجرا شده و نصب نما از ضلع جنوبی آغاز شده است.", + ], + }, + { + "slug": "aramesh-business-center", + "title": "مرکز تجاری آرامش تهران", + "location": "تهران، سعادت‌آباد", + "year": "۱۴۰۶ (پیش‌بینی)", + "status": "progress", + "image": IMAGES[1], + "gallery": [IMAGES[1], IMAGES[3], IMAGES[4]], + "area": "۲۷٬۳۰۰ متر مربع", + "floors": "۱۲ طبقه", + "units": "۹۰ دفتر اداری", + "client": "مشارکت با هلدینگ آرامش", + "summary": "مجتمع اداری-تجاری با هسته مرکزی شفاف و لابی دوبلکس؛ در مرحله اجرای سفت‌کاری.", + "body": [ + "برنامه‌ریزی فضایی این مجموعه بر اساس دفاتر انعطاف‌پذیر انجام شد؛ هر تراز می‌تواند به دو، سه یا شش دفتر مستقل تقسیم شود.", + "اجرای تأسیسات مکانیکی به‌صورت هم‌زمان با سفت‌کاری پیش می‌رود تا زمان کل پروژه کوتاه‌تر شود.", + ], + }, +] + +BEFORE_AFTER = { + "title": "از کانسپت تا خلق واقعیت", + "lead": "دستگیره را بکشید و ببینید نمای ساختمانی چگونه از حالت اولیه به فرم مدرن تبدیل می‌شود.", + "before": "https://cdn.housedesign.ai/se/housedesign/assets/housedesign-home/compare/1_before.webp", + "after": "https://cdn.housedesign.ai/se/housedesign/assets/housedesign-home/compare/1_after.webp", + "before_label": "کانسپت اولیه", + "after_label": "پروژه تکمیل‌شده", +} + +TIMELINE = [ + {"year": "۱۳۸۳", "title": "آغاز مسیر", "desc": "تأسیس دفتر معماری آرامش با تیمی چهارنفره و نخستین پروژه بازسازی در تهران.", "image": IMAGES[2]}, + {"year": "۱۳۸۵", "title": "نخستین ساخت مستقل", "desc": "تحویل اولین ساختمان مسکونی هفت‌طبقه؛ آغاز همکاری با پیمانکاران سازه‌ای بزرگ.", "image": IMAGES[0]}, + {"year": "۱۳۸۷", "title": "ورود به بازار تجاری", "desc": "طراحی و اجرای اولین مجتمع تجاری با رویکرد بازدهی سرمایه و جریان مخاطب.", "image": IMAGES[1]}, + {"year": "۱۳۸۹", "title": "استاندارد بین‌المللی", "desc": "استقرار نظام کنترل کیفیت و دریافت گواهی‌نامه‌های مدیریت پروژه.", "image": IMAGES[4]}, + {"year": "۱۳۹۵", "title": "بلندمرتبه‌سازی", "desc": "آغاز ساخت نخستین برج بیست‌طبقه شرکت در شمال تهران.", "image": IMAGES[3]}, + {"year": "۱۴۰۳", "title": "گروه ساختمانی آرامش امروز", "desc": "پرتفوی ۴۷ پروژه، دفاتر در سه شهر و صندوق سرمایه‌گذاری ساختمانی اختصاصی.", "image": IMAGES[0]}, +] + +WHY = [ + {"title": "کیفیت بدون مصالحه", "desc": "انتخاب مصالح و مجریان بر اساس ماندگاری چنددهه‌ای، نه سریع‌ترین تحویل."}, + {"title": "شفافیت مالی کامل", "desc": "گزارش ماهانه هزینه‌ها و پیشرفت فیزیکی برای همه شرکای پروژه."}, + {"title": "تحویل به‌موقع", "desc": "۹۴٪ پروژه‌های ما در بازه زمانی قراردادی یا زودتر تحویل داده شده‌اند."}, + {"title": "طراحی اختصاصی", "desc": "هیچ نقشه‌ای دو بار اجرا نمی‌شود؛ هر پروژه هویت معماری خودش را دارد."}, + {"title": "خدمات پس از تحویل", "desc": "پنج سال پشتیبانی فنی و نگهداری تأسیسات پس از تحویل کلید."}, + {"title": "پایداری و انرژی", "desc": "طراحی پوسته و تأسیسات با هدف کاهش مصرف انرژی تا ۳۵ درصد."}, +] + +TESTIMONIALS = [ + {"name": "مهندس سارا کیانی", "role": "سرمایه‌گذار پروژه پارسه", "quote": "گزارش‌های ماهانه گروه ساختمانی آرامش آن‌قدر دقیق بود که هیچ‌وقت نیاز نشد نگران سرمایه‌ام باشم. پروژه دو ماه زودتر از موعد به بهره‌برداری رسید."}, + {"name": "دکتر امیرحسین راد", "role": "ساکن برج الهیه", "quote": "کیفیت اجرا در جزئیاتی دیده می‌شود که معمولاً کسی به آن‌ها فکر نمی‌کند؛ از عایق صوتی پنجره‌ها تا نور غیرمستقیم راهروها."}, + {"name": "مریم دلاور", "role": "مدیر مجموعه ویلاهای دریاکنار", "quote": "همکاری با تیم طراحی، تجربه‌ای واقعاً حرفه‌ای بود. ایده‌های ما را شنیدند و به زبان معماری ترجمه کردند."}, +] + +TEAM = [ + {"name": "کامران آرامش", "role": "بنیان‌گذار و مدیرعامل", "bio": "بیش از سه دهه تجربه در ساخت بلندمرتبه."}, + {"name": "نگار شریفی", "role": "مدیر ارشد طراحی", "bio": "معمار ارشد، فارغ‌التحصیل دانشگاه تهران و میلان."}, + {"name": "بهزاد مرادی", "role": "مدیر اجرایی پروژه‌ها", "bio": "مسئول کنترل کیفیت و زمان‌بندی همه کارگاه‌ها."}, + {"name": "الهام نیک‌آیین", "role": "مدیر سرمایه‌گذاری", "bio": "طراح ساختارهای مشارکت و تحلیل بازدهی ملکی."}, +] + +ARTICLES = [ + { + "slug": "natural-light", + "title": "معماری آرام: چرا نور طبیعی گران‌ترین مصالح ساختمان است؟", + "date": "۲۴ مرداد ۱۴۰۳", + "category": "معماری", + "image": IMAGES[1], + "read": "۶ دقیقه", + "excerpt": "نور طبیعی نه‌تنها کیفیت زندگی در فضا را تعیین می‌کند، بلکه مستقیماً بر مصرف انرژی و ارزش بلندمدت ملک اثر می‌گذارد.", + "body": [ + "در بازار ساخت‌وساز، ارزش یک واحد اغلب با متراژ سنجیده می‌شود؛ اما تجربه ما در بیست سال گذشته نشان داده کیفیت نور، تعیین‌کننده‌تر از چند متر اضافه است.", + "جهت‌گیری ساختمان، عمق دهانه و ارتفاع کف تا سقف، سه متغیر اصلی نور طبیعی هستند. تصمیم درباره هر سه، پیش از ترسیم اولین پلان گرفته می‌شود و بعد از آن تقریباً برگشت‌پذیر نیست.", + "در برج آرامش الهیه، چرخش ۱۲ درجه‌ای توده ساختمان نسبت به شمال، دو ساعت نور مفید بیشتر در روزهای زمستان به واحدها اضافه کرد؛ تصمیمی بی‌هزینه با اثری چنددهه‌ای.", + "برای انتخاب یک واحد، پیشنهاد ما ساده است: بازدید را در ساعت‌های میانی صبح و اواخر بعدازظهر انجام دهید و کیفیت نور را در همان ساعت‌هایی بسنجید که واقعاً در خانه هستید.", + ], + }, + { + "slug": "real-estate-portfolio", + "title": "راهنمای انتخاب سبد سرمایه‌گذاری ملکی در بازار امروز", + "date": "۱۰ مرداد ۱۴۰۳", + "category": "سرمایه‌گذاری", + "image": IMAGES[3], + "read": "۹ دقیقه", + "excerpt": "سه معیار کلیدی برای تشکیل سبدی متعادل از دارایی ملکی، بر اساس افق زمانی و سطح ریسک شما.", + "body": [ + "سبد ملکی متعادل، ترکیبی از دارایی‌های با نقدشوندگی متفاوت است؛ نه همه سرمایه در یک پروژه بلندمدت، نه همه در واحدهای آماده.", + "افق زمانی، نخستین معیار است. اگر برنامه شما کمتر از دو سال است، پیش‌خرید در پروژه‌ای که تازه شروع شده انتخاب پرریسکی خواهد بود.", + "معیار دوم شفافیت گزارش‌دهی است. بدون گزارش ماهانه پیشرفت فیزیکی و مالی، ارزیابی وضعیت سرمایه شما عملاً غیرممکن است.", + "معیار سوم، تنوع کاربری است. ترکیب مسکونی و تجاری، نوسان بازدهی سبد را در دوره‌های رکود محسوس کاهش می‌دهد.", + ], + }, + { + "slug": "facade-quality", + "title": "پنج نکته فنی که کیفیت نما را پس از ده سال مشخص می‌کند", + "date": "۲۸ تیر ۱۴۰۳", + "category": "اجرا", + "image": IMAGES[4], + "read": "۵ دقیقه", + "excerpt": "نمای ساختمان در سال اول زیبا به نظر می‌رسد؛ تفاوت کیفیت اجرا در سال دهم آشکار می‌شود.", + "body": [ + "اتصالات مکانیکی سنگ نما، مهم‌تر از خود سنگ است. اتصال چسبی در اقلیم سردوگرم ایران عمر کوتاهی دارد.", + "درزهای انقباضی باید در نقشه دیده شوند، نه در کارگاه تصمیم‌گیری شوند؛ نبودشان اولین دلیل ترک‌های افقی نما است.", + "آب‌بندی دور پنجره، پرتکرارترین نقطه شکست است. اجرای نوار آب‌بند پیش از نصب قرنیز، این ریسک را حذف می‌کند.", + "زیرسازی گالوانیزه گرم، در برابر زیرسازی رنگ‌شده، تفاوت قیمت اندکی دارد و تفاوت عمر بسیار زیادی.", + "در پایان، برنامه بازدید دوره‌ای سالانه نما را جدی بگیرید؛ ترمیم یک درز، هزینه‌ای در حد هیچ در برابر بازسازی کل نما است.", + ], + }, + { + "slug": "commercial-yield", + "title": "چگونه بازدهی یک پروژه تجاری را پیش از ساخت تخمین بزنیم؟", + "date": "۱۵ تیر ۱۴۰۳", + "category": "سرمایه‌گذاری", + "image": IMAGES[2], + "read": "۷ دقیقه", + "excerpt": "مدل ساده‌ای برای برآورد بازدهی، بر پایه جریان مخاطب، ترکیب واحدها و نرخ اجاره منطقه.", + "body": [ + "برآورد بازدهی تجاری با سه عدد آغاز می‌شود: جریان روزانه مخاطب، نرخ اجاره هر مترمربع در منطقه و نسبت سطح مفید به کل.", + "خطای رایج، نادیده‌گرفتن ترازهای بالاتر است. بدون طراحی درست مسیر عمودی، ترازهای سه به بالا معمولاً نیمی از نرخ تراز همکف را هم به‌دست نمی‌آورند.", + "در نهایت، سناریوی محافظه‌کارانه را هم بنویسید؛ اگر پروژه در سناریوی بدبینانه هم توجیه داشت، سرمایه‌گذاری منطقی است.", + ], + }, + { + "slug": "facade-materials", + "title": "مصالح نما: مقایسه سنگ، کامپوزیت و ترموود در اقلیم ایران", + "date": "۲ تیر ۱۴۰۳", + "category": "اجرا", + "image": IMAGES[0], + "read": "۸ دقیقه", + "excerpt": "هر مصالح نما در اقلیم متفاوتی درست کار می‌کند؛ انتخاب اشتباه، هزینه نگهداری را چند برابر می‌کند.", + "body": [ + "سنگ در اقلیم خشک، بهترین ماندگاری را دارد؛ اما وزن آن نیازمند زیرسازی دقیق و محاسبه سازه‌ای است.", + "کامپوزیت انتخاب سریع و سبکی است، مشروط بر انتخاب گرید ضد آتش و درنظرگرفتن انقباض حرارتی در طول ورق.", + "ترموود در اقلیم مرطوب شمال، بهترین پاسخ زیبایی‌شناسانه است، ولی برنامه نگهداری دوسالانه دارد.", + ], + }, + { + "slug": "renovate-or-rebuild", + "title": "بازسازی یا نوسازی؟ معیارهای تصمیم‌گیری برای مالکان", + "date": "۲۰ خرداد ۱۴۰۳", + "category": "معماری", + "image": IMAGES[1], + "read": "۶ دقیقه", + "excerpt": "اگر عمر سازه و ضوابط شهری اجازه دهند، بازسازی می‌تواند بازدهی بالاتری از تخریب کامل داشته باشد.", + "body": [ + "نخست وضعیت سازه ارزیابی می‌شود؛ اگر مقاوم‌سازی هزینه‌ای بیش از ۴۰ درصد ساخت نو دارد، نوسازی منطقی‌تر است.", + "دومین معیار، ضوابط شهری روز است. گاهی تخریب، سطح زیربنای مجاز کمتری به شما می‌دهد و این تصمیم را کاملاً تغییر می‌دهد.", + "در بازسازی، بیشترین ارزش افزوده از سه جا می‌آید: نما، ورودی و بازآرایی پلان داخلی.", + ], + }, +] + +FAQS = [ + {"question": "حداقل سرمایه برای مشارکت در پروژه‌های گروه ساختمانی آرامش چقدر است؟", "answer": "ساختار مشارکت هر پروژه متفاوت است، اما معمولاً واحدهای سرمایه‌گذاری از سطح مشخصی آغاز می‌شوند و جزئیات دقیق در جلسه ارزیابی اولیه ارائه می‌شود."}, + {"question": "مدت زمان معمول ساخت یک برج مسکونی چقدر است؟", "answer": "بسته به تعداد طبقات و شرایط زمین، میانگین اجرای یک برج مسکونی بین ۲۸ تا ۴۲ ماه از زمان صدور پروانه تا تحویل کلید است."}, + {"question": "آیا امکان تغییر نقشه داخلی واحد وجود دارد؟", "answer": "بله. تا پیش از پایان مرحله سفت‌کاری، امکان شخصی‌سازی پلان داخلی با هماهنگی تیم طراحی و تأیید سازه فراهم است."}, + {"question": "خدمات پس از تحویل شامل چه مواردی می‌شود؟", "answer": "پنج سال پشتیبانی فنی تأسیسات مکانیکی و برقی، بازدید دوره‌ای سالانه و تضمین اجرای نما و عایق‌کاری."}, + {"question": "پروژه‌ها در چه شهرهایی اجرا می‌شوند؟", "answer": "در حال حاضر تهران، اصفهان و سواحل شمالی کشور مناطق اصلی فعالیت ما هستند و پروژه‌ای نیز در جزیره کیش در دست ساخت است."}, +] + +# ---------- محتوای قبلاً هاردکد‌شده در فرانت‌اند (الان در دیتابیس) ---------- + +NAV_ITEMS = [ + {"label": "صفحه اصلی", "href": "/"}, + {"label": "درباره ما", "href": "/about"}, + {"label": "خدمات", "href": "/services"}, + {"label": "پروژه‌ها", "href": "/projects"}, + {"label": "سرمایه‌گذاری", "href": "/investment"}, + {"label": "وبلاگ", "href": "/blog"}, + {"label": "سوالات متداول", "href": "/faq"}, + {"label": "تماس با ما", "href": "/contact"}, +] + +HERO = { + "kicker": "از سال ۱۳۸۳ — تهران، اصفهان، سواحل شمال", + "title": "ساختمانی که آرامش را معماری می‌کند", + "title_highlight": "معماری", + "subtitle": "طراحی، ساخت و سرمایه‌گذاری در پروژه‌های مسکونی و تجاری لوکس؛ با استانداردی که در جزئیات دیده می‌شود و در سال‌ها دوام می‌آورد.", + "badge_value": "۴۷", + "badge_label": "پروژه تحویل‌شده در بیست‌ویک سال فعالیت حرفه‌ای", + "cta_primary_label": "مشاهده پروژه‌ها", + "cta_primary_href": "/projects", + "cta_secondary_label": "مشاوره سرمایه‌گذاری", + "cta_secondary_href": "/contact", + "scroll_label": "اسکرول کنید", +} + +STORY = { + "title": "درباره ی ما", + "paragraphs": [ + "گروه ساختمانی آرامش با یک دفتر کوچک معماری آغاز شد و امروز یکی از مجموعه‌های شناخته‌شده ساخت‌وساز لوکس در ایران است. در تمام این سال‌ها یک اصل تغییر نکرده است: ساختمان باید برای زندگی ساخته شود، نه برای فروش سریع.", + "هر پروژه با مطالعه زمین، نور، اقلیم و الگوی زندگی ساکنان آغاز می‌شود؛ سپس تیم طراحی، سازه و اجرا در یک فرآیند واحد آن را تا تحویل کلید پیش می‌برند.", + ], + "quote": "«ساختمان برای ما فقط آجر و بتن نیست؛ بلکه تعهدی است به کیفیت زندگی ساکنان. ما فضاهایی می‌سازیم که در گذر زمان ارزش و هویت خود را حفظ کنند.»", + "quote_author": "کامران آرامش — مدیرعامل شرکت ساختمانی آرامش", + "stat1_value": "۹۴٪", + "stat1_label": "تحویل به‌موقع پروژه‌ها", + "stat2_value": "۵ سال", + "stat2_label": "پشتیبانی پس از تحویل", + "link_label": "داستان کامل ما", + "link_href": "/about", + "bento": [ + {"src": IMAGES[0], "alt": "نمای برج مسکونی آرامش در تهران"}, + {"src": IMAGES[1], "alt": "آتریوم مرکزی مجتمع تجاری پارسه"}, + {"src": IMAGES[2], "alt": "ویلاهای ساخته‌شده در سواحل شمال"}, + {"src": IMAGES[3], "alt": "نمای شبانه برج دوقلوی نیاوران"}, + ], +} + +SECTION_HEADINGS = [ + {"key": "services", "title": "خدمات", "lead": "از نخستین خط روی کاغذ تا تحویل کلید، مدیریت سرمایه و طراحی محوطه؛ همه مراحل در یک مجموعه و زیر یک نظام کنترل کیفیت."}, + {"key": "featured_projects", "title": "پروژه‌های شاخص", "lead": "کارهایی که بیشترین چیز را درباره استاندارد ما می‌گویند؛ چه آن‌هایی که تحویل شده‌اند و چه آن‌هایی که همین امروز در حال ساخت‌اند.", "cta_label": "همه پروژه‌ها", "cta_href": "/projects"}, + {"key": "gallery", "title": "گالری تصاویر", "lead": "قاب‌هایی از کارگاه‌ها و پروژه‌های تحویل‌شده گروه ساختمانی آرامش."}, + {"key": "timeline", "title": "مسیر ما", "lead": "از یک دفتر چهارنفره تا ۴۷ پروژه. خط زمان پروژه‌های ما را ببینید."}, + {"key": "why_us", "title": "چرا گروه ساختمانی آرامش", "lead": "شش دلیلی که کارفرمایان و سرمایه‌گذاران بار دوم هم به ما برمی‌گردند.", "image": IMAGES[3]}, + {"key": "testimonials", "title": "نظرات مشتریان"}, + {"key": "team", "title": "تیم ما", "lead": "کسانی که تصمیم‌های اصلی هر پروژه را می‌گیرند و پاسخ‌گوی آن هستند."}, + {"key": "articles", "label": "وبلاگ", "title": "آخرین مقالات", "lead": "یادداشت‌های تیم معماری، اجرا و سرمایه‌گذاری گروه ساختمانی آرامش درباره آنچه در کارگاه و بازار می‌گذرد.", "cta_label": "همه مقالات", "cta_href": "/blog"}, + {"key": "faq", "title": "سوالات متداول", "lead": "پاسخ کوتاه به پرتکرارترین پرسش‌های سرمایه‌گذاران و خریداران."}, + {"key": "investment_models", "title": "مدل‌های همکاری", "lead": "بسته به میزان سرمایه، افق زمانی و سطح ریسکی که می‌پذیرید، یکی از این سه مسیر مناسب شماست."}, + {"key": "process", "title": "فرآیند کار ما", "lead": "چهار مرحله شفاف، از نخستین جلسه تا تحویل کلید."}, +] + +PAGE_HEROES = [ + {"key": "about", "title": "درباره ما", "lead": "گروه ساختمانی آرامش مجموعه‌ای از معماران، مهندسان سازه و مدیران پروژه است که از سال ۱۳۸۳ در حوزه ساخت‌وساز لوکس فعالیت می‌کند.", "image": IMAGES[3]}, + {"key": "services", "title": "خدمات", "lead": "هشت حوزه تخصصی که چرخه کامل یک پروژه ساختمانی را از ایده تا بهره‌برداری پوشش می‌دهند.", "image": IMAGES[1]}, + {"key": "projects", "title": "پروژه‌ها", "lead": "مجموعه‌ای از برج‌های مسکونی، مراکز تجاری و ویلاهای اختصاصی؛ از پروژه‌های تحویل‌شده تا کارگاه‌های فعال امروز.", "image": IMAGES[0]}, + {"key": "investment", "title": "سرمایه‌گذاری", "lead": "سه مسیر روشن برای ورود به بازار ساخت‌وساز لوکس، با ساختار مالی شفاف و گزارش‌دهی منظم.", "image": IMAGES[4]}, + {"key": "blog", "title": "وبلاگ", "lead": "یادداشت‌های تیم معماری، اجرا و سرمایه‌گذاری گروه ساختمانی آرامش درباره آنچه در کارگاه و بازار می‌گذرد.", "image": IMAGES[2]}, + {"key": "faq", "title": "سوالات متداول", "lead": "پیش از تماس، شاید پاسخ پرسش شما همین‌جا باشد. اگر نبود، خوشحال می‌شویم مستقیم صحبت کنیم.", "image": IMAGES[1]}, + {"key": "contact", "title": "تماس با ما", "lead": "برای مشاوره ساخت، سرمایه‌گذاری یا بازدید از پروژه‌ها، تیم ما در سه شهر آماده پاسخ‌گویی است.", "image": IMAGES[1]}, +] + +INVESTMENT_MODELS = [ + { + "title": "مشارکت در ساخت", + "desc": "مالک زمین و گروه ساختمانی آرامش در سود پروژه شریک می‌شوند؛ اجرا، مجوزها و فروش بر عهده ماست.", + "points": ["ارزیابی رایگان زمین", "قرارداد شفاف با ضمانت اجرا", "گزارش ماهانه پیشرفت"], + }, + { + "title": "صندوق پروژه‌محور", + "desc": "سرمایه‌گذاری روی یک پروژه مشخص با واحدهای قابل خرید و بازدهی محاسبه‌شده در پایان دوره.", + "points": ["حداقل دوره ۲۴ ماه", "گزارش ارزش‌گذاری فصلی", "امکان انتقال واحد سرمایه"], + }, + { + "title": "پیش‌خرید واحد", + "desc": "خرید واحد مسکونی یا تجاری در مراحل ابتدایی ساخت با قیمت پیش از تکمیل و پرداخت مرحله‌ای.", + "points": ["پرداخت مرحله‌ای متناسب با پیشرفت", "امکان شخصی‌سازی پلان", "تضمین زمان تحویل"], + }, +] + +PROCESS_STEPS = [ + {"step": "۰۱", "title": "مطالعه و امکان‌سنجی", "desc": "بررسی زمین، ضوابط شهری و تحلیل بازدهی اقتصادی پروژه."}, + {"step": "۰۲", "title": "طراحی و فاز اجرایی", "desc": "کانسپت معماری، طراحی سازه و تأسیسات و تهیه نقشه‌های اجرایی."}, + {"step": "۰۳", "title": "اجرا و کنترل کیفیت", "desc": "ساخت با نظارت مستمر، گزارش ماهانه پیشرفت و کنترل هزینه."}, + {"step": "۰۴", "title": "تحویل و پشتیبانی", "desc": "تحویل کلید، آموزش بهره‌برداری و پنج سال پشتیبانی فنی."}, +] + +OFFICES = [ + {"city": "تهران", "address": "خیابان ولیعصر، برج آرامش، طبقه هجدهم", "hours": "شنبه تا چهارشنبه، ۹ تا ۱۸"}, + {"city": "اصفهان", "address": "خیابان چهارباغ بالا، مجتمع پارسه، طبقه ششم", "hours": "شنبه تا چهارشنبه، ۹ تا ۱۷"}, + {"city": "نوشهر", "address": "بلوار دریا، دفتر پروژه دریاکنار", "hours": "شنبه تا پنجشنبه، ۱۰ تا ۱۶"}, +] + +GALLERY_IMAGES = [ + {"src": IMAGES[0], "alt": "تصویری از پروژه‌های گروه ساختمانی آرامش، شماره ۱"}, + {"src": IMAGES[1], "alt": "تصویری از پروژه‌های گروه ساختمانی آرامش، شماره ۲"}, + {"src": IMAGES[2], "alt": "تصویری از پروژه‌های گروه ساختمانی آرامش، شماره ۳"}, + {"src": IMAGES[3], "alt": "تصویری از پروژه‌های گروه ساختمانی آرامش، شماره ۴"}, + {"src": IMAGES[4], "alt": "تصویری از پروژه‌های گروه ساختمانی آرامش، شماره ۵"}, + {"src": IMAGES[1], "alt": "تصویری از پروژه‌های گروه ساختمانی آرامش، شماره ۶"}, +] + +STATUS_LABELS = [ + {"key": "done", "label": "انجام شده"}, + {"key": "progress", "label": "در حال انجام"}, +] + +CONTACT_SECTION = { + "label": "تماس با ما", + "title": "پروژه‌تان را با ما در میان بگذارید", + "lead": "شماره‌تان را بگذارید؛ کارشناس ما در کمتر از یک روز کاری برای یک جلسه مشاوره رایگان تماس می‌گیرد.", + "address_label": "دفتر مرکزی", + "phone_label": "تلفن", + "email_label": "ایمیل", + "name_label": "نام", + "name_placeholder": "نام و نام خانوادگی", + "phone_field_label": "شماره تلفن", + "phone_placeholder": "09xx xxx xxxx", + "note_label": "توضیحات", + "note_optional_label": "(اختیاری)", + "note_placeholder": "درباره پروژه یا درخواست خود بنویسید…", + "submit_label": "ارسال درخواست مشاوره", + "sending_label": "در حال ارسال…", + "success_title": "درخواست شما ثبت شد", + "success_text": "از اعتماد شما سپاسگزاریم. کارشناسان ما به‌زودی با شما تماس می‌گیرند.", + "success_button_label": "ثبت درخواست جدید", +} + +NEWSLETTER = { + "title": "خبرنامه گروه ساختمانی آرامش", + "lead": "هر ماه یک ایمیل: پروژه‌های جدید، فرصت‌های سرمایه‌گذاری و یادداشت‌های تیم معماری.", + "placeholder": "نشانی ایمیل شما", + "button_label": "عضویت", + "sending_label": "در حال ارسال…", + "success_text": "عضویت شما ثبت شد.", +} + +FOOTER = { + "about_text": "گروه ساختمانی آرامش از سال ۱۳۸۳ ساختمان‌هایی می‌سازد که قرار است چند نسل در آن‌ها زندگی کنند؛ معماری آرام، اجرای دقیق و سرمایه‌گذاری شفاف.", + "pages_heading": "صفحات", + "services_heading": "خدمات", + "contact_heading": "ارتباط با ما", + "copyright": "© ۱۴۰۳ گروه ساختمانی آرامش — تمامی حقوق محفوظ است.", + "tagline": "طراحی و ساخت با دقتِ معماری", + "socials": [ + {"label": "اینستاگرام", "href": "#", "icon": "instagram"}, + {"label": "لینکدین", "href": "#", "icon": "linkedin"}, + {"label": "تلگرام", "href": "#", "icon": "telegram"}, + ], +} + +SEO_META = [ + {"page_key": key, "title": "", "description": ""} + for key in ["home", "about", "services", "projects", "investment", "blog", "faq", "contact"] +] diff --git a/backend/app/seo_pages.py b/backend/app/seo_pages.py new file mode 100644 index 0000000..6df8cea --- /dev/null +++ b/backend/app/seo_pages.py @@ -0,0 +1,239 @@ +"""سئوی سمت سرور — robots.txt، sitemap.xml و سرو HTML صفحات وبلاگ/پروژه با متای تزریق‌شده. + +فرانت‌اند یک SPA است؛ خزنده‌های اجتماعی (تلگرام، واتس‌اپ و...) جاوااسکریپت اجرا نمی‌کنند. +این ماژول همان index.html بیلد فرانت‌اند را می‌خواند و پیش از ارسال، تگ‌های +title / description / OG / canonical / JSON-LD را از دیتابیس داخل آن تزریق می‌کند — +بازدیدکننده‌های عادی همان SPA همیشگی را می‌بینند و خزنده‌ها متای درست را می‌خوانند. +""" + +import html +import json +import re +from pathlib import Path + +from fastapi import APIRouter, Depends, Response +from fastapi.responses import HTMLResponse +from sqlalchemy.orm import Session + +from . import models +from .config import settings +from .database import get_db + +router = APIRouter() + +STATIC_PATHS = ["/", "/about", "/services", "/projects", "/investment", "/blog", "/faq", "/contact"] + + +def _absolute(path: str) -> str: + if not path: + return "" + if path.startswith(("http://", "https://")): + return path + site = settings.site_url.rstrip("/") + return f"{site}{path}" if site else path + + +def _read_index() -> str | None: + try: + return (Path(settings.frontend_dist) / "index.html").read_text(encoding="utf-8") + except OSError: + return None + + +def _doc_text(doc: object, limit: int = 300) -> str: + """متن ساده سند TipTap — برای noscript و پیش‌نمایش.""" + words: list[str] = [] + + def walk(node: object) -> None: + if not isinstance(node, dict): + return + if node.get("type") == "text" and isinstance(node.get("text"), str): + words.append(node["text"]) + for child in node.get("content") or []: + walk(child) + + if isinstance(doc, dict): + for block in doc.get("content") or []: + walk(block) + text = " ".join(words).strip() + return text[:limit] + ("…" if len(text) > limit else "") + + +def _shell( + *, + title: str, + description: str, + path: str, + og_image: str = "", + og_type: str = "website", + jsonld: dict | None = None, + noscript: str = "", +) -> HTMLResponse: + """index.html فرانت‌اند با متای تزریق‌شده؛ اگر بیلد در دسترس نبود، صفحه حداقلی.""" + title = title.strip() or "گروه ساختمانی آرامش" + description = description.strip() + canonical = _absolute(path) + image = _absolute(og_image) + esc = html.escape + + # تگ title جای تگ موجود index.html را می‌گیرد و در بلوک تکرار نمی‌شود + tags = [ + f'', + f'', + f'', + f'', + f'', + f'', + ] + if image: + tags.append(f'') + if jsonld: + payload = json.dumps(jsonld, ensure_ascii=False).replace("{payload}') + block = "\n ".join(tags) + + raw = _read_index() + if raw is None: + # بیلد فرانت‌اند سوار نشده — صفحه حداقلی با خودِ متا تا خزنده‌ها دست خالی نمانند + body = f"

{esc(title)}

{esc(description)}

" + return HTMLResponse( + f'' + f"{esc(title)}{block}{body}" + ) + + raw = re.sub(r".*?", f"{esc(title)}", raw, count=1, flags=re.S) + raw = raw.replace("", f" {block}\n ", 1) + if noscript: + raw = raw.replace("", f"\n ", 1) + return HTMLResponse(raw) + + +def _brand_name(db: Session) -> str: + brand = db.query(models.Brand).first() + return brand.name if brand else "گروه ساختمانی آرامش" + + +def _page_meta(db: Session, page_key: str) -> models.SeoMeta | None: + return db.query(models.SeoMeta).filter(models.SeoMeta.page_key == page_key).first() + + +# ---------- robots.txt و sitemap.xml ---------- + + +@router.get("/robots.txt", response_class=Response) +def robots(db: Session = Depends(get_db)): + lines = ["User-agent: *", "Disallow: /admin", "Disallow: /api", ""] + if settings.site_url: + lines.append(f"Sitemap: {_absolute('/sitemap.xml')}") + return Response(content="\n".join(lines), media_type="text/plain; charset=utf-8") + + +@router.get("/sitemap.xml", response_class=Response) +def sitemap(db: Session = Depends(get_db)): + urls = list(STATIC_PATHS) + urls += [ + f"/blog/{a.slug}" + for a in db.query(models.Article) + .filter(models.Article.status == "published") + .order_by(models.Article.position, models.Article.id) + .all() + ] + urls += [ + f"/projects/{p.slug}" + for p in db.query(models.Project).order_by(models.Project.position, models.Project.id).all() + ] + items = "\n".join( + f" {html.escape(_absolute(u))}" for u in urls + ) + xml = ( + '\n' + '\n' + f"{items}\n\n" + ) + return Response(content=xml, media_type="application/xml; charset=utf-8") + + +# ---------- صفحات وبلاگ ---------- + + +@router.get("/blog", response_class=HTMLResponse) +def blog_list(db: Session = Depends(get_db)): + meta = _page_meta(db, "blog") + return _shell( + title=meta.title if meta and meta.title else "وبلاگ | " + _brand_name(db), + description=meta.description if meta else "", + path="/blog", + og_image=meta.image if meta else "", + ) + + +@router.get("/blog/{slug}", response_class=HTMLResponse) +def blog_detail(slug: str, db: Session = Depends(get_db)): + article = ( + db.query(models.Article) + .filter(models.Article.slug == slug, models.Article.status == "published") + .first() + ) + if article is None: + # مقاله ناموجود یا پیش‌نویس — پوسته ساده بدون متا؛ SPA پیام «یافت نشد» را نشان می‌دهد + return _shell(title=_brand_name(db), description="", path=f"/blog/{slug}") + + brand = _brand_name(db) + first_text = _doc_text(article.body) if not isinstance(article.body, list) else " ".join(article.body[:2]) + jsonld = { + "@context": "https://schema.org", + "@type": "Article", + "headline": article.title, + "description": article.excerpt, + "image": _absolute(article.image), + "inLanguage": "fa", + "author": {"@type": "Organization", "name": brand}, + "publisher": {"@type": "Organization", "name": brand}, + "mainEntityOfPage": _absolute(f"/blog/{article.slug}"), + } + noscript = f"

{html.escape(article.title)}

{html.escape(first_text)}

" + return _shell( + title=f"{article.title} | {brand}", + description=article.excerpt, + path=f"/blog/{article.slug}", + og_image=article.image, + og_type="article", + jsonld=jsonld, + noscript=noscript, + ) + + +# ---------- صفحات پروژه ---------- + + +@router.get("/projects", response_class=HTMLResponse) +def projects_list(db: Session = Depends(get_db)): + meta = _page_meta(db, "projects") + return _shell( + title=meta.title if meta and meta.title else "پروژه‌ها | " + _brand_name(db), + description=meta.description if meta else "", + path="/projects", + ) + + +@router.get("/projects/{slug}", response_class=HTMLResponse) +def project_detail(slug: str, db: Session = Depends(get_db)): + project = db.query(models.Project).filter(models.Project.slug == slug).first() + if project is None: + return _shell(title=_brand_name(db), description="", path=f"/projects/{slug}") + + brand = _brand_name(db) + return _shell( + title=f"{project.title} | {brand}", + description=project.summary, + path=f"/projects/{project.slug}", + og_image=project.image, + jsonld={ + "@context": "https://schema.org", + "@type": "WebPage", + "name": project.title, + "description": project.summary, + "image": _absolute(project.image), + "inLanguage": "fa", + }, + ) diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..80e9d4a --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,4 @@ +# وابستگی‌های توسعه و تست — pip install -r requirements-dev.txt +-r requirements.txt +pytest==8.3.4 +httpx==0.27.2 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..bc23690 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,13 @@ +# نسخه‌ها دقیق پین شده‌اند تا ارتقای خودکار یک وابستگی، استقرار را نشکند. +# ارتقا: نسخه را دستی بالا ببرید و روی محیط آماده‌سازی تست کنید. +fastapi==0.115.8 +uvicorn[standard]==0.34.0 +sqlalchemy==2.0.36 +psycopg[binary]==3.2.3 +pydantic==2.10.4 +pydantic-settings==2.6.1 +PyJWT==2.10.1 +bcrypt==4.2.1 +email-validator==2.2.0 +boto3==1.35.0 +python-multipart==0.0.18 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..7277948 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,93 @@ +"""پیکربندی تست‌ها — دیتابیس SQLite فایل‌محور و داده‌های حداقلی. + +متغیرهای محیطی پیش از ایمپورت app تنظیم می‌شوند تا settings همان‌ها را بخواند. +""" + +import os +import pathlib + +os.environ["DATABASE_URL"] = "sqlite:///./test.db?check_same_thread=false" +os.environ["SECRET_KEY"] = "test-secret-key" +os.environ["ADMIN_USERNAME"] = "admin" +os.environ["ADMIN_PASSWORD"] = "test-admin-pass-1" +os.environ["SITE_URL"] = "https://test.example" + +import pytest +from fastapi.testclient import TestClient + +from app import models +from app.auth import hash_password +from app.config import settings +from app.database import Base, SessionLocal, engine +from app.main import app + +TEST_DB = pathlib.Path("test.db") + +DOC = {"type": "doc", "content": [{"type": "paragraph"}]} + + +@pytest.fixture(scope="session", autouse=True) +def seed_db(tmp_path_factory): + # اتصالات ساخت جدول در ایمپورت ماژول باید بسته شوند تا فایل قدیمی حذف شود + engine.dispose() + if TEST_DB.exists(): + TEST_DB.unlink() + Base.metadata.create_all(bind=engine) + # dist ساختگی فرانت‌اند برای تست تزریق متا در صفحات جزئیات + dist = tmp_path_factory.mktemp("dist") + (dist / "index.html").write_text( + "Shell" + "
", + encoding="utf-8", + ) + settings.frontend_dist = str(dist) + settings.site_url = "https://test.example" + + db = SessionLocal() + try: + db.add(models.AdminUser(username="admin", password_hash=hash_password("test-admin-pass-1"))) + db.add(models.Brand(name="گروه ساختمانی آرامش")) + db.add( + models.Article( + slug="published-post", title="مقاله منتشرشده", status="published", + excerpt="چکیده مقاله", body=DOC, position=1, + ) + ) + db.add( + models.Article( + slug="draft-post", title="پیش‌نویس", status="draft", + excerpt="چکیده پیش‌نویس", body=DOC, position=2, + ) + ) + db.commit() + finally: + db.close() + yield + db.close() + engine.dispose() + if TEST_DB.exists(): + TEST_DB.unlink() + + +@pytest.fixture(autouse=True) +def reset_rate_limiter(): + from app import rate_limit + + rate_limit._failed.clear() + yield + rate_limit._failed.clear() + + +@pytest.fixture() +def client(): + return TestClient(app) + + +@pytest.fixture() +def auth_client(client): + res = client.post("/api/auth/login", json={"username": "admin", "password": "test-admin-pass-1"}) + assert res.status_code == 200, res.text + token = res.json()["access_token"] + client.headers.update({"Authorization": f"Bearer {token}"}) + yield client + client.headers.pop("Authorization", None) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..953e734 --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,182 @@ +"""تست‌های API — مسیرهای عمومی، احراز هویت، CRUD مدیریت، پیش‌نویس مقاله و سئو.""" + +import re + +DOC = {"type": "doc", "content": [{"type": "paragraph"}]} + + +# ---------- عمومی ---------- + + +def test_health(client): + res = client.get("/api/health") + assert res.status_code == 200 + assert res.json()["status"] == "ok" + + +def test_site_hides_draft_articles(client): + res = client.get("/api/site") + assert res.status_code == 200 + slugs = [a["slug"] for a in res.json()["articles"]] + assert "published-post" in slugs + assert "draft-post" not in slugs + + +def test_public_articles_hide_drafts(client): + res = client.get("/api/articles") + assert all(a["status"] == "published" for a in res.json()) + assert client.get("/api/articles/draft-post").status_code == 404 + + +def test_admin_endpoints_reject_anonymous(client): + assert client.get("/api/admin/articles").status_code == 401 + + +# ---------- احراز هویت ---------- + + +def test_login_success_and_failure(client): + assert ( + client.post("/api/auth/login", json={"username": "admin", "password": "wrong"}).status_code + == 401 + ) + res = client.post("/api/auth/login", json={"username": "admin", "password": "test-admin-pass-1"}) + assert res.status_code == 200 + assert res.json()["access_token"] + + +def test_change_password(auth_client, client): + res = auth_client.post( + "/api/auth/change-password", + json={"current_password": "wrong", "new_password": "brand-new-pass-9"}, + ) + assert res.status_code == 400 + + res = auth_client.post( + "/api/auth/change-password", + json={"current_password": "test-admin-pass-1", "new_password": "brand-new-pass-9"}, + ) + assert res.status_code == 200 + + # رمز قدیمی رد و رمز جدید قبول می‌شود + assert ( + client.post("/api/auth/login", json={"username": "admin", "password": "test-admin-pass-1"}).status_code + == 401 + ) + assert ( + client.post("/api/auth/login", json={"username": "admin", "password": "brand-new-pass-9"}).status_code + == 200 + ) + + # بازگرداندن رمز برای تست‌های بعدی + res = client.post("/api/auth/login", json={"username": "admin", "password": "brand-new-pass-9"}) + auth = {"Authorization": f"Bearer {res.json()['access_token']}"} + res = client.post( + "/api/auth/change-password", + json={"current_password": "brand-new-pass-9", "new_password": "test-admin-pass-1"}, + headers=auth, + ) + assert res.status_code == 200 + + +def test_login_rate_limited(client): + for _ in range(5): + res = client.post("/api/auth/login", json={"username": "admin", "password": "wrong"}) + assert res.status_code == 401 + res = client.post("/api/auth/login", json={"username": "admin", "password": "wrong"}) + assert res.status_code == 429 + assert "Retry-After" in res.headers + + +# ---------- CRUD مدیریت ---------- + + +def test_article_crud_and_draft_flow(auth_client, client): + created = auth_client.post( + "/api/admin/articles", + json={"slug": "flow-post", "title": "مقاله جریان", "body": DOC}, + ) + assert created.status_code == 201, created.text + article = created.json() + # مقاله تازه پیش‌نویس است و در سایت عمومی دیده نمی‌شود + assert article["status"] == "draft" + assert "flow-post" not in [a["slug"] for a in client.get("/api/articles").json()] + + res = auth_client.patch(f"/api/admin/articles/{article['id']}", json={"seo_keyphrase": "ساختمان"}) + assert res.json()["seo_keyphrase"] == "ساختمان" + + res = auth_client.patch(f"/api/admin/articles/{article['id']}", json={"status": "published"}) + assert res.status_code == 200 + assert "flow-post" in [a["slug"] for a in client.get("/api/articles").json()] + + assert auth_client.delete(f"/api/admin/articles/{article['id']}").status_code == 204 + assert "flow-post" not in [a["slug"] for a in client.get("/api/articles").json()] + + +def test_service_roundtrip_and_reorder(auth_client): + ids = [] + for name in ("خدمت الف", "خدمت ب"): + res = auth_client.post("/api/admin/services", json={"title": name}) + assert res.status_code == 201 + ids.append(res.json()["id"]) + + res = auth_client.post("/api/admin/services/reorder", json={"ids": ids[::-1]}) + assert res.status_code == 200 + ordered = [s["id"] for s in res.json() if s["id"] in ids] + assert ordered == ids[::-1] + + res = auth_client.patch(f"/api/admin/services/{ids[0]}", json={"desc": "توضیح تازه"}) + assert res.json()["desc"] == "توضیح تازه" + + for i in ids: + assert auth_client.delete(f"/api/admin/services/{i}").status_code == 204 + + +def test_seo_meta_create_and_patch(auth_client): + res = auth_client.post( + "/api/admin/seo-meta", json={"page_key": "test-page", "title": "عنوان", "description": "توضیح"} + ) + assert res.status_code == 201 + meta_id = res.json()["id"] + res = auth_client.patch(f"/api/admin/seo-meta/{meta_id}", json={"title": "عنوان تازه"}) + assert res.json()["title"] == "عنوان تازه" + assert auth_client.delete(f"/api/admin/seo-meta/{meta_id}").status_code == 204 + + +# ---------- سئو ---------- + + +def test_robots_txt(client): + res = client.get("/robots.txt") + assert res.status_code == 200 + assert "Disallow: /admin" in res.text + assert "Sitemap: https://test.example/sitemap.xml" in res.text + + +def test_sitemap_includes_published_only(client): + res = client.get("/sitemap.xml") + assert res.status_code == 200 + assert "/blog/published-post" in res.text + assert "/blog/draft-post" not in res.text + assert res.text.startswith("", res.text)) == 1 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..4784fa5 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,57 @@ +# لایه تولید — TLS با Let's Encrypt و انتشار روی پورت‌های استاندارد ۸۰/۴۴۳. +# پیش‌نیاز: متغیرهای DOMAIN و ACME_EMAIL در .env؛ به‌علاوه رکورد DNS دامنه به این سرور. +# +# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +# +# همه درخواست‌های HTTP به HTTPS هدایت می‌شوند و مسیریابی‌ها فقط روی دامنه DOMAIN پاسخ می‌دهند. + +services: + traefik: + command: !override + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.le.acme.email=${ACME_EMAIL:?ACME_EMAIL را در .env تنظیم کنید} + - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.le.acme.httpchallenge=true + - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web + ports: !override + - "80:80" + - "443:443" + volumes: !override + - /var/run/docker.sock:/var/run/docker.sock:ro + - letsencrypt_data:/letsencrypt + + backend: + environment: + # سایت هم‌مبدأ است — CORS لازم نیست + CORS_ORIGINS: '[]' + SITE_URL: https://${DOMAIN:?DOMAIN را در .env تنظیم کنید} + labels: + - traefik.enable=true + - traefik.http.routers.api.rule=Host(`${DOMAIN}`) && PathPrefix(`/api`) + - traefik.http.routers.api.entrypoints=websecure + - traefik.http.routers.api.tls.certresolver=le + - traefik.http.routers.seo.rule=Host(`${DOMAIN}`) && (PathPrefix(`/blog/`) || Path(`/blog`) || PathPrefix(`/projects/`) || Path(`/projects`) || PathPrefix(`/robots.txt`) || PathPrefix(`/sitemap.xml`)) + - traefik.http.routers.seo.entrypoints=websecure + - traefik.http.routers.seo.tls.certresolver=le + + minio: + labels: + - traefik.enable=true + - traefik.http.routers.media.rule=Host(`${DOMAIN}`) && PathPrefix(`/media`) + - traefik.http.routers.media.entrypoints=websecure + - traefik.http.routers.media.tls.certresolver=le + + frontend: + labels: + - traefik.enable=true + - traefik.http.routers.web.rule=Host(`${DOMAIN}`) && PathPrefix(`/`) + - traefik.http.routers.web.entrypoints=websecure + - traefik.http.routers.web.tls.certresolver=le + +volumes: + letsencrypt_data: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ceb7466 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,158 @@ +# مقادیر محرمانه از فایل .env ریشه خوانده می‌شوند — از .env.example کپی کنید: +# cp .env.example .env +# برای انتشار با TLS، روی این فایل docker-compose.prod.yml را هم سوار کنید (README). + +services: + # نقطه ورود واحد — مسیریابی / (سایت استاتیک)، /api (بک‌اند) و /media (MinIO) + traefik: + image: traefik:v3 + restart: unless-stopped + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + ports: + # پورت به 8080 تغییر کرد تا با پورت 5173 سرور لوکال Vite تداخل نکند + - "${TRAEFIK_HTTP_PORT:-8080}:80" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + db: + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_USER: aramesh + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD را در .env تنظیم کنید} + POSTGRES_DB: arameshplaza + volumes: + - db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U aramesh -d arameshplaza"] + interval: 5s + timeout: 5s + retries: 12 + + minio: + image: minio/minio:latest + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 5s + timeout: 5s + retries: 12 + ports: + - "9001:9001" # کنسول MinIO + - "9000:9000" # دسترسی لوکال به تصاویر و مدیا + labels: + - traefik.enable=true + - traefik.http.routers.media.rule=PathPrefix(`/media`) + - traefik.http.routers.media.entrypoints=web + - traefik.http.routers.media.priority=100 + - traefik.http.routers.media.middlewares=media-rewrite + - traefik.http.middlewares.media-rewrite.replacepathregex.regex=^/media/(.*) + - traefik.http.middlewares.media-rewrite.replacepathregex.replacement=/aramesh-media/$$1 + - traefik.http.services.media.loadbalancer.server.port=9000 + + # یک‌بار اجرا می‌شود: ساخت باکت و بازگذاشتن خواندن عمومی برای سرو تصاویر + minio-setup: + image: minio/mc:latest + depends_on: + minio: + condition: service_healthy + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} + MINIO_BUCKET: aramesh-media + restart: "no" + entrypoint: > + /bin/sh -c " + mc alias set local http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD && + mc mb --ignore-existing local/$$MINIO_BUCKET && + mc anonymous set download local/$$MINIO_BUCKET && + echo 'minio-setup: bucket ready' + " + + backend: + build: ./backend + restart: unless-stopped + ports: + - "8000:8000" # اتصال مستقیم بک‌اند به ویندوز برای فرانت‌اند لوکال + environment: + DATABASE_URL: postgresql+psycopg://aramesh:${POSTGRES_PASSWORD}@db:5432/arameshplaza + SECRET_KEY: ${SECRET_KEY:?SECRET_KEY را در .env تنظیم کنید} + CORS_ORIGINS: ${CORS_ORIGINS:-'["http://localhost:5173","http://127.0.0.1:5173"]'} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD را در .env تنظیم کنید} + S3_ENDPOINT: http://minio:9000 + S3_ACCESS_KEY: ${MINIO_ROOT_USER:-aramesh} + S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} + S3_BUCKET: aramesh-media + MEDIA_PUBLIC_BASE: /media + # نشانی عمومی سایت — برای sitemap و آدرس مطلق OG (مثل https://aramesh.example) + SITE_URL: ${SITE_URL:-} + # بیلد فرانت‌اند برای تزریق متای سئو در صفحات /blog و /projects + FRONTEND_DIST: /srv/frontend + volumes: + - ./frontend/dist:/srv/frontend:ro + depends_on: + db: + condition: service_healthy + minio: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s + labels: + - traefik.enable=true + - traefik.http.routers.api.rule=PathPrefix(`/api`) + - traefik.http.routers.api.entrypoints=web + - traefik.http.routers.api.priority=100 + - traefik.http.services.api.loadbalancer.server.port=8000 + # robots.txt و sitemap.xml و صفحات وبلاگ/پروژه با متای سئو — اولویت بالاتر از SPA + - traefik.http.routers.seo.rule=PathPrefix(`/blog/`) || Path(`/blog`) || PathPrefix(`/projects/`) || Path(`/projects`) || PathPrefix(`/robots.txt`) || PathPrefix(`/sitemap.xml`) + - traefik.http.routers.seo.entrypoints=web + - traefik.http.routers.seo.priority=50 + - traefik.http.routers.seo.service=api + + # فقط سرو فایل‌های استاتیک SPA — بیلد روی هاست (npm run build) انجام شده است + frontend: + build: ./frontend + restart: unless-stopped + depends_on: + - backend + - minio + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost/"] + interval: 15s + timeout: 5s + retries: 5 + labels: + - traefik.enable=true + - traefik.http.routers.web.rule=PathPrefix(`/`) + - traefik.http.routers.web.entrypoints=web + - traefik.http.routers.web.priority=1 + - traefik.http.services.web.loadbalancer.server.port=80 + + # ابزار پشتیبان‌گیری — فقط با پروفایل tools اجرا می‌شود (scripts/backup.sh این کار را می‌کند) + mc: + image: minio/mc:latest + profiles: ["tools"] + depends_on: + minio: + condition: service_healthy + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} + +volumes: + db_data: + minio_data: \ No newline at end of file diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Caddyfile b/frontend/Caddyfile new file mode 100644 index 0000000..71db35c --- /dev/null +++ b/frontend/Caddyfile @@ -0,0 +1,12 @@ +:80 { + root * /srv + encode gzip + + # مسیریابی SPA — مسیرهای ناشناخته به index.html برمی‌گردند + try_files {path} /index.html + file_server + + # کش بلندمدت برای فایل‌های هش‌دار بیلد + @assets path /assets/* + header @assets Cache-Control "public, immutable, max-age=31536000" +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..496113a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,10 @@ +# ---------- فرانت‌اند فقط سرو می‌کند ---------- +# بیلد روی هاست انجام می‌شود (npm install && npm run build) و خروجی dist/ +# داخل ایمیج کپی می‌شود — شبکه کانتینر بیلد برای npm قابل‌اعتماد نیست. + +FROM caddy:2-alpine + +COPY Caddyfile /etc/caddy/Caddyfile +COPY dist /srv + +EXPOSE 80 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..ac41333 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,33 @@ +# فرانت‌اند گروه ساختمانی آرامش + +سایت React + Vite + Tailwind — تمام متن‌ها و تصاویر از بک‌اند (`GET /api/site`) خوانده می‌شود. + +## اجرا + +```bash +npm install +npm run dev +``` + +در حالت توسعه، Vite درخواست‌های `/api` و `/media` را به Traefik (پورت ۵۱۷۳) پروکسی می‌کند — +پس استک Docker باید در حال اجرا باشد. آدرس بک‌اند در `src/config.ts` از `VITE_API_URL` +خوانده می‌شود؛ پیش‌فرض خالی یعنی درخواست‌های هم‌مبدأ. + +## ساخت + +بیلد روی هاست انجام می‌شود (کانتینر فقط با Caddy فایل‌های `dist/` را سرو می‌کند): + +```bash +npm run build +``` + +> نکته: بک‌اند برای تزریق متای سئو در صفحات `/blog` و `/projects` همین پوشه `dist/` را +> (در compose به `/srv/frontend` سوار است) می‌خواند — بعد از هر تغییر فرانت، `npm run build` +> و سپس ری‌استارت بک‌اند کافی است. + +## بخش‌های کلیدی + +- `src/api.tsx` — بارگذاری داده سایت و ادغام پیش‌نویس مقاله‌ها برای ادمین +- `src/admin/` — لایه ویرایش زنده (دوقلوی دیجیتال): نوار مدیریت، کشوی موجودیت‌ها، صفحه نوشتن مقاله +- `src/seo.ts` — تگ‌های title / description / OG / canonical / JSON-LD هر صفحه +- `src/data/site.ts` — انواع داده مشترک diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..94f729c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,21 @@ + + + + + + + گروه ساختمانی آرامش + + + + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..b866068 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5203 @@ +{ + "name": "magic-patterns-vite-template", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "magic-patterns-vite-template", + "version": "0.0.1", + "dependencies": { + "@emotion/react": "^11.13.3", + "@tiptap/extension-image": "^3.31.3", + "@tiptap/extension-link": "^3.31.3", + "@tiptap/pm": "^3.31.3", + "@tiptap/react": "^3.31.3", + "@tiptap/starter-kit": "^3.31.3", + "@tiptap/static-renderer": "^3.31.3", + "framer-motion": "^11.5.4", + "lucide-react": "0.522.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "sweetalert2": "^11.26.25" + }, + "devDependencies": { + "@types/node": "^20.11.18", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@typescript-eslint/eslint-plugin": "^5.54.0", + "@typescript-eslint/parser": "^5.54.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "latest", + "eslint": "^8.50.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.1", + "postcss": "latest", + "tailwindcss": "3.4.17", + "typescript": "^5.5.4", + "vite": "^5.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT", + "optional": true + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tiptap/core": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.31.3.tgz", + "integrity": "sha512-Cz50pvciQrxdSxgTkHOVz0uD0Yl/8Xt0QatGD6ILm47jW8EzyHR9RkUGs/D5IqzXKuVPntfw1ttaT926vXfiRg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.31.3.tgz", + "integrity": "sha512-fyY2XMbyDDDfOTQ1Qdrnqa1qwC9DWE4n7AfE0EKQI0G8MfLV8RaDlLDcOZDJ9JbMPY7/Gx7EjyK4NKxSW9n2hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.31.3.tgz", + "integrity": "sha512-dIuYhKk8TitKU/FeDpoTeWZhU42YgDN5npgWNjAmMmRktPdoxnH3/wGSiwXlqZgJWjehN7kPWDePWpJeAImGpQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.31.3.tgz", + "integrity": "sha512-EV6ZnwKc++2OM/OcD54n8s1B7C9LP7GKAtdEPwu0t3BYJf3sG8Ueoinf7SgGPO9oMPg96GneOkDNm1urMV167g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.31.3.tgz", + "integrity": "sha512-qEyyoPapPef4LO8XKaN83bxtaNzkJ4kFn/IxLnEKd4BJ3Mvi4MH2yJYlyDqwFnMoev4pMA1zBHRAt/C0THRmZw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.31.3.tgz", + "integrity": "sha512-SzxOqchrD2AcN3uT67PjKmRFEMOU3vNNiwNaamZJUbZrI6Hmy+bvdHJrc3jrIddyyCrAtsnAfRI0cmorW1jGfg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.31.3.tgz", + "integrity": "sha512-nvknt4FhyJQjYcvxptmeUlFsIAc8ibua3E5BN4Pim374/9RWepH4cdE9X0/qUTtguHElLx0iKtSIY3rW2qGTFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.31.3.tgz", + "integrity": "sha512-EexgmqnyDNyGlISxo7SMrp5MygpJYmqD+0cY5jB6L1U6L4CpKKRWUt8OO1sWzEHDE1+TTvwt+WIFoIWAziOtEA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.31.3.tgz", + "integrity": "sha512-NWomSfu5CSC7VacnMSDzKT8qm66SzMfZwVPEtwY5bPpRTJgTiT1rNK0neDrrzfMN27MfylGyKWWf7Q5Qf8w/fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.31.3" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.31.3.tgz", + "integrity": "sha512-rd4VJ9PGSP9Eop8ZTEwaLZcMzMXLuJKe3hUNf58rq+zANpwM+9fI+vB7g9MAc3eXwUNDxNDVACFIL2oeBqpQ3A==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.31.3.tgz", + "integrity": "sha512-EBXKb1FrVStsNYCcRGtd9jmzveCvR+eqgg1rVqoONrqFK6U7bga6LN+1dMKroP1kliDVgveYkP3vRYxqw+rFqg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.31.3" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.31.3.tgz", + "integrity": "sha512-QAdCvNO4+yW9ATwsrej11NTkDYFqPLIEQr3ARNrKOK1qaiS7A0fia2SEukb/hrkP3A6mbozhoQt2r2RGUf/DpQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.31.3.tgz", + "integrity": "sha512-rk5VHMAeQcg06SLauN6EGdD2jc0O2qY8QkZYPd0LxNvLblw2BxBx+lxUQSYwLAT9Ie5914gKIK2YbRyO2Ts3ig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.31.3.tgz", + "integrity": "sha512-YnHGy2KShRwvCseAmmxl9VP7R0qaj8QMp3DA6DJWZqp7r5gLGvDkAqhedxqqefqsE4Y43hjkBdjtB9Ce78LkIw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.31.3.tgz", + "integrity": "sha512-wWNG9BOtx2Cg4vwxPVGDIJ5DGX+ETkldeoaFJLB1NXUL4OPUiVTf8cHZ+hdYgJWP704BEB7jQgjlpvdi6VigTg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.31.3.tgz", + "integrity": "sha512-ibGvdvAPyfxBMUVNRI43eb9h2/Jka1MRG5GtnGqbcCX/2+Y/y0EOfFrPQPkuYphiWQYyu9+FzPKMB/pjuaLuKQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.31.3.tgz", + "integrity": "sha512-986wOQzTL9Zr5lf84LCLpm+YOms8A0K39/8DVoqRfebqcOe0/eq4bnztmAlfabOd+kJY92g3AgZERFUx/w+dcw==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.31.3.tgz", + "integrity": "sha512-LoveGnC0FVdCV4jUNBaG1ZA+KWE07+adzV3kGy6uUYFcJEjbVUHTnPDrBOob3IwOSO3sCwIkvQb6EVYeXn/4yg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.31.3.tgz", + "integrity": "sha512-4QlKOriJJMvg95QJTEsy9BUYPBQ6UvJyb8WURRwdUtQUkkqb8h32lg/eyQUv2FzW9IT1AdUyNZfVaxH64kRfQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.31.3.tgz", + "integrity": "sha512-If8UOEdDZbPJU6iYTvLtH6DOp2KBy6BKxg9UELL1AevVetGHEF/7lW8hP50Gn1tHMVpPRqmhSzVrJRpEJJgb/Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.31.3.tgz", + "integrity": "sha512-mp3g11NgA/PYu8rj7J7Ez3l4qBy6WfTSmHIG4PZvEGG5w2oUAIkgb9DU7nPPzjmeme27oazFYZw+AtZA0+u4tw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.31.3.tgz", + "integrity": "sha512-+iPku7wJfy5hbNDNLX8dveFtYVsZMmh7vztjuq8hT3mipSC4IByDehDbU8fzUCjfXiEzmI7mQn7c8LGmHULuzA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.31.3.tgz", + "integrity": "sha512-G29bhKttYwcKHT+BI6emWVFol3RO/gUXxQVcmr/iT8LXXy7j8J6HFUnsKM+Kg5YlP1rxMRgsa65dbrQVahZi0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.31.3.tgz", + "integrity": "sha512-gdsWtF+taeaCu6V+5Ct10fGo0ACUy1GnYtbb+mcathBt8OqbT+Ws60p/yEmKesBDz2Hn+B5IcWy6+2BBZl5ZTg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.31.3.tgz", + "integrity": "sha512-HghdJaOwRqYzsAxqSyNyb+IWyOMcdCl8IoiBETA9BZCJAqdXzFLcuWp7CqCqJPam9dgkWosSoHqTCAO4nTBfpw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.31.3.tgz", + "integrity": "sha512-8sJNPGGUe8f3aDojcOW5cfVL7I5NrBbE0UWxG08qoi9Tea6qWbvQJsCR9tsrOapr/DaLr3kpbGZ9s1gEEfcNcA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.31.3.tgz", + "integrity": "sha512-sZime0SWsz/k62W2WvHx5Ig7G2h7kVhrrmnqy+wEgIHfDwEfOlelRjaWCiBCFlF7dxGUntJusCh9FxlLhni0Ag==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.11", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.42.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.31.3.tgz", + "integrity": "sha512-QiwQqvaLFLm5EMFu5tg7nAgXJxCUiUTLD8EsK+TqVV5P4bqOoMOCM39khbhXTJyahCuYpiFWx5YOSDtC/JiPtg==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.31.3", + "@tiptap/extension-floating-menu": "^3.31.3" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.31.3.tgz", + "integrity": "sha512-WKof9RewdmGHvWJ1wn0/HVNG2mV+HOgVRyJkKekuM9fgr6BZAAH/xZsWE1eon+94JnQ+KtK2ThydXQM/qc6b2A==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/extension-blockquote": "3.31.3", + "@tiptap/extension-bold": "3.31.3", + "@tiptap/extension-bullet-list": "3.31.3", + "@tiptap/extension-code": "3.31.3", + "@tiptap/extension-code-block": "3.31.3", + "@tiptap/extension-document": "3.31.3", + "@tiptap/extension-dropcursor": "3.31.3", + "@tiptap/extension-gapcursor": "3.31.3", + "@tiptap/extension-hard-break": "3.31.3", + "@tiptap/extension-heading": "3.31.3", + "@tiptap/extension-horizontal-rule": "3.31.3", + "@tiptap/extension-italic": "3.31.3", + "@tiptap/extension-link": "3.31.3", + "@tiptap/extension-list": "3.31.3", + "@tiptap/extension-list-item": "3.31.3", + "@tiptap/extension-list-keymap": "3.31.3", + "@tiptap/extension-ordered-list": "3.31.3", + "@tiptap/extension-paragraph": "3.31.3", + "@tiptap/extension-strike": "3.31.3", + "@tiptap/extension-text": "3.31.3", + "@tiptap/extension-underline": "3.31.3", + "@tiptap/extensions": "3.31.3", + "@tiptap/pm": "3.31.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/static-renderer": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/static-renderer/-/static-renderer-3.31.3.tgz", + "integrity": "sha512-TzbN/aSTQBhGZL94aaEnfbwZw/gIiQd8/q3TucTYuzdbsrsZNfBMg7CCA9W9dOg4vlKxCTLRVas21ivsrUm7og==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.407", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", + "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.2.tgz", + "integrity": "sha512-Ywe6jodPTWOTL9/k0bV7gdfP8twKL5Y8I8CZ933fAY5gBekICZSUQTbyH6ut2NZCNyB05mSUwAuEqdEIaOOlDQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.522.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz", + "integrity": "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.2.tgz", + "integrity": "sha512-ViYrjMSg3YFiXwIhKaluu+/mi3Yrxt6AR8ri14ulTaGcZtXO1CThl7A2gv79qx5fQnOw8woKwyBU2u+9PVCm3w==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz", + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.11", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.1.tgz", + "integrity": "sha512-t4F5615FycnCqsX7ShTUs8+jfnwcf46kuFRvSl/3qFx2QTZ4DjgowesLHQXcFP7G//TjesqZG3WtgL7vdyVJyA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.3", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz", + "integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sweetalert2": { + "version": "11.26.25", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.25.tgz", + "integrity": "sha512-+hunCOJdJ6FLj04T9YSLvvZXRjsvIkTeTKP2e4VF8CaBias961BTnWiSFAy7F/CM5eq3QK2Rraoc5Gzftslvkg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..de07a6f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "magic-patterns-vite-template", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "npx vite", + "build": "npx vite build", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "preview": "npx vite preview" + }, + "dependencies": { + "@emotion/react": "^11.13.3", + "@tiptap/extension-image": "^3.31.3", + "@tiptap/extension-link": "^3.31.3", + "@tiptap/pm": "^3.31.3", + "@tiptap/react": "^3.31.3", + "@tiptap/starter-kit": "^3.31.3", + "@tiptap/static-renderer": "^3.31.3", + "framer-motion": "^11.5.4", + "lucide-react": "0.522.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "sweetalert2": "^11.26.25" + }, + "devDependencies": { + "@types/node": "^20.11.18", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@typescript-eslint/eslint-plugin": "^5.54.0", + "@typescript-eslint/parser": "^5.54.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "latest", + "eslint": "^8.50.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.1", + "postcss": "latest", + "tailwindcss": "3.4.17", + "typescript": "^5.5.4", + "vite": "^5.2.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..3fa715f --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,140 @@ +import React, { useEffect } from 'react'; +import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Preloader } from './components/Preloader'; +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; +import { FloatingContact } from './components/FloatingContact'; +import { Home } from './pages/Home'; +import { About } from './pages/About'; +import { ServicesPage } from './pages/ServicesPage'; +import { Projects } from './pages/Projects'; +import { ProjectDetail } from './pages/ProjectDetail'; +import { Investment } from './pages/Investment'; +import { Blog } from './pages/Blog'; +import { ArticleDetail } from './pages/ArticleDetail'; +import { FaqPage } from './pages/FaqPage'; +import { ContactPage } from './pages/ContactPage'; +import { SiteProvider, useSiteData } from './api'; +import { applySeo, seoKeyForPath } from './seo'; +import { AdminProvider, useAdmin } from './admin/AdminContext'; +import { AdminBar } from './admin/AdminBar'; +import { AdminLoginPage } from './admin/AdminLoginPage'; +import { ArticleWritePage } from './admin/ArticleWritePage'; + +function ScrollToTop() { + const { pathname } = useLocation(); + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'auto' }); + }, [pathname]); + return null; +} + +function AnimatedRoutes() { + const location = useLocation(); + return ( + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); + +} + +function AppChrome() { + const { pathname } = useLocation(); + const { isAuthenticated } = useAdmin(); + const { data } = useSiteData(); + const isAdminPage = pathname.startsWith('/admin'); + + // عنوان و توضیح متای هر صفحه از دیتابیس (سئو) — جز صفحه‌های جزئیات که خودشان ست می‌کنند + useEffect(() => { + if (isAdminPage || !data) return; + const key = seoKeyForPath(pathname); + if (!key) return; + const meta = data.seo_meta?.find((s) => s.page_key === key); + const brand = data.brand.name; + const navLabel = data.nav.find((n) => n.href === `/${key}`)?.label; + applySeo({ + title: meta?.title || (key === 'home' ? brand : navLabel ? `${navLabel} | ${brand}` : brand), + description: meta?.description ?? '', + path: pathname, + }); + }, [pathname, data, isAdminPage]); + + return ( + <> + {!isAdminPage &&
} + + {!isAdminPage &&