diff --git a/.gitignore b/.gitignore index 88765ed..185971f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,8 @@ h origin main # OS Files Thumbs.db + +# wrangler files +.wrangler +.dev.vars* +!.dev.vars.example diff --git a/docs/bug-report-auto-logout.md b/docs/bug-report-auto-logout.md new file mode 100644 index 0000000..41f719a --- /dev/null +++ b/docs/bug-report-auto-logout.md @@ -0,0 +1,174 @@ +# Bug Report: مشكلة الـ Logout التلقائي العشوائي + +## وصف المشكلة + +المستخدم يسجل دخول بنجاح، لكن في بعض الأحيان **يُخرَج من التطبيق فوراً** دون أي تدخل منه. +المشكلة **غير منتظمة** — أحياناً بتحصل وأحياناً لا. + +--- + +## السبب الجذري: مشكلتان متداخلتان + +--- + +### المشكلة الأولى (الأخطر): Race Condition في تخزين الجلسة + +**الملف:** `src/integrations/supabase/client.ts` — دالة `setItem` + +#### كيف كان الكود يعمل؟ + +التطبيق بيستخدم custom storage بدل localStorage الافتراضي لـ Supabase. +دالة `setItem` كانت مسؤولة عن القرار: هل تحفظ الجلسة في **localStorage** (دائم) أو **sessionStorage** (مؤقت يتمسح عند إغلاق التاب). + +```ts +// الكود القديم المعطوب +let rememberMe = true; +try { + rememberMe = localStorage.getItem('rememberMe') !== 'false'; +} catch (e) {} + +if (rememberMe) { + localStorage.setItem(key, value); // جلسة دائمة +} else { + sessionStorage.setItem(key, value); // جلسة مؤقتة تتمسح +} +``` + +#### لماذا هذا خطأ؟ + +عند تسجيل الدخول يحدث التسلسل التالي بالتوازي: + +``` +المستخدم يضغط "تسجيل الدخول" + | + v + Supabase يُرجع session + | + -----+--------------------- + | | + v v + supabase يُطلق Auth.tsx يحفظ + setItem() لحفظ rememberMe=true + الجلسة في localStorage + | + | يقرأ localStorage.getItem('rememberMe') + | + v + الحالة الطبيعية: يجد null => true => يحفظ في localStorage (OK) + حالة المشكلة: يجد 'false' (قيمة قديمة مترسبة) => يحفظ في sessionStorage (BUG) + | + عند تحديث الصفحة: sessionStorage يتمسح + | + SIGNED_OUT event + | + Logout تلقائي! +``` + +#### لماذا مش بتحصل دايماً؟ + +لأن الـ Race Condition بتعتمد على: +- **سرعة الاتصال** — لو الاستجابة سريعة، `setItem` بييجي بعد ما `rememberMe` اتحفظ (OK) +- **حالة localStorage** — لو فيه قيمة قديمة `'false'` من جلسة سابقة => يحفظ في `sessionStorage` (BUG) +- **نوع الـ Event** — `INITIAL_SESSION` و `TokenRefreshed` بيطلقوا `setItem` بشكل مستقل عن flow تسجيل الدخول + +#### الإصلاح + +```ts +// الكود الجديد +let rememberMe = true; +try { + const stored = localStorage.getItem('rememberMe'); + // فقط لو المستخدم صراحةً اختار 'false' نحوّل لـ sessionStorage + if (stored === 'false') { + rememberMe = false; + } +} catch (e) {} +``` + +| السيناريو | الكود القديم | الكود الجديد | +|-----------|-------------|--------------| +| `rememberMe = null` (مش متحفظ) | true => localStorage (OK) | true => localStorage (OK) | +| `rememberMe = 'true'` | true => localStorage (OK) | true => localStorage (OK) | +| `rememberMe = 'false'` (مقصود) | false => sessionStorage (OK) | false => sessionStorage (OK) | +| قيمة قديمة `'false'` مترسبة | false => sessionStorage => Logout عشوائي (BUG) | لا يحدث — نفس السيناريو السابق (OK) | + +--- + +### المشكلة الثانية: Stale Closure في AuthContext + +**الملف:** `src/contexts/AuthContext.tsx` — داخل `onAuthStateChange` + +#### الكود القديم + +```ts +// الكود القديم +useEffect(() => { + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, session) => { + if (event === 'SIGNED_IN') { + // profile هنا من الـ closure القديم! + // useEffect اشتغل مرة واحدة، profile كانت null في البداية + // وظلت null داخل الـ callback للأبد + if (!profile || profile.id !== session.user.id) { + fetchProfile(session.user.id); + } + } + } + ); +}, [fetchProfile]); // profile مش في dependencies! +``` + +#### شرح المشكلة + +الـ `useEffect` بيشتغل **مرة واحدة** ويُنشئ callback للـ `onAuthStateChange`. +هذا الـ callback بيحتفظ بـ **snapshot** من قيمة `profile` في وقت إنشائه (اللي كانت `null`). + +``` +عند تسجيل الدخول: + profile state => { id: 'user-123', ... } (القيمة الحقيقية في الذاكرة) + profile في الـ callback => null (snapshot قديم من وقت إنشاء useEffect) + +نتيجة: كل مرة بييجي event يشوف profile=null فيعمل fetchProfile دايماً => احتمال مشاكل +``` + +#### الإصلاح + +```ts +// الكود الجديد — استخدام Ref بدل State +const profileIdRef = useRef(null); + +// عند حفظ الـ profile: +if (profileData) { + setProfile(profileData); + profileIdRef.current = profileData.id; // ref دايماً محدث +} + +// في onAuthStateChange: +if (!profileIdRef.current || profileIdRef.current !== session.user.id) { + // ref بيقرأ القيمة الحالية دايماً، مش snapshot قديم + fetchProfile(session.user.id); +} +``` + +| | useState | useRef | +|--|--|--| +| القيمة داخل useEffect | snapshot من وقت آخر render | دايماً القيمة الحالية | +| يسبب Stale Closure | نعم | لا | + +--- + +## ملخص التغييرات + +| الملف | المشكلة | الإصلاح | +|-------|---------|---------| +| `src/integrations/supabase/client.ts` | Race Condition — ممكن يحفظ الجلسة في sessionStorage | تغيير المنطق: localStorage دايماً ما لم يكن 'false' صراحةً | +| `src/contexts/AuthContext.tsx` | Stale Closure — قراءة profile من closure قديم | استبدال profile بـ profileIdRef للحصول على القيمة الحالية دايماً | + +--- + +## نتيجة الإصلاح + +- الجلسة دايماً تتحفظ في localStorage بشكل موثوق +- تحديث الصفحة لا يُخرج المستخدم من التطبيق +- Token Refresh لا يسبب logout عشوائي +- fetchProfile بيشتغل في الوقت الصح بس diff --git a/package-lock.json b/package-lock.json index a219d71..5fc8a98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "vite_react_shadcn_ts", "version": "0.0.0", + "license": "GPL-3.0", "dependencies": { "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", @@ -3427,7 +3428,6 @@ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3439,7 +3439,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3499,7 +3498,6 @@ "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", @@ -3732,7 +3730,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4100,7 +4097,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -4564,7 +4560,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -4703,8 +4698,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4814,7 +4808,6 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6260,7 +6253,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6453,7 +6445,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6480,7 +6471,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6508,7 +6498,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.61.1.tgz", "integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7208,7 +7197,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -7334,7 +7322,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7416,7 +7403,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7652,7 +7638,6 @@ "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -7746,7 +7731,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 3844f54..13c40e6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "build": "vite build", "build:dev": "vite build --mode development", "lint": "eslint .", - "preview": "vite preview" + "preview": "pnpm run build && wrangler dev", + "deploy": "pnpm run build && wrangler deploy" }, "dependencies": { "@hookform/resolvers": "^3.10.0", @@ -70,6 +71,7 @@ "zod": "^3.25.76" }, "devDependencies": { + "@cloudflare/vite-plugin": "^1.44.0", "@eslint/js": "^9.32.0", "@tailwindcss/typography": "^0.5.16", "@types/js-cookie": "^3.0.6", @@ -86,6 +88,7 @@ "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", - "vite": "^7.3.0" + "vite": "^7.3.0", + "wrangler": "^4.110.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 477e439..c488065 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: specifier: ^3.25.76 version: 3.25.76 devDependencies: + '@cloudflare/vite-plugin': + specifier: ^1.44.0 + version: 1.44.0(vite@7.3.3(@types/node@22.19.19)(jiti@1.21.7))(workerd@1.20260708.1)(wrangler@4.110.0) '@eslint/js': specifier: ^9.32.0 version: 9.39.4 @@ -228,6 +231,9 @@ importers: vite: specifier: ^7.3.0 version: 7.3.3(@types/node@22.19.19)(jiti@1.21.7) + wrangler: + specifier: ^4.110.0 + version: 4.110.0 packages: @@ -239,162 +245,375 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/vite-plugin@1.44.0': + resolution: {integrity: sha512-8wGGunqRcs34o4GRq0Rurp7GZg30xtLJeRGUU81a49r9zQRjlp3xIlsWr3nFlSCso4eE3cjZfiKC/2y116M4TQ==} + hasBin: true + peerDependencies: + vite: ^6.1.0 || ^7.0.0 || ^8.0.0 + wrangler: ^4.110.0 + + '@cloudflare/workerd-darwin-64@1.20260708.1': + resolution: {integrity: sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260708.1': + resolution: {integrity: sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260708.1': + resolution: {integrity: sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260708.1': + resolution: {integrity: sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260708.1': + resolution: {integrity: sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.7': resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.7': resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.7': resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.7': resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.7': resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.7': resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.7': resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.7': resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.7': resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.7': resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.7': resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.7': resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.7': resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.7': resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.7': resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.7': resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.7': resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.7': resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.7': resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.7': resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.7': resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.7': resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -479,6 +698,143 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -492,6 +848,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -504,6 +863,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1201,79 +1569,66 @@ packages: resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.4': resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.4': resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.4': resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.4': resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.4': resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.4': resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.4': resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.4': resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.4': resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.4': resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.4': resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.4': resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.4': resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} @@ -1305,6 +1660,13 @@ packages: cpu: [x64] os: [win32] + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} + '@supabase/auth-js@2.106.2': resolution: {integrity: sha512-VcAjUErkHkhC5Jaf+g/G1qbkQrFh8edaCdHa7pxJmHUjkWKjT7UnYCtPA89XV0N0GIYRkEqJZw5V62CtOxTmBQ==} engines: {node: '>=20.0.0'} @@ -1355,42 +1717,36 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.15.40': resolution: {integrity: sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-ppc64-gnu@1.15.40': resolution: {integrity: sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] - libc: [glibc] '@swc/core-linux-s390x-gnu@1.15.40': resolution: {integrity: sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==} engines: {node: '>=10'} cpu: [s390x] os: [linux] - libc: [glibc] '@swc/core-linux-x64-gnu@1.15.40': resolution: {integrity: sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.15.40': resolution: {integrity: sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.15.40': resolution: {integrity: sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==} @@ -1647,6 +2003,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} @@ -1744,6 +2103,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1833,6 +2196,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -1871,6 +2238,9 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -1880,6 +2250,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2163,6 +2538,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -2254,6 +2633,11 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + miniflare@4.20260708.1: + resolution: {integrity: sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA==} + engines: {node: '>=22.0.0'} + hasBin: true + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -2348,6 +2732,12 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2604,6 +2994,10 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2641,6 +3035,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2722,6 +3120,13 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unzipper@0.10.14: resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} @@ -2833,9 +3238,36 @@ packages: resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} engines: {node: '>=0.8'} + workerd@1.20260708.1: + resolution: {integrity: sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.110.0: + resolution: {integrity: sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260708.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xlsx@0.18.5: resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} engines: {node: '>=0.8'} @@ -2848,6 +3280,12 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} @@ -2861,84 +3299,207 @@ snapshots: '@babel/runtime@7.29.7': {} + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260708.1 + + '@cloudflare/vite-plugin@1.44.0(vite@7.3.3(@types/node@22.19.19)(jiti@1.21.7))(workerd@1.20260708.1)(wrangler@4.110.0)': + dependencies: + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1) + miniflare: 4.20260708.1 + unenv: 2.0.0-rc.24 + vite: 7.3.3(@types/node@22.19.19)(jiti@1.21.7) + wrangler: 4.110.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - workerd + + '@cloudflare/workerd-darwin-64@1.20260708.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260708.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260708.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260708.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260708.1': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.27.7': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': dependencies: eslint: 9.39.4(jiti@1.21.7) @@ -3041,6 +3602,102 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.2 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3055,6 +3712,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3067,6 +3729,18 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -3841,6 +4515,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.17': {} + '@supabase/auth-js@2.106.2': dependencies: tslib: 2.8.1 @@ -4197,6 +4875,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} + bluebird@3.4.7: {} brace-expansion@1.1.15: @@ -4304,6 +4984,8 @@ snapshots: concat-map@0.0.1: {} + cookie@1.1.1: {} + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -4373,6 +5055,8 @@ snapshots: deep-is@0.1.4: {} + detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} didyoumean@1.2.2: {} @@ -4408,6 +5092,8 @@ snapshots: dependencies: once: 1.4.0 + error-stack-parser-es@1.0.5: {} + es-errors@1.3.0: {} esbuild@0.27.7: @@ -4439,6 +5125,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -4713,6 +5428,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@4.1.5: {} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -4783,6 +5500,18 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + miniflare@4.20260708.1: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.28.0 + workerd: 1.20260708.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -4863,6 +5592,10 @@ snapshots: path-parse@1.0.7: {} + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5131,6 +5864,37 @@ snapshots: setimmediate@1.0.5: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5168,6 +5932,8 @@ snapshots: tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5266,6 +6032,12 @@ snapshots: undici-types@6.21.0: {} + undici@7.28.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unzipper@0.10.14: dependencies: big-integer: 1.6.52 @@ -5361,8 +6133,34 @@ snapshots: word@0.3.0: {} + workerd@1.20260708.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260708.1 + '@cloudflare/workerd-darwin-arm64': 1.20260708.1 + '@cloudflare/workerd-linux-64': 1.20260708.1 + '@cloudflare/workerd-linux-arm64': 1.20260708.1 + '@cloudflare/workerd-windows-64': 1.20260708.1 + + wrangler@4.110.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260708.1 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260708.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrappy@1.0.2: {} + ws@8.21.0: {} + xlsx@0.18.5: dependencies: adler-32: 1.3.1 @@ -5377,6 +6175,19 @@ snapshots: yocto-queue@0.1.0: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.17 + cookie: 1.1.1 + youch-core: 0.3.3 + zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 diff --git a/src/App.tsx b/src/App.tsx index ed33a5a..6add8b8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,6 +44,7 @@ const MyEvents = lazy(() => import("./pages/events/MyEvents")); const CourseManagement = lazy(() => import("./pages/courses/CourseManagement")); const MyCourses = lazy(() => import("./pages/courses/MyCourses")); const SubmissionManagement = lazy(() => import("./pages/hr/SubmissionManagement")); +const HRDashboard = lazy(() => import("./pages/hr/Dashboard")); const Birthdays = lazy(() => import("./pages/admin/Birthdays")); const TrainerManagement = lazy(() => import("./pages/trainers/TrainerManagement")); const IndividualCompetition = lazy(() => import("./pages/ethics/IndividualCompetition")); @@ -66,6 +67,7 @@ const FollowUpManagement = lazy(() => import("./pages/admin/FollowUpManagement") const UnderFollowUp = lazy(() => import("./pages/supervisor/UnderFollowUp")); const LogForVolunteer = lazy(() => import("./pages/supervisor/LogForVolunteer")); const VolunteerPortal = lazy(() => import("./pages/VolunteerPortal")); +const Kiosk = lazy(() => import("./pages/Kiosk")); const NotFound = lazy(() => import("./pages/NotFound")); const ExecutiveDashboard = lazy(() => import("./pages/executive/Dashboard")); const AboutProject = lazy(() => import("./pages/AboutProject")); @@ -228,6 +230,7 @@ function AppRoutes() { } /> {/* HR Routes */} + } /> } /> } /> @@ -273,6 +276,7 @@ function AppRoutes() { {/* Public Volunteer Portal — no auth required */} } /> + } /> } /> diff --git a/src/components/courses/CourseSchedule.tsx b/src/components/courses/CourseSchedule.tsx index 68c1c81..9edbc0f 100644 --- a/src/components/courses/CourseSchedule.tsx +++ b/src/components/courses/CourseSchedule.tsx @@ -72,7 +72,12 @@ const DAYS_LABELS: Record = { const HEAD_ROLES = ['admin', 'supervisor', 'head_production', 'head_fourth_year', 'head_events', 'head_caravans', 'committee_leader']; -export default function CourseSchedule() { +interface CourseScheduleProps { + isKiosk?: boolean; + branchId?: string; +} + +export default function CourseSchedule({ isKiosk = false, branchId }: CourseScheduleProps) { const { primaryRole, user } = useAuth(); const { isRTL, language } = useLanguage(); const { activeBranch, canViewAllBranches } = useBranch(); @@ -85,12 +90,13 @@ export default function CourseSchedule() { const [currentMonth, setCurrentMonth] = useState(new Date()); const canViewDetails = true; // Everyone can view details now, but content varies - const isHead = HEAD_ROLES.includes(primaryRole || ''); + const isHead = isKiosk ? false : HEAD_ROLES.includes(primaryRole || ''); const locale = language === 'ar' ? ar : enUS; useEffect(() => { - if (!user?.id) return; - const cacheKey = `rtc_course_schedule_${user.id}_${activeBranch?.id || 'all'}`; + if (!isKiosk && !user?.id) return; + const cacheId = isKiosk ? (branchId || 'kiosk') : (user?.id || 'anonymous'); + const cacheKey = `rtc_course_schedule_${cacheId}_${isKiosk ? (branchId || 'all') : (activeBranch?.id || 'all')}`; const cached = localStorage.getItem(cacheKey); if (cached) { try { @@ -103,7 +109,7 @@ export default function CourseSchedule() { } } fetchData(!!cached); - }, [user?.id, activeBranch?.id]); + }, [user?.id, activeBranch?.id, isKiosk, branchId]); const fetchData = async (hasCache = false) => { if (!hasCache) { @@ -118,8 +124,9 @@ export default function CourseSchedule() { setCourses(coursesList); setCircles(circlesList); - if (user?.id) { - const cacheKey = `rtc_course_schedule_${user.id}_${activeBranch?.id || 'all'}`; + if (isKiosk || user?.id) { + const cacheId = isKiosk ? (branchId || 'kiosk') : (user?.id || 'anonymous'); + const cacheKey = `rtc_course_schedule_${cacheId}_${isKiosk ? (branchId || 'all') : (activeBranch?.id || 'all')}`; localStorage.setItem(cacheKey, JSON.stringify({ courses: coursesList, circles: circlesList @@ -139,7 +146,10 @@ export default function CourseSchedule() { .select('*') .order('schedule_time', { ascending: true }); - if (canViewAllBranches && activeBranch?.id) q = (q as any).eq('branch_id', activeBranch.id); + const branchToFilter = isKiosk ? branchId : (canViewAllBranches && activeBranch?.id ? activeBranch.id : undefined); + if (branchToFilter) { + q = q.eq('branch_id', branchToFilter); + } const { data, error } = await q; if (error) throw error; @@ -157,7 +167,10 @@ export default function CourseSchedule() { .select('id, schedule, is_active, teacher_id') .eq('is_active', true); - if (canViewAllBranches && activeBranch?.id) circlesQuery = (circlesQuery as any).eq('branch_id', activeBranch.id); + const branchToFilter = isKiosk ? branchId : (canViewAllBranches && activeBranch?.id ? activeBranch.id : undefined); + if (branchToFilter) { + circlesQuery = circlesQuery.eq('branch_id', branchToFilter); + } const { data: circlesData, error: circlesError } = await circlesQuery; if (circlesError) throw circlesError; diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index a792b3d..c84b764 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -23,7 +23,7 @@ export function AppLayout() {

{user?.role === 'admin' && 'Admin Dashboard'} {user?.role === 'supervisor' && 'Head of Branch Dashboard'} - {user?.role === 'volunteer' && 'Volunteer Portal'} + {user?.role === 'volunteer' && 'Volunteer Platform'}

diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index d971248..eccd4c9 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -230,6 +230,7 @@ export function AppSidebar() { const hrNavItems = [ { title: t('nav.dashboard'), url: '/dashboard', icon: Home }, + { title: isRTL ? 'داشبورد HR' : 'HR Dashboard', url: '/hr/dashboard', icon: BarChart3 }, { title: isRTL ? 'إدارة المشاركات' : 'Submission Management', url: '/hr/submissions', icon: FileCheck }, { title: isRTL ? 'تحت المتابعة' : 'Under Follow-Up', url: '/supervisor/under-follow-up', icon: UserCheck }, { title: t('nav.userManagement'), url: '/admin/users', icon: Users }, @@ -253,6 +254,7 @@ export function AppSidebar() { case 'head_hr': return [ { title: t('nav.dashboard'), url: '/dashboard', icon: Home }, + { title: isRTL ? 'داشبورد HR' : 'HR Dashboard', url: '/hr/dashboard', icon: BarChart3 }, { title: isRTL ? 'إدارة المشاركات' : 'Submission Management', url: '/hr/submissions', icon: FileCheck }, { title: isRTL ? 'تحت المتابعة' : 'Under Follow-Up', url: '/supervisor/under-follow-up', icon: UserCheck }, { title: isRTL ? 'شيت المتابعة' : 'Follow-Up Sheet', url: '/admin/followup', icon: UserCheck }, @@ -476,9 +478,8 @@ export function AppSidebar() { className="h-10 w-10 shrink-0 rounded-lg object-cover" /> {!collapsed && ( -
+
RTC {activeBranch ? (language === 'ar' ? activeBranch.name_ar : activeBranch.name) : 'Mohandseen'} - {t('app.tagline')}
)}
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 13921f8..564c578 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -32,6 +32,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [features, setFeatures] = useState([]); const [isLoading, setIsLoading] = useState(true); + // Ref to track current profile id — avoids stale closure inside onAuthStateChange + const profileIdRef = useRef(null); + const fetchProfile = useCallback(async (userId: string) => { try { const { data: profileData, error: profileError } = await supabase @@ -47,6 +50,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children if (profileData) { setProfile(profileData); + profileIdRef.current = profileData.id; } const { data: rolesData, error: rolesError } = await supabase @@ -132,8 +136,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children setSession(session); setUser(session?.user ?? null); if (session?.user) { - // Only fetch profile if we don't have it or it's different - if (!profile || profile.id !== session.user.id) { + // Use ref to avoid stale closure — check if profile is already loaded for this user + if (!profileIdRef.current || profileIdRef.current !== session.user.id) { fetchProfile(session.user.id); } } @@ -143,11 +147,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children setProfile(null); setRoles([]); setFeatures([]); + profileIdRef.current = null; setIsLoading(false); } else if (event === 'TOKEN_REFRESH_ERROR') { - console.error('Token refresh error occurred'); - // Do not immediately sign out, let the session expire naturally or wait for next action - // but user might need to re-login next time they try an action + console.error('Token refresh error — session may have expired. User will need to log in again on next action.'); + // Do not force sign out — let the session expire naturally. + // Supabase will fire SIGNED_OUT automatically if the refresh truly fails. } // Ensure loading is false after any auth event if it wasn't already diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index 685974d..7687730 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -123,7 +123,7 @@ const translations: Record> = { 'accountCreated': 'Your account has been created successfully.', 'signupError': 'An error occurred during signup.', 'emailAlreadyExists': 'This email is already registered.', - 'volunteerPortal': 'Volunteer Management Portal', + 'volunteerPortal': 'Volunteer Management Platform', 'signIn': 'Sign In', 'signUp': 'Sign Up', 'email': 'Email', @@ -143,7 +143,7 @@ const translations: Record> = { 'dashboard.currentLevel': 'Volunteer Grade', 'dashboard.recentActivity': 'Recent Activity', 'dashboard.logNewActivity': 'Log New Activity', - 'dashboard.verse': '﴿وَمَنْ تَطَوَّعَ خَيْرًا فَإِنَّ اللَّهَ شَاكِرٌ عَلِيمٌ﴾', + 'dashboard.verse': '﴿فَمَن تَطَوَّعَ خَيْرًا فَهُوَ خَيْرٌ لَّهُ ۚ﴾', // Admin Dashboard @@ -277,7 +277,7 @@ const translations: Record> = { // App 'app.name': 'برنامج تسجيل المشاركات', - 'app.tagline': 'Volunteer Portal', + 'app.tagline': 'Volunteer Platform', 'app.language': 'Language', 'theme.toggle': 'Theme', 'theme.light': 'Light', @@ -397,7 +397,7 @@ const translations: Record> = { 'accountCreated': 'تم إنشاء حسابك بنجاح.', 'signupError': 'حدث خطأ أثناء إنشاء الحساب.', 'emailAlreadyExists': 'هذا البريد الإلكتروني مسجل بالفعل.', - 'volunteerPortal': 'بوابة إدارة المتطوعين', + 'volunteerPortal': 'منصة إدارة المتطوعين', 'signIn': 'تسجيل الدخول', 'signUp': 'إنشاء حساب', 'email': 'البريد الإلكتروني', @@ -417,7 +417,7 @@ const translations: Record> = { 'dashboard.currentLevel': 'الدرجة التطوعية', 'dashboard.recentActivity': 'النشاط الأخير', 'dashboard.logNewActivity': 'تسجيل مشاركة جديدة', - 'dashboard.verse': '﴿وَمَنْ تَطَوَّعَ خَيْرًا فَإِنَّ اللَّهَ شَاكِرٌ عَلِيمٌ﴾', + 'dashboard.verse': '﴿فَمَن تَطَوَّعَ خَيْرًا فَهُوَ خَيْرٌ لَّهُ ۚ﴾', // Admin Dashboard @@ -568,7 +568,7 @@ const LanguageContext = createContext(null); export function LanguageProvider({ children }: { children: ReactNode }) { const [language, setLanguageState] = useState(() => { const saved = localStorage.getItem('rtc-language'); - return (saved as Language) || 'en'; + return (saved as Language) || 'ar'; }); const setLanguage = (lang: Language) => { diff --git a/src/integrations/supabase/client.ts b/src/integrations/supabase/client.ts index 080ab04..a4aaed7 100644 --- a/src/integrations/supabase/client.ts +++ b/src/integrations/supabase/client.ts @@ -5,45 +5,108 @@ import type { Database } from './types'; const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL; const SUPABASE_PUBLISHABLE_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY; +// Memory fallback for Supabase session storage in case storage APIs fail or throw +const supabaseMemoryStore = new Map(); + // High-performance dynamic storage manager const customStorage = { getItem: (key: string) => { - // 1. Try to read from fast client-side web storage first - let val = localStorage.getItem(key) || sessionStorage.getItem(key); + // 1. Try memory store first + const memVal = supabaseMemoryStore.get(key); + if (memVal) return memVal; + + // 2. Try to read from fast client-side web storage + let val: string | null = null; + try { + val = localStorage.getItem(key) || sessionStorage.getItem(key); + } catch (e) { + console.warn('[Supabase Client] Failed to read web storage:', e); + } - // 2. If not found, check if there is an active session in cookies (migration fallback) + // 3. If not found, check if there is an active session in cookies (migration fallback) if (!val) { - val = Cookies.get(key) || null; - if (val) { - // Migrate to standard web storage immediately to bypass the 4KB cookie limit - const rememberMe = localStorage.getItem('rememberMe') !== 'false'; - if (rememberMe) { - localStorage.setItem(key, val); - } else { - sessionStorage.setItem(key, val); + try { + val = Cookies.get(key) || null; + if (val) { + // Sync to memory + supabaseMemoryStore.set(key, val); + + // Migrate to standard web storage immediately to bypass the 4KB cookie limit + let rememberMe = true; + try { + rememberMe = localStorage.getItem('rememberMe') !== 'false'; + } catch (e) { + // ignore + } + + try { + if (rememberMe) { + localStorage.setItem(key, val); + } else { + sessionStorage.setItem(key, val); + } + } catch (e) { + console.warn('[Supabase Client] Failed to write fallback session to storage:', e); + } + // Clean up the cookie to prevent sending heavy auth payloads in request headers + Cookies.remove(key); } - // Clean up the cookie to prevent sending heavy auth payloads in request headers - Cookies.remove(key); + } catch (e) { + console.warn('[Supabase Client] Failed to access cookies:', e); } } return val; }, setItem: (key: string, value: string) => { - const rememberMe = localStorage.getItem('rememberMe') !== 'false'; - if (rememberMe) { - localStorage.setItem(key, value); - sessionStorage.removeItem(key); // Ensure clean separation - } else { - sessionStorage.setItem(key, value); - localStorage.removeItem(key); // Ensure clean separation + // Always store in memory for maximum reliability + supabaseMemoryStore.set(key, value); + + // IMPORTANT: Default to localStorage (persistent) to avoid race conditions where + // rememberMe hasn't been written yet during initial login flow (token refresh, INITIAL_SESSION events). + // Only switch to sessionStorage when the user has *explicitly* chosen not to be remembered. + let rememberMe = true; + try { + const stored = localStorage.getItem('rememberMe'); + // Only switch to session-only if explicitly set to 'false' + if (stored === 'false') { + rememberMe = false; + } + } catch (e) { + // If localStorage is unavailable, default to persistent memory only } + + try { + if (rememberMe) { + localStorage.setItem(key, value); + sessionStorage.removeItem(key); // Ensure clean separation + } else { + sessionStorage.setItem(key, value); + localStorage.removeItem(key); // Ensure clean separation + } + } catch (e) { + console.warn('[Supabase Client] Failed to write session to storage:', e); + } + // Always clean up cookies to prevent duplicate state or header size warnings - Cookies.remove(key); + try { + Cookies.remove(key); + } catch (e) { + // ignore + } }, removeItem: (key: string) => { - localStorage.removeItem(key); - sessionStorage.removeItem(key); - Cookies.remove(key); + supabaseMemoryStore.delete(key); + try { + localStorage.removeItem(key); + sessionStorage.removeItem(key); + } catch (e) { + console.warn('[Supabase Client] Failed to remove session from storage:', e); + } + try { + Cookies.remove(key); + } catch (e) { + // ignore + } }, }; diff --git a/src/main.tsx b/src/main.tsx index d4a0b1e..a344076 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,103 @@ +// ─── Polyfill/Safe Storage wrapper to prevent quota exceeded or security/incognito crashes ─── +(function safeStoragePolyfill() { + const memoryFallback = new Map>(); + + const getMemoryMap = (storage: any): Map => { + let map = memoryFallback.get(storage); + if (!map) { + map = new Map(); + memoryFallback.set(storage, map); + } + return map; + }; + + const createMemoryStorage = () => { + const store = new Map(); + const storageInstance = { + getItem: (key: string) => store.get(key) || null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + clear: () => store.clear(), + key: (index: number) => Array.from(store.keys())[index] || null, + get length() { return store.size; } + }; + memoryFallback.set(storageInstance, store); + return storageInstance; + }; + + let storageAccessible = false; + try { + const testKey = '__storage_test__'; + window.localStorage.setItem(testKey, testKey); + window.localStorage.removeItem(testKey); + storageAccessible = true; + } catch (e) { + storageAccessible = false; + } + + if (!storageAccessible) { + console.warn('[Storage Polyfill] Web Storage is disabled or throws on access. Redefining on window.'); + try { + Object.defineProperty(window, 'localStorage', { + value: createMemoryStorage(), + configurable: true, + enumerable: true, + writable: true + }); + Object.defineProperty(window, 'sessionStorage', { + value: createMemoryStorage(), + configurable: true, + enumerable: true, + writable: true + }); + } catch (err) { + console.error('[Storage Polyfill] Failed to redefine storage on window:', err); + } + } else { + // Storage is accessible, but might throw on quota limits later. Patch prototype. + try { + const originalGetItem = Storage.prototype.getItem; + Storage.prototype.getItem = function (key: string): string | null { + try { + return originalGetItem.call(this, key); + } catch (e) { + return getMemoryMap(this).get(key) || null; + } + }; + + const originalSetItem = Storage.prototype.setItem; + Storage.prototype.setItem = function (key: string, value: string): void { + try { + originalSetItem.call(this, key, value); + } catch (e) { + console.warn('[Storage Polyfill] setItem failed (quota exceeded?), falling back to memory:', e); + getMemoryMap(this).set(key, value); + } + }; + + const originalRemoveItem = Storage.prototype.removeItem; + Storage.prototype.removeItem = function (key: string): void { + try { + originalRemoveItem.call(this, key); + } catch (e) { + getMemoryMap(this).delete(key); + } + }; + + const originalClear = Storage.prototype.clear; + Storage.prototype.clear = function (): void { + try { + originalClear.call(this); + } catch (e) { + getMemoryMap(this).clear(); + } + }; + } catch (err) { + console.error('[Storage Polyfill] Failed to patch Storage prototype:', err); + } + } +})(); + import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App.tsx"; diff --git a/src/pages/AboutProject.tsx b/src/pages/AboutProject.tsx index dac43d0..16d215e 100644 --- a/src/pages/AboutProject.tsx +++ b/src/pages/AboutProject.tsx @@ -1,5 +1,5 @@ import { useLanguage } from '@/contexts/LanguageContext'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -15,8 +15,14 @@ import { BookOpen, Sparkles, Globe, - ImageIcon, Building2, + Trophy, + Smartphone, + UserCheck, + LayoutDashboard, + ArrowRight, + ArrowLeft, + CheckCircle2, } from 'lucide-react'; import logo from '@/assets/logo.png'; @@ -77,279 +83,304 @@ const techStack = [ { name: 'Edge Functions', desc_ar: 'دوال الخادم', desc_en: 'Server Functions', color: 'from-teal-500 to-green-600' }, ]; -// Photo placeholder component -function PhotoPlaceholder({ label, className = '' }: { label: string; className?: string }) { - return ( -
- - {label} -
- ); -} - export default function AboutProject() { const { language, isRTL } = useLanguage(); const isAr = language === 'ar'; + const [imgError, setImgError] = useState(false); return (
- {/* Hero */} -
+ {/* Premium Hero Header */} +
-
-
-
-
- RTC Logo +
+
+ +
+
+
+
+ RTC Logo
-

- {isAr ? 'منصة RTC' : 'RTC Platform'} -

-

- {isAr - ? 'نظام متكامل لإدارة المتطوعين، الأنشطة، الكورسات، الفعاليات، وتتبع مشاركات الأعضاء' - : 'A comprehensive system for managing volunteers, activities, courses, events, and tracking member participation'} -

-
- - - React + TypeScript - - - - Supabase - - - - {isAr ? 'متعدد اللغات' : 'Multilingual'} - + +
+

+ {isAr ? 'منصة RTC' : 'RTC Platform'} +

+

+ {isAr + ? 'نظام متكامل لإدارة المتطوعين، الأنشطة، الكورسات، الفعاليات، وتتبع مشاركات الأعضاء وصناعة الأثر الخيري.' + : 'A comprehensive system for managing volunteers, activities, courses, events, and tracking member participation.'} +

+
- {/* Tabs */} -
+ {/* Tabs / Navigation */} +
- - + + - {isAr ? 'الفريق' : 'Team'} + {isAr ? 'قصة المنصة وفريقها' : 'Our Story & Team'} - - - {isAr ? 'كيفية الاستخدام' : 'How to Use'} + + + {isAr ? 'كيفية الاستخدام' : 'User Manual'} - - - {isAr ? 'في حالة توفاني الله' : 'If I\'m Gone'} + + + {isAr ? 'المطورين والمستندات' : 'Developer Docs'} {/* ══════════════════════════════════════ - TAB 1 — TEAM + TAB 1 — STORY & TEAM ══════════════════════════════════════ */} - - - {/* Project Story */} -
-
-
- + + + {/* Story Section */} +
+
+
+
-

{isAr ? 'قصة المشروع' : 'Project Story'}

+

{isAr ? 'قصة وتأسيس المشروع' : 'The Genesis Story'}

- - - + +

- تم إنشاء هذا المشروع لخدمة نشاط RTC الخيري التابع لجمعية رسالة، - بهدف تنظيم وتسهيل إدارة شؤون المتطوعين والعمليات الداخلية بدل الاعتماد على الطرق العشوائية أو المتابعة اليدوية. + تم إنشاء هذا المشروع لخدمة نشاط RTC الخيري التابع لجمعية رسالة، بهدف تنظيم وتسهيل إدارة شؤون المتطوعين والعمليات الداخلية بدل الاعتماد على الطرق العشوائية أو المتابعة اليدوية.

- {/* Image placeholder #1 */} - + {/* Core Value Pillars Grid */} +
+
+
+ +
+
+

جمع البيانات بشكل أدق

+

تسجيل منظم وتوثيقي لجميع مسارات المتطوع والأنشطة والنسب الشهرية دون ضياع للجهود.

+
+
-

- جاءت فكرة المشروع من الحاجة إلى نظام واضح ومنظم يساعد فريق العمل على: -

-
    -
  • جمع البيانات بشكل أدق
  • -
  • تسهيل إدارة المتطوعين
  • -
  • تقليل الوقت والمجهود المبذول في المتابعة
  • -
  • التركيز أكثر على الهدف الأساسي وهو خدمة الناس وصناعة أثر حقيقي
  • -
+
+
+ +
+
+

تسهيل إدارة المتطوعين

+

منصة رقمية موحدة تمنح كل متطوع القدرة على تسجيل مشاركاته بنفسه ومتابعة تقدمه.

+
+
+ +
+
+ +
+
+

تقليل الوقت والمجهود

+

أتمتة حساب نسب الحضور، والdeficit، وتارجت الشهر، وعمليات التصدير للإكسيل بنقرة واحدة.

+
+
- {/* Image placeholder #2 */} - +
+
+ +
+
+

التركيز على صناعة الأثر

+

توفير الجهد الإداري الضخم ليوجه مباشرة لخدمة المستفيدين وتطوير جودة العمل الخيري.

+
+
+

- هذا المشروع لم يتم إنشاؤه كمجرد تدريب تقني أو إضافة للسيرة الذاتية، - بل بُنيَ بنية أن يكون صدقة جارية، يستمر نفعها مع الوقت، - ويساهم ولو بجزء بسيط في دعم العمل الخيري وتنظيمه وتطويره. + هذا المشروع لم يتم إنشاؤه كمجرد تدريب تقني أو إضافة للسيرة الذاتية، بل بُنيَ بنية أن يكون صدقة جارية، يستمر نفعها مع الوقت، ويساهم ولو بجزء بسيط في دعم العمل الخيري وتنظيمه وتطويره.

- كل سطر كود في هذا المشروع كُتب على أمل أن يكون سببًا في تسهيل الخير، - ومساعدة من يعملون لأجل الناس دون مقابل. + كل سطر كود في هذا المشروع كُتب على أمل أن يكون سببًا في تسهيل الخير، ومساعدة من يعملون لأجل الناس دون مقابل.

-
+
﴿وَمَا تُقَدِّمُوا لِأَنفُسِكُم مِّنْ خَيْرٍ تَجِدُوهُ عِندَ اللَّهِ﴾ 🤍
-

- ولا يفوتني في هذا المقام أن أتقدم بخالص الشكر والتقدير لزميلي وصديقي، - خير الصديق إياد جابر سعد الدين جابر، - على دعمه ومساندته الحقيقية طوال فترة العمل على المشروع، - فلولاه – بعد فضل الله – ما كان لهذا المشروع أن يخرج إلى النور. +

+ ولا يفوتني في هذا المقام أن أتقدم بخالص الشكر والتقدير لزميلي وصديقي، خير الصديق إياد جابر سعد الدين جابر، على دعمه ومساندته الحقيقية طوال فترة العمل على المشروع، فلولاه – بعد فضل الله – ما كان لهذا المشروع أن يخرج إلى النور.

-
- {/* Developers */} -
-
-
- + {/* Developer Profiles */} +
+
+
+
-

{isAr ? 'فريق التطوير' : 'Development Team'}

+

{isAr ? 'فريق التطوير' : 'Development Team'}

- - -
- {/* Developer 1 — Omar */} -
- +
+ {/* Developer 1 - Omar */} + +
+ +
+
+
+ ع +
+
+
-

عمر

-

مطوّر البرمجيات

- - - @Omar-0O - +

{isAr ? 'عمر' : 'Omar'}

+

{isAr ? 'مطوّر البرمجيات الرئيسي' : 'Lead Software Developer'}

+
+

+ {isAr ? 'مسؤول عن بناء بنية النظام التقنية والربط مع قاعدة البيانات وتطوير الواجهات.' : 'Responsible for system architecture, database integrations, and core UI development.'} +

+
- - {/* Developer 2 — Eyad */} -
- + + + + {/* Developer 2 - Eyad */} + +
+ +
+
+
+ إ +
+
+
-

إياد جابر

-

مطوّر البرمجيات

+

{isAr ? 'إياد جابر' : 'Eyad Jaber'}

+

{isAr ? 'مطوّر برمجيات وشريك التأسيس' : 'Software Developer & Co-Founder'}

+
+

+ {isAr ? 'شريك في تصميم وبناء المنصة، مراجعة العمليات، وتقديم الدعم الإداري والتقني الكامل.' : 'Co-designed the platform workflows, reviewed requirements, and provided full technical support.'} +

+
+ + {isAr ? 'مطور البرمجيات' : 'Software Developer'} +
-
- - {/* Team photo placeholder */} -
- -
-
- + + +
- {/* RTC Mohandseen Branch — Supporters */} -
-
-
- + {/* Supporters / Branch */} +
+
+
+
-

{isAr ? 'نشاط RTC المهندسين' : 'RTC Mohandseen Branch'}

+

{isAr ? 'نشاط RTC المهندسين' : 'RTC Mohandseen Supporters'}

- - - + +

- نشاط RTC المهندسين هو النشاط - الذي احتضن هذا المشروع وأعطاه سبب وجوده. وقد كان لفريق النشاط دور محوري في دعم فكرة المشروع - منذ بداياتها الأولى. + نشاط RTC المهندسين هو الكيان الخيري والفرع الرائع الذي احتضن هذا المشروع وأعطاه سبب وجوده. وقد كان لفرق هذا النشاط الدور المحوري في دعم فكرة النظام وتجريبه منذ بداياته الأولى.

- {/* Image placeholder — Branch */} - -

- أصحاب الفضل من فريق النشاط الذين آمنوا بالفكرة، وصبروا على فريق التطوير طوال مراحل البناء - والاختبار والتعديل، ولم يبخلوا يومًا بملاحظة أو فكرة أو تشجيع. + أصحاب الفضل من النشاط الذين آمنوا بالفكرة وصبروا طوال مراحل البناء والاختبار، ولم يبخلوا يومًا بملاحظة أو تشجيع:

- {/* Supporters placeholder grid — يمكن استبدالها بأسماء حقيقية */} -
- {[ - { label: 'صورة + اسم — الداعم الأول' }, - { label: 'صورة + اسم — الداعم الثاني' }, - { label: 'صورة + اسم — الداعم الثالث' }, - ].map((item, i) => ( -
- -
-
-
- ))} + {/* Team Group Photo Container */} +
+
+ {!imgError ? ( + RTC Team Group Photo setImgError(true)} + /> + ) : ( +
+
+ RTC +
+ {isAr ? 'الصورة الجماعية لإدارة نشاط RTC المهندسين' : 'RTC Mohandseen Management Group Photo'} + (/src/assets/group-photo.jpg) +
+ )} +
- {/* Second image placeholder */} - - -

- جزاكم الله خيرًا على كل لحظة دعم وصبر 🤍 +

+ جزاكم الله خيرًا على كل لحظة دعم وصبر وجعلها في ميزان حسناتكم 🤍

-
- {/* ══════════════════════════════════════ - TAB 2 — HOW TO USE + TAB 2 — USER MANUAL ══════════════════════════════════════ */} - - - {/* Video */} -
-
-
- + + + {/* Tutorial Video Section */} +
+
+
+
-

{isAr ? 'فيديو شرح المنصة' : 'Platform Tutorial Video'}

+

{isAr ? 'فيديو دليل المنصة' : 'Platform Video Guide'}

- - -
-
- -
-
-

- {isAr ? 'فيديو شرح المنصة' : 'Platform Tutorial Video'} -

-

+ + + +

+ +
+
+
+

{isAr ? 'شرح فيديو عملي متكامل' : 'Step-by-Step Video walkthrough'}

+

{isAr - ? 'شاهد الفيديو لتتعلم كيفية استخدام جميع مميزات المنصة خطوة بخطوة' - : 'Watch the video to learn how to use all platform features step by step'} + ? 'قمنا بتسجيل فيديو توضيحي يشرح كيفية إضافة المشاركات، إدارتها، وتصدير التقارير لجميع فئات المستخدمين.' + : 'A comprehensive walkthrough tutorial demonstrating how to use the platform as a volunteer, supervisor, or HR.'}

-
+ @@ -358,135 +389,216 @@ export default function AboutProject() {
+ {/* Guides for Roles */} +
+
+
+ +
+

{isAr ? 'دليل المستخدم حسب الصلاحيات' : 'Role-Based Instructions'}

+
+
+ {/* Volunteer Guide */} + + +
+ +
+ {isAr ? '1. للمتطوعين' : '1. For Volunteers'} + + {isAr ? 'تسجيل ومتابعة النشاط الفردي' : 'Track and submit activities'} + +
+ +
+ + {isAr ? 'الدخول عبر الرابط الشخصي المخصص لك.' : 'Access via your unique personal link.'} +
+
+ + {isAr ? 'تسجيل المشاركة بتحديد اللجنة وتاريخ العمل.' : 'Submit participations selecting committee & date.'} +
+
+ + {isAr ? 'متابعة نقاطك وحالة المشاركات (معلق/مقبول).' : 'Monitor points and approval states.'} +
+
+
+ + {/* Supervisor Guide */} + + +
+ +
+ {isAr ? '2. للمشرفين ومسؤولي الفروع' : '2. For Supervisors'} + + {isAr ? 'إدارة حضور اللجان والأنشطة' : 'Manage branch committees'} + +
+ +
+ + {isAr ? 'تسجيل المشاركات الجماعية لمتطوعي لجان فرعك.' : 'Log group participations for branch volunteers.'} +
+
+ + {isAr ? 'إدارة وتحديث بيانات حلقات القرآن وحضور الكورسات.' : 'Manage Quran circles & course attendance sheets.'} +
+
+ + {isAr ? 'اعتماد أو رفض طلبات المشاركة المعلقة.' : 'Approve or reject pending volunteer requests.'} +
+
+
+ + {/* HR Guide */} + + +
+ +
+ {isAr ? '3. لمسؤولي الـ HR' : '3. For HR Teams'} + + {isAr ? 'تتبع التارجت الشهري والعجز' : 'Monitor monthly deficit targets'} + +
+ +
+ + {isAr ? 'متابعة نسب حضور وتفاعل متطوعي الفرع.' : 'Track volunteer targets and active statistics.'} +
+
+ + {isAr ? 'حساب deficit المشاركات الشهري تلقائياً.' : 'Compute monthly deficits automatically.'} +
+
+ + {isAr ? 'إرسال تنبيهات التذكير بالواتساب وتصدير التقارير.' : 'Send reminder WhatsApp messages & export data.'} +
+
+
+
+
{/* ══════════════════════════════════════ - TAB 3 — في حالة توفاني الله + TAB 3 — TECHNICAL / DEVS ══════════════════════════════════════ */} - - - {/* Intro */} - - -
- -

في حالة توفاني الله

+ + + {/* Ongoing Charity Box (In case I am gone) */} + +
+ +
+ +

في حالة توفاني الله

-

- هذه الصفحة موجّهة لأي مطوّر يرغب في الاستمرار بتشغيل هذا المشروع أو الانطلاق منه لبناء نظام مشابه. - الهدف الأساسي هو ضمان استمرار هذه الصدقة الجارية وعدم توقّفها بتوقّف أصحابها. +

+ هذه الصفحة والملف موجّه لأي مطوّر يرغب في الاستمرار بتشغيل هذا المشروع أو الانطلاق منه لبناء نظام مشابه. الهدف الأساسي هو ضمان استمرار هذه الصدقة الجارية وعدم توقّفها بتوقّف أصحابها.

- جميع ما تحتاجه موجود في هذا الريبو: - قاعدة البيانات كاملة عبر الـ Migrations، - وكود الواجهة، والـ Edge Functions، والإعدادات — لا شيء مفقود. + جميع ما تحتاجه موجود في هذا المستودع (GitHub Repository): قاعدة البيانات كاملة عبر ملفات الـ Migrations، وكود الواجهة البرمجية، والـ Edge Functions، والإعدادات — لا شيء مفقود.

-
+
﴿وَمَا تُقَدِّمُوا لِأَنفُسِكُم مِّنْ خَيْرٍ تَجِدُوهُ عِندَ اللَّهِ﴾ 🤍
- {/* Fork & Setup Guide */} -
-
-
- + {/* Step-by-step installation setup guide */} +
+
+
+
-

- {isAr ? 'خطوات نسخ المشروع وإعداد داتابيز جديدة' : 'Fork & Setup a New Database'} +

+ {isAr ? 'خطوات استنساخ وتشغيل المشروع' : 'Clone & Setup Guide'}

- {/* YouTube link */} - - -
-
- -
-
-

فيديو شرح كيفية النسخ والإعداد

-

شاهد الفيديو لفهم الخطوات بشكل تفصيلي قبل البدء

-
- -
-
-
- + {/* video callout */} +
+
+ +

{isAr ? 'فيديو شرح تفصيلي لنسخ وتشغيل الداتابيز' : 'Detailed video guide on how to clone & run database migrations'}

+
+ +
-
+ {/* Migration Steps Grid */} +
{migrationSteps.map((s) => ( - - + + - + {s.step} - {isAr ? s.title_ar : s.title_en} + {isAr ? s.title_ar : s.title_en} -
-                        {s.code}
-                      
+
+
+                          {s.code}
+                        
+
))}
-
- {/* Tech Stack */} -
-
-
- + {/* Tech Stack List */} +
+
+
+
-

{isAr ? 'التقنيات المستخدمة في المشروع' : 'Tech Stack'}

+

{isAr ? 'البنية التكنولوجية للمشروع' : 'Technological Architecture'}

-

- إليك قائمة بكل التقنيات المستخدمة لتساعدك على فهم البنية التقنية للمشروع قبل البدء. -

-
+ +
{techStack.map((tech, i) => (
-
-
-

{tech.name}

-

{isAr ? tech.desc_ar : tech.desc_en}

+
+
+

{tech.name}

+

{isAr ? tech.desc_ar : tech.desc_en}

))}
-
diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index e6f8d9c..63fe502 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -32,19 +32,79 @@ export default function Auth() { const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - setIsLoading(true); // Clean input const cleanEmail = email.trim(); + const cleanPassword = password.trim(); + + if (!cleanEmail) { + toast({ + title: t('error'), + description: isRTL ? 'يرجى إدخال البريد الإلكتروني أو اسم الفرع' : 'Please enter your email or branch name', + variant: 'destructive', + }); + return; + } + + setIsLoading(true); console.log('Attempting login with:', cleanEmail); + // If password is empty, attempt branch kiosk shortcut + if (!cleanPassword) { + try { + const { data: branches, error: branchError } = await supabase + .from('branches') + .select('*'); + + if (!branchError && branches) { + const matchedBranch = branches.find(b => + b.name?.toLowerCase().trim() === cleanEmail.toLowerCase() || + b.name_ar?.toLowerCase().trim() === cleanEmail.toLowerCase() || + b.name?.toLowerCase().includes(cleanEmail.toLowerCase()) || + b.name_ar?.toLowerCase().includes(cleanEmail.toLowerCase()) + ); + + if (matchedBranch) { + localStorage.setItem('rtc_kiosk_branch_id', matchedBranch.id); + + toast({ + title: isRTL ? 'تم الدخول كشاشة تفاعلية' : 'Kiosk Access Granted', + description: isRTL + ? `تم توجيهك إلى شاشة فرع ${matchedBranch.name_ar || matchedBranch.name}` + : `Redirecting to Kiosk for ${matchedBranch.name} branch`, + }); + + navigate('/kiosk'); + setIsLoading(false); + return; + } + } + } catch (err) { + console.error('Error in branch login shortcut:', err); + } + + toast({ + title: t('error'), + description: isRTL + ? 'يرجى إدخال كلمة المرور أو اسم فرع صحيح' + : 'Please enter a password or a valid branch name', + variant: 'destructive', + }); + setIsLoading(false); + return; + } + // Save remember me preference before login - localStorage.setItem('rememberMe', String(rememberMe)); + try { + localStorage.setItem('rememberMe', String(rememberMe)); + } catch (e) { + console.warn('Failed to save rememberMe preference:', e); + } try { const { data, error } = await supabase.auth.signInWithPassword({ email: cleanEmail, - password: password, + password: cleanPassword, }); if (error) { @@ -120,16 +180,17 @@ export default function Auth() { {t('auth.loginSubtitle')} -
+
- + setEmail(e.target.value)} - required />
@@ -141,8 +202,6 @@ export default function Auth() { placeholder="••••••••" value={password} onChange={(e) => setPassword(e.target.value)} - required - minLength={6} className="ltr:pr-10 rtl:pl-10" /> +
+
+
+
+ ); + } + + const currentBranchName = branches.find(b => b.id === selectedBranchId)?.[isRTL ? 'name_ar' : 'name'] || ''; + + return ( +
+ {/* Header Section */} +
+
+
+ +
+
+ +
+
+

+ {isRTL ? 'تسجيل مشاركات الميداني' : 'Field Participation Logging'} +

+ + + + + +

+ {isRTL ? 'تغيير الفرع الحالي:' : 'Change Current Branch:'} +

+
+ {branches.map((b) => ( + + ))} +
+
+
+
+
+ + {/* Language Switcher */} +
+ +
+
+ + + + {/* Main Grid Section */} +
+ + {/* Left Column: Logging Form */} + + +
+
+ +
+
+ {isRTL ? 'تسجيل مشاركة جديدة بالميداني' : 'Log New Field Participation'} + + {isRTL ? 'تحقق من رقم هاتفك لتسجيل مشاركتك' : 'Verify your phone number to log your participation'} + +
+
+
+ + + + + {/* Phone Lookup Field */} +
+ +
+
+ +
+ +
+
+ + {/* Conditional fields displayed once phone check is done */} + {hasSearched && ( +
+ + {/* Account Found Details */} + {volunteer ? ( +
+
+

+ {volunteer.full_name_ar || volunteer.full_name} +

+
+ + + + {(volunteer.full_name_ar || volunteer.full_name)?.charAt(0).toUpperCase() || 'U'} + + +
+ ) : ( +
+
+

+ {isRTL ? 'الرقم غير مسجل كمتطوع' : 'Phone not registered as volunteer'} +

+
+ +
+ + setGuestName(e.target.value)} + className="h-12 text-base px-4 border-2 hover:border-primary/50 transition-colors bg-background" + disabled={submitting} + required + /> +
+
+ )} + + {/* Committee Selection */} +
+ + +
+ + {/* Activity Date */} +
+ + + + + + + { + if (date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + setActivityDate(`${year}-${month}-${day}`); + setIsCalendarOpen(false); + } + }} + disabled={(date) => date > new Date() || date < new Date("1900-01-01")} + initialFocus + /> + + +
+ + {/* Activity Type */} +
+ + + {selectedActivity?.description && ( +
+ +

+ {isRTL ? selectedActivity.description_ar : selectedActivity.description} +

+
+ )} +
+ + {/* Wore Vest Switch */} +
+
+
+ +
+
+ + {!isRTL && ( +

+ Earn extra impact points by wearing the official activity vest +

+ )} +
+
+ +
+ + {/* Description */} +
+ +