diff --git a/web/Miqaat/.gitignore b/web/Miqaat/.gitignore new file mode 100644 index 0000000..98400f0 --- /dev/null +++ b/web/Miqaat/.gitignore @@ -0,0 +1,10 @@ + +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ +.env + +.netlify diff --git a/web/Miqaat/README.md b/web/Miqaat/README.md new file mode 100644 index 0000000..ff12530 --- /dev/null +++ b/web/Miqaat/README.md @@ -0,0 +1,165 @@ +# 🌙 ميقات | Miqaat +> Accurate prayer times for Algerian Wilayas. + +[![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://react.dev/) +[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) +[![CSS3](https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white)](https://developer.mozilla.org/en-US/docs/Web/CSS) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white)](https://tailwindcss.com/) +[![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white)](https://reactrouter.com/) + +

+ Miqaat Logo +

+ +

+ Overview • + Live Demo • + Key Features • + Screenshots • + Tech Stack • + Getting Started +

+ +## ✨ Overview +**Miqaat** is a web application designed to provide accurate prayer times specifically for users in Algeria. + +## 🌐 Live Demo +[View App](https://miqaat-eight.vercel.app/) + +## 🚀 Key Features +* 📍 **Dynamic Location Selection**: Easily choose your Wilaya using an interface or by using automatic GPS detection. +* ⏳ **Live Countdown**: Real-time updates and countdowns to the next prayer time. +* 📅 **Comprehensive Calendar**: View weekly and monthly prayer schedules with an elegant, printable layout. +* 🌓 **Dark Mode Support**: Fully responsive design that adapts to your system preferences. +* 🔔 **Smart Notifications**: Frontend-scheduled notifications firing 1 minute before each prayer. +* 📲 **PWA-ready Architecture**: Designed for future offline support and mobile installability. + +## 📸 Screenshots + +### 🏠 Home Dashboard +Comprehensive view of current and upcoming prayer times with a live clock. +

+ Home Screen +

+ +### 📍 Location Selection +an interface for choosing your location via list or GPS. +

+ Location Selection +

+ +### 🗓️ Prayer Calendar +weekly/monthly prayer time calendar. +

+ Calendar View +

+ +## 🛠️ Tech Stack +* **Frontend**: React (React Router v7) +* **Language**: TypeScript +* **Styling**: + - **Tailwind CSS** (utility-first) + - **Custom CSS** (design tokens, shimmer animations, theming system) +* **Icons**: Lucide React +* **Persistence**: LocalStorage for user preferences +* **Deployment**: Optimized production build via Vite + +## 💎 Design System +Miqaat is built on a custom design system that ensures visual consistency and a premium user experience. + +| Token Category | Implementation | +| :--- | :--- | +| **Typography** | `Changa` (Arabic Brand), `DM Sans` (Body), `Cormorant Garamond` (Display) | +| **Color Palette** | Deep Algerian Indigo (`#1A5276`) & Luxurious Gold (`#C9A84C`) | +| **Visual Effects** | Glassmorphism, Shimmering Skeletons, and Custom Glow Shadows | + + +## 🏗️ Project Architecture +The project follows a modular and scalable architecture, separating UI concerns from business logic and data fetching. + +### 🧩 System Overview +```mermaid +graph TD + subgraph UI_Layer [UI Layer] + R[Routes / Pages] --> B[Blocks / Layout Sections] + B --> C[Atomic Components] + end + + subgraph Logic_Layer [Logic & State] + B --> H[Custom Hooks] + H --> CTX[Global Context] + CTX --> LS[(LocalStorage)] + end + + subgraph Data_Layer [Data Layer] + H --> S[API Services] + S --> API[External Adhan API] + S --> D[Static Data / Types] + end + + style UI_Layer fill:#f0f7ff,stroke:#0369a1,stroke-width:2px + style Logic_Layer fill:#f0fdf4,stroke:#15803d,stroke-width:2px + style Data_Layer fill:#fff7ed,stroke:#c2410c,stroke-width:2px +``` + +### 📁 Directory Breakdown +* **`app/routes/`**: Entry points for each page (Home, Settings, Calendar, Location). +* **`app/blocks/`**: Complex, layout-specific sections (e.g., Prayer Grid, Search Header) that compose pages. +* **`app/components/`**: Reusable atomic UI elements (Buttons, Inputs, Skeletons) following a consistent design system. +* **`app/hooks/`**: Encapsulated business logic (GPS tracking, prayer time calculations, countdown timers). +* **`app/context/`**: Global state management using React Context for user preferences and location data. +* **`app/services/`**: Data fetching layer and API clients for external communication. +* **`app/data/`**: Static configuration, types, and constants. +* **`app/i18n/`**: Localization dictionary and logic for Arabic/English support. + + +## 🔌 API Integration +Miqaat leverages the [Adhan DZ API](https://adhan-dz.dexter21767.com) to provide accurate, real-time data specifically for Algeria. + +### Data Fetching +* **Source**: [adhan-dz.dexter21767.com](https://adhan-dz.dexter21767.com) +* **Endpoints**: + - `/cities`: Fetches a comprehensive list of Algerian Wilayas and Baladiyas. + - `/prayerTimes`: Retrieves daily or monthly prayer schedules based on the selected city ID and date range. +* **Implementation**: Logic is centralized in [prayerApi.ts](file:///home/haroune-dev/Desktop/Miqaat/app/services/prayerApi.ts) (raw fetch layer) and [api.ts](file:///home/haroune-dev/Desktop/Miqaat/app/services/api.ts) (application mapping). + +### Key Features +* **Dynamic Data**: Times are fetched dynamically based on the user's selected location. +* **State Sync**: Custom hooks (`usePrayerTimes`) handle loading states, error reporting, and real-time synchronization with the application context. + + +## 🏁 Getting Started + +### Prerequisites +* Node.js (LTS version recommended) +* npm or yarn + +### Installation +1. Clone the repository: + ```bash + git clone https://github.com/Haroune-dev/miqaat.git + ``` +2. Install dependencies: + ```bash + npm install + ``` +3. Start the development server: + ```bash + npm run dev + ``` + +## 🌟 Project Highlights +* **Zero Layout Shift**: Optimized loading skeletons for a premium first-load experience. +* **RTL/LTR Support**: Fully localized interface for Arabic and English users. +* **Performance**: Lightweight bundle with optimized asset delivery. + +## 🔮 Future Improvements +* [ ] Full Offline Data Persistence (Service Worker). +* [ ] Support for Baladiyas (Sub-provinces). +* [ ] Multi-language Expansion (French support). +* [ ] Advanced Analytics & Insights. + +## 👤 Author +**Haroune-dev** + +--- diff --git a/web/Miqaat/app/blocks/__global/footer-information.module.css b/web/Miqaat/app/blocks/__global/footer-information.module.css new file mode 100644 index 0000000..ad934a9 --- /dev/null +++ b/web/Miqaat/app/blocks/__global/footer-information.module.css @@ -0,0 +1,174 @@ +.root { + background-color: #0b1120; /* very dark navy */ + color: #f8fafc; +} + +.brand { + text-decoration: none; + color: #f8fafc; +} + +.brandText { + font-family: var(--family-display); + font-size: 1.75rem; + font-weight: 700; +} + +:global([lang="ar"]) .brandText { + font-family: var(--family-brand-ar); + font-weight: 700; + font-size: 2rem; +} + +.logoImg { + width: 2.25rem; + height: 2.25rem; + border-radius: var(--radius-sm); + object-fit: cover; + background: white; + padding: 2px; +} + +.slogan { + font-size: var(--text-sm); + color: #94a3b8; + opacity: 0.8; + max-width: 500px; + text-align: center; + margin: 0; +} + +.gridTitle { + font-size: 1.1rem; + font-weight: 700; + margin: 0; +} + +.gridLink { + display: flex; + align-items: center; + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-lg); + color: #94a3b8; + text-decoration: none; + transition: all 0.3s; +} + +.gridLink:hover { + background: rgba(255, 255, 255, 0.05); + color: #f8fafc; +} + +.gridLink svg { + color: #94a3b8; + transition: color 0.3s; +} + +.gridLink:hover svg { + color: #f8fafc; +} + +.linkText { + flex: 1; + margin: 0 var(--space-4); + font-weight: 500; + font-size: var(--text-lg); +} + +.chevron { + opacity: 0.5; +} + +.contactDesc { + color: #94a3b8; + font-size: var(--text-md); + margin: 0; + text-align: center; +} + +.contactButton { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-1) var(--space-4); + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-md); + color: #38bdf8; + text-decoration: none; + font-weight: 600; + font-size: var(--text-xs); + transition: all 0.3s; +} + +.contactButton:hover { + background: rgba(56, 189, 248, 0.1); + border-color: #38bdf8; + transform: translateY(-2px); +} + +.boxIcon { + width: 2rem; + height: 2rem; + background: rgba(255, 255, 255, 0.05); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; +} + +.boxIcon svg { + color: #94a3b8; + transition: color 0.3s, transform 0.3s; +} + +.boxIcon:hover { + background: rgba(255, 255, 255, 0.1); +} + +.boxIcon:hover svg { + color: #f8fafc; + transform: scale(1.1); +} + +.copyLine1 { + font-size: var(--text-base); + font-weight: 600; + margin: 0; + color: #f8fafc; + opacity: 0.8; + white-space: nowrap; +} + +.copyLine2 { + font-size: var(--text-sm); + margin: var(--space-1) 0 0; + color: #64748b; + white-space: nowrap; +} + +.scrollTop { + width: 2.25rem; + height: 2.25rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #38bdf8; + cursor: pointer; + transition: all 0.3s; +} + +.scrollTop:hover { + background: rgba(56, 189, 248, 0.1); + transform: translateY(-4px); +} + +@media (max-width: 850px) { + .brandText { + font-size: 2.5rem; + } +} diff --git a/web/Miqaat/app/blocks/__global/footer-information.tsx b/web/Miqaat/app/blocks/__global/footer-information.tsx new file mode 100644 index 0000000..c014236 --- /dev/null +++ b/web/Miqaat/app/blocks/__global/footer-information.tsx @@ -0,0 +1,108 @@ +import { Mail, ChevronRight, ArrowUp } from "lucide-react"; +import { Link } from "react-router"; +import classnames from "classnames"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./footer-information.module.css"; + +export interface FooterInformationProps { + className?: string; +} + +const GithubIcon = ({ size = 20 }: { size?: number }) => ( + + + +); + +const LinkedinIcon = ({ size = 20 }: { size?: number }) => ( + + + +); + +export function FooterInformation({ className }: FooterInformationProps) { + const { t } = useLanguage(); + + const scrollToTop = () => { + if (typeof window !== 'undefined') { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + + return ( +
+
+ {/* Top Header */} +
+ + {t("nav.logo.prayer")} + Logo + +

{t("footer.slogan")}

+
+ + {/* Main Sections */} +
+
+ {/* Links Section */} +
+

{t("footer.links")}

+ +
+ + {/* Contact Section */} +
+

{t("footer.contact")}

+
+

{t("footer.contactDesc")}

+ + + {t("footer.sendMessage")} + +
+
+
+
+ + {/* Bottom Bar */} +
+
+
+ + + + + + + + + +
+
+ +
+

haroune zeraib © 2026

+

{t("footer.subCopy")}

+
+ +
+ +
+
+
+
+ ); +} diff --git a/web/Miqaat/app/blocks/__global/navigation-header.module.css b/web/Miqaat/app/blocks/__global/navigation-header.module.css new file mode 100644 index 0000000..d45744f --- /dev/null +++ b/web/Miqaat/app/blocks/__global/navigation-header.module.css @@ -0,0 +1,152 @@ +.root { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 64px; + padding: var(--space-4) 24px; + background-color: color-mix(in srgb, var(--color-bg-card) 85%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-bottom: 1px solid var(--color-border); + box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.03); + position: sticky; + top: 0; + z-index: 100; +} + +.logo { + display: flex; + align-items: center; + gap: var(--space-3); + text-decoration: none; + color: var(--color-text); +} + +.logoIcon { + display: flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + background: var(--color-primary); + border-radius: var(--radius-md); + color: #fff; + flex-shrink: 0; +} + +.logoText { + font-family: var(--family-display); + font-size: var(--text-2xl); + font-weight: 700; + color: var(--color-primary); + letter-spacing: 0.01em; +} + +:global([lang="ar"]) .logoText { + font-family: var(--family-brand-ar); + font-weight: 700; + font-size: 2.25rem; +} + +.navWrapper { + display: flex; +} + +.nav { + display: flex; + align-items: center; + gap: var(--space-5); +} + +.navLink { + position: relative; + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-sm); + font-size: var(--text-base); + font-weight: 500; + color: var(--color-text-muted); + text-decoration: none; + transition: color 0.3s ease; +} + +.navLink::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0%; + height: 2px; + background-color: var(--color-primary); + transition: width 0.3s ease; + border-radius: 2px; +} + +.navLink:hover { + color: var(--color-text); +} + +.navLink:hover::after { + width: 100%; +} + +.navLinkActive { + color: var(--color-primary); + font-weight: 600; +} + +.navLinkActive::after { + width: 100%; +} + +.menuToggle { + display: none; + background: none; + border: none; + color: var(--color-text); + cursor: pointer; + padding: var(--space-2); + align-items: center; + justify-content: center; +} + +@media (max-width: 768px) { + .menuToggle { + display: flex; + } + + .navWrapper { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--color-bg-card); + border-bottom: 1px solid var(--color-border); + box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); + padding: 0 24px; + display: flex; + flex-direction: column; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; + } + + .navWrapperOpen { + max-height: 300px; + padding: var(--space-4) 24px; + } + + .nav { + flex-direction: column; + align-items: flex-start; + width: 100%; + gap: var(--space-3); + } + + .navLink { + padding: var(--space-3) 0; + width: 100%; + } +} diff --git a/web/Miqaat/app/blocks/__global/navigation-header.tsx b/web/Miqaat/app/blocks/__global/navigation-header.tsx new file mode 100644 index 0000000..570ef79 --- /dev/null +++ b/web/Miqaat/app/blocks/__global/navigation-header.tsx @@ -0,0 +1,68 @@ +import { NavLink, Link, useLocation } from "react-router"; +import { Settings, MapPin, CalendarDays, Menu, X } from "lucide-react"; +import classnames from "classnames"; +import { useState, useEffect } from "react"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./navigation-header.module.css"; + +export interface NavigationHeaderProps { + className?: string; +} + +export function NavigationHeader({ className }: NavigationHeaderProps) { + const { t } = useLanguage(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const location = useLocation(); + + useEffect(() => { + setIsMenuOpen(false); + }, [location]); + + return ( + + ); +} diff --git a/web/Miqaat/app/blocks/calendar/calendar-grid.module.css b/web/Miqaat/app/blocks/calendar/calendar-grid.module.css new file mode 100644 index 0000000..15c627c --- /dev/null +++ b/web/Miqaat/app/blocks/calendar/calendar-grid.module.css @@ -0,0 +1,183 @@ +.root { + width: 100%; +} + +.tableWrapper { + background: var(--color-bg-card); + border-radius: var(--radius-xl); + padding: var(--space-6); + box-shadow: var(--shadow-lg); + overflow-x: auto; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.tableWrapper:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-xl); +} + +.table { + width: 100%; + border-collapse: collapse; + background: transparent; + font-size: var(--text-base); + min-width: max-content; +} + +.headerRow { + background: var(--color-primary); +} + +.th { + padding: var(--space-4) var(--space-3); + text-align: center; + font-weight: 700; + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--color-text-inverse); + white-space: nowrap; +} + +.th:first-child { + width: 200px; /* Slightly more space for the full date */ +} + +.row { + border-bottom: 1px solid var(--color-border-light); + transition: background 0.15s; +} + +.row:last-child { + border-bottom: none; +} + +.row:hover { + background: var(--color-highlight); +} + +.rowToday { + background: var(--color-highlight); + font-weight: 600; +} + +.rowToday:hover { + background: var(--color-highlight); +} + +.td { + padding: var(--space-4) var(--space-3); + text-align: center; + color: var(--color-text); + font-family: var(--family-mono); + white-space: nowrap; + font-size: var(--text-base); +} + +.dayCell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-2); +} + +.dayInfo { + display: flex; + flex-direction: column; + line-height: 1.2; + gap: 2px; +} + +.dayName { + font-weight: 700; + color: var(--color-primary); + font-size: var(--text-base); +} + +.dateText { + font-size: var(--text-xs); + color: var(--color-text-muted); + font-weight: 500; +} + +.todayBadge { + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: var(--radius-full); + background: var(--color-accent); + color: var(--color-text-inverse); + font-size: 0.625rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +@media (max-width: 480px) { + .th, + .td { + padding: var(--space-2) var(--space-4); + font-size: var(--text-xs); + } +} + +/* ── Print Styles ────────────────────────────────────── */ +@media print { + .root { + width: 100%; + } + + .tableWrapper { + border: 1px solid #ccc; + box-shadow: none; + overflow: visible; + } + + .table { + min-width: 0; + font-size: 10pt; + } + + .headerRow { + background: #1a5276 !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + .th { + color: #fff !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + padding: 4pt 6pt; + } + + .row { + border-bottom: 1px solid #ddd; + } + + .row:hover { + background: transparent; + } + + .rowToday { + background: #f0f4f8 !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + .td { + padding: 4pt 6pt; + color: #000; + } + + .dayCell { + color: #1a5276; + } + + .todayBadge { + background: #c9a84c; + color: #fff; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} diff --git a/web/Miqaat/app/blocks/calendar/calendar-grid.tsx b/web/Miqaat/app/blocks/calendar/calendar-grid.tsx new file mode 100644 index 0000000..53a8514 --- /dev/null +++ b/web/Miqaat/app/blocks/calendar/calendar-grid.tsx @@ -0,0 +1,75 @@ +import classnames from "classnames"; +import type { DayPrayerTimes } from "~/services/api"; +import { useLanguage } from "~/i18n/language-context"; +import type { TranslationKey } from "~/i18n/translations"; +import style from "./calendar-grid.module.css"; +import { formatTime } from "~/utils/time-utils"; + + +export interface CalendarGridProps { + className?: string; + data: DayPrayerTimes[]; + timeFormat: "12h" | "24h"; +} + +const PRAYER_COLUMNS = ["Fajr", "Sunrise", "Duha", "Dhuhr", "Asr", "Maghrib", "Isha"] as const; + + +export function CalendarGrid({ className, data, timeFormat }: CalendarGridProps) { + const { t, locale } = useLanguage(); + const today = new Date(); + const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; + + return ( +
+
+ + + + + {PRAYER_COLUMNS.map((name) => { + const key = `prayer.${name}` as TranslationKey; + return ( + + ); + })} + + + + {data.map((row) => { + const isToday = row.date === todayStr; + return ( + + + {PRAYER_COLUMNS.map((name) => { + const prayer = row.times.find((p) => p.name === name); + return ( + + ); + })} + + ); + })} + +
{t("calendar.day")} + {t(key)} +
+
+ + {new Date(row.date).toLocaleDateString(locale === "ar" ? "ar-DZ" : "en-US", { weekday: "long" })} + + + {new Date(row.date).toLocaleDateString(locale === "ar" ? "ar-DZ" : "en-US", { day: "numeric", month: "long" })} + +
+ {isToday && {t("calendar.today")}} +
+ {prayer ? formatTime(prayer.time, timeFormat, locale) : "—"} +
+
+
+ ); +} diff --git a/web/Miqaat/app/blocks/home/additional-info-section.module.css b/web/Miqaat/app/blocks/home/additional-info-section.module.css new file mode 100644 index 0000000..bea60dd --- /dev/null +++ b/web/Miqaat/app/blocks/home/additional-info-section.module.css @@ -0,0 +1,49 @@ +.root { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: var(--space-5); +} + +.card { + border-radius: var(--radius-lg); + padding: var(--space-8) var(--space-5); + box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-3); + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +.card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-md); + border-color: var(--color-border); +} + +.iconBox { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: var(--radius-md); + background: var(--color-highlight); + color: var(--color-primary); +} + +.label { + font-size: var(--text-sm); + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + font-weight: 800; +} + +.value { + font-family: var(--family-body); + font-size: var(--text-xl); + font-weight: 700; + color: var(--color-text); + line-height: 1; +} diff --git a/web/Miqaat/app/blocks/home/additional-info-section.tsx b/web/Miqaat/app/blocks/home/additional-info-section.tsx new file mode 100644 index 0000000..d0c7833 --- /dev/null +++ b/web/Miqaat/app/blocks/home/additional-info-section.tsx @@ -0,0 +1,76 @@ +import { Sunrise, Sunset, Moon, Sparkles, Clock } from "lucide-react"; +import classnames from "classnames"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./additional-info-section.module.css"; +import { formatMinutesToTime } from "~/utils/time-utils"; + +function parseTimeToMinutes(timeStr: string): number { + const [h, m] = timeStr.split(":").map(Number); + return h * 60 + m; +} + + +export interface AdditionalInfoSectionProps { + className?: string; +} + + +export function AdditionalInfoSection({ className }: AdditionalInfoSectionProps) { + const { prayerTimes, timeFormat } = useAppContext(); + const { t, locale } = useLanguage(); + + const sunrise = prayerTimes.find((p) => p.name === "Sunrise"); + const maghrib = prayerTimes.find((p) => p.name === "Maghrib"); + const fajr = prayerTimes.find((p) => p.name === "Fajr"); + + let midnightStr = "--:--"; + let lastThirdStr = "--:--"; + + if (maghrib && fajr) { + const maghribMin = parseTimeToMinutes(maghrib.time); + let fajrMin = parseTimeToMinutes(fajr.time); + + // Fajr is the next day + if (fajrMin <= maghribMin) { + fajrMin += 1440; + } + + const nightDuration = fajrMin - maghribMin; + + const midnightMin = maghribMin + nightDuration / 2; + const lastThirdMin = maghribMin + (2 * nightDuration) / 3; + + midnightStr = formatMinutesToTime(midnightMin, timeFormat, locale); + lastThirdStr = formatMinutesToTime(lastThirdMin, timeFormat, locale); + } + + return ( +
+ {sunrise && ( +
+
+
{t("info.sunrise")}
+
{formatMinutesToTime(parseTimeToMinutes(sunrise.time), timeFormat, locale)}
+
+ )} + {maghrib && ( +
+
+
{t("info.sunset")}
+
{formatMinutesToTime(parseTimeToMinutes(maghrib.time), timeFormat, locale)}
+
+ )} +
+
+
{t("info.midnight")}
+
{midnightStr}
+
+
+
+
{t("info.lastThird")}
+
{lastThirdStr}
+
+
+ ); +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/home/current-prayer-highlight.module.css b/web/Miqaat/app/blocks/home/current-prayer-highlight.module.css new file mode 100644 index 0000000..003bd5a --- /dev/null +++ b/web/Miqaat/app/blocks/home/current-prayer-highlight.module.css @@ -0,0 +1,110 @@ +.root { + background: var(--color-bg-card); + border-radius: var(--radius-lg); + padding: var(--space-6) var(--space-6); + box-shadow: var(--shadow-md), var(--shadow-accent-glow); + border: 2px solid var(--color-accent); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-4); + text-align: center; +} + +.header { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--color-accent-muted); + padding: var(--space-2) var(--space-6); + border-radius: var(--radius-full); + color: #5d4a1f; + /* لون غامق يتناسب مع الذهبي لزيادة التباين */ + border: 1px solid rgba(var(--color-accent), 0.3); + margin-bottom: var(--space-2); +} + +.headerLabel { + font-size: var(--text-base); + letter-spacing: 0.05em; + font-weight: 900; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); +} + +.iconWrapper { + display: flex; + align-items: center; + justify-content: center; + width: 3.5rem; + height: 3.5rem; + background: linear-gradient(135deg, var(--color-accent), var(--color-accent-light)); + border-radius: var(--radius-md); + color: #1a2b3c; + flex-shrink: 0; +} + +.name { + font-family: var(--family-display); + font-size: var(--text-2xl); + font-weight: 700; + color: var(--color-text); + line-height: 1.2; +} + +.footer { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-1); +} + +.timeLabel { + font-size: var(--text-sm); + color: var(--color-text); + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; + opacity: 0.7; +} + +.time { + font-family: var(--family-mono); + font-size: 3rem; + font-weight: 700; + color: var(--color-accent); + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: center; + gap: var(--space-3); + line-height: 1; + direction: rtl; +} + +.periodPart { + font-size: 1.5rem; + font-weight: 600; + margin-top: 4px; +} + +:global([lang="ar"]) .periodPart { + font-family: var(--family-brand-ar); + font-size: 2.5rem; + margin-top: 0; +} + +@media (max-width: 480px) { + .root { + padding: var(--space-5) var(--space-4); + } + + .name { + font-size: var(--text-xl); + } +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/home/current-prayer-highlight.tsx b/web/Miqaat/app/blocks/home/current-prayer-highlight.tsx new file mode 100644 index 0000000..a687da2 --- /dev/null +++ b/web/Miqaat/app/blocks/home/current-prayer-highlight.tsx @@ -0,0 +1,78 @@ +import { Moon, Sun, Cloud, Sunrise, Sunset, Star, AlertTriangle, SunDim } from "lucide-react"; +import classnames from "classnames"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import { usePrayerStatus } from "~/hooks/use-prayer-status"; +import type { TranslationKey } from "~/i18n/translations"; +import style from "./current-prayer-highlight.module.css"; +import { formatTime } from "~/utils/time-utils"; + + +export interface CurrentPrayerHighlightProps { + className?: string; +} + +const PRAYER_ICONS: Record = { + moon: , + sun: , + "sun-dim": , + sunrise: , + sunset: , + "cloud-sun": , + star: , +}; + + +export function CurrentPrayerHighlight({ className }: CurrentPrayerHighlightProps) { + const { currentPrayer, prayerTimes, timeFormat } = useAppContext(); + const { t, locale } = useLanguage(); + const prayerStatus = usePrayerStatus(prayerTimes); + + if (!currentPrayer) return null; + + const prayerKey = `prayer.${currentPrayer.name}` as TranslationKey; + let displayName = t(prayerKey); + let displayIcon = PRAYER_ICONS[currentPrayer.icon] ?? ; + + if (prayerStatus === "forbidden") { + displayName = t("home.forbiddenPrayer"); + displayIcon = ; + } else if (prayerStatus === "duha") { + displayName = t("home.duha"); + displayIcon = ; + } + + return ( +
+
+ {t("home.currentPrayer")} +
+ +
+
+ {displayIcon} +
+
{displayName}
+
+ +
+
{t("home.prayerTime")}
+
+ {(() => { + const timeStr = formatTime(currentPrayer.time, timeFormat, locale); + const parts = timeStr.split(" "); + if (parts.length > 1) { + return ( + <> + {parts[0]} + {parts[1]} + + ); + } + return {timeStr}; + })()} +
+
+
+ ); +} diff --git a/web/Miqaat/app/blocks/home/current-time-card.module.css b/web/Miqaat/app/blocks/home/current-time-card.module.css new file mode 100644 index 0000000..d7e9fe6 --- /dev/null +++ b/web/Miqaat/app/blocks/home/current-time-card.module.css @@ -0,0 +1,72 @@ +.root { + background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%); + border-radius: var(--radius-xl); + padding: var(--space-8) var(--space-8); + color: var(--color-text-inverse); + box-shadow: var(--shadow-lg), var(--shadow-glow); + text-align: center; + position: relative; + overflow: hidden; +} + +.root::before { + content: ""; + position: absolute; + top: -40%; + right: -10%; + width: 280px; + height: 280px; + background: rgba(255, 255, 255, 0.05); + border-radius: 50%; + pointer-events: none; +} + +.root::after { + content: ""; + position: absolute; + bottom: -30%; + left: -5%; + width: 200px; + height: 200px; + background: rgba(255, 255, 255, 0.04); + border-radius: 50%; + pointer-events: none; +} + +.timeWrapper { + position: relative; + z-index: 1; +} + +.time { + font-family: var(--family-mono); + font-size: var(--text-4xl); + font-weight: 300; + line-height: 1; + letter-spacing: -0.02em; + margin-bottom: var(--space-2); + direction: ltr; +} + +.date { + font-size: var(--text-base); + opacity: 0.85; + margin-bottom: var(--space-2); +} + +.hijri { + font-family: var(--family-body); + font-size: var(--text-md); + color: var(--color-accent-light); + font-weight: 600; + opacity: 1; +} + +@media (max-width: 480px) { + .root { + padding: var(--space-6) var(--space-5); + } + .time { + font-size: var(--text-3xl); + } +} diff --git a/web/Miqaat/app/blocks/home/current-time-card.tsx b/web/Miqaat/app/blocks/home/current-time-card.tsx new file mode 100644 index 0000000..eae5ed0 --- /dev/null +++ b/web/Miqaat/app/blocks/home/current-time-card.tsx @@ -0,0 +1,40 @@ +import classnames from "classnames"; +import { useClock } from "~/hooks/use-clock"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./current-time-card.module.css"; + +export interface CurrentTimeCardProps { + className?: string; +} + +export function CurrentTimeCard({ className }: CurrentTimeCardProps) { + const { timeFormat } = useAppContext(); + const { locale } = useLanguage(); + const { timeOnly, period, hijriDate } = useClock(timeFormat, locale); + + return ( +
+
+
+ {period && ( + + {period} + + )} + {timeOnly} +
+
{hijriDate}
+
+
+ ); +} diff --git a/web/Miqaat/app/blocks/home/location-display.module.css b/web/Miqaat/app/blocks/home/location-display.module.css new file mode 100644 index 0000000..adeaa45 --- /dev/null +++ b/web/Miqaat/app/blocks/home/location-display.module.css @@ -0,0 +1,65 @@ +.root { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-4) var(--space-5); + background: var(--color-bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border-light); +} + +.locationInfo { + display: flex; + align-items: center; + gap: var(--space-3); +} + +.icon { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + background: var(--color-highlight); + border-radius: var(--radius-md); + color: var(--color-primary); + flex-shrink: 0; +} + +.text { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.city { + font-size: var(--text-md); + font-weight: 600; + color: var(--color-text); +} + +.country { + font-size: var(--text-sm); + color: var(--color-text-muted); +} + +.changeBtn { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-full); + border: 1.5px solid var(--color-primary); + background: transparent; + color: var(--color-primary); + font-size: var(--text-sm); + font-weight: 500; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.changeBtn:hover { + background: var(--color-primary); + color: var(--color-text-inverse); +} diff --git a/web/Miqaat/app/blocks/home/location-display.tsx b/web/Miqaat/app/blocks/home/location-display.tsx new file mode 100644 index 0000000..a4570af --- /dev/null +++ b/web/Miqaat/app/blocks/home/location-display.tsx @@ -0,0 +1,32 @@ +import { Link } from "react-router"; +import { MapPin, Navigation } from "lucide-react"; +import classnames from "classnames"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./location-display.module.css"; + +export interface LocationDisplayProps { + className?: string; +} + +export function LocationDisplay({ className }: LocationDisplayProps) { + const { location } = useAppContext(); + const { t, locale } = useLanguage(); + return ( +
+
+
+ +
+
+ {locale === "ar" ? (location.cityAr || location.city) : location.city} + {locale === "ar" ? (location.countryAr || location.country) : location.country} +
+
+ + + {t("location.change")} + +
+ ); +} diff --git a/web/Miqaat/app/blocks/home/next-prayer-countdown.module.css b/web/Miqaat/app/blocks/home/next-prayer-countdown.module.css new file mode 100644 index 0000000..68a9648 --- /dev/null +++ b/web/Miqaat/app/blocks/home/next-prayer-countdown.module.css @@ -0,0 +1,109 @@ +.root { + background: var(--color-bg-card); + border-radius: var(--radius-lg); + padding: var(--space-6) var(--space-6); + box-shadow: var(--shadow-md); + border: 1px solid var(--color-border-light); + display: flex; + flex-direction: column; + align-items: center; +} + +.header { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + background: var(--color-highlight); + padding: var(--space-2) var(--space-6); + border-radius: var(--radius-full); + margin: 0 auto var(--space-5) auto; + color: var(--color-primary); + border: 1px solid var(--color-border-light); +} + +.headerLabel { + font-size: var(--text-base); + letter-spacing: 0.05em; + color: var(--color-primary); + font-weight: 900; +} + +.prayerName { + font-family: var(--family-display); + font-size: var(--text-2xl); + font-weight: 600; + color: var(--color-primary); + margin-bottom: var(--space-5); +} + +.countdown { + display: flex; + flex-direction: row; + direction: ltr; + align-items: center; + justify-content: center; + gap: var(--space-3); + margin-bottom: var(--space-4); +} + +.unit { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-1); +} + +.digits { + font-family: var(--family-mono); + font-size: var(--text-3xl); + font-weight: 700; + color: var(--color-primary); + background: var(--color-highlight); + border-radius: var(--radius-md); + min-width: 4rem; + padding: var(--space-2) var(--space-3); + line-height: 1; + letter-spacing: 0.02em; +} + +.unitLabel { + font-size: var(--text-sm); + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-text); + font-weight: 600; + opacity: 0.7; +} + +.separator { + font-family: var(--family-mono); + font-size: var(--text-2xl); + color: var(--color-text-muted); + margin-bottom: var(--space-5); + font-weight: 700; +} + +.prayerTime { + font-size: var(--text-lg); + color: var(--color-text); + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + white-space: nowrap; +} + +.prayerTime strong { + color: var(--color-primary); + font-family: var(--family-mono); + font-weight: 700; +} + +@media (max-width: 480px) { + .digits { + font-size: var(--text-2xl); + min-width: 3rem; + } +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/home/next-prayer-countdown.tsx b/web/Miqaat/app/blocks/home/next-prayer-countdown.tsx new file mode 100644 index 0000000..3d3c430 --- /dev/null +++ b/web/Miqaat/app/blocks/home/next-prayer-countdown.tsx @@ -0,0 +1,78 @@ +import { Clock } from "lucide-react"; +import classnames from "classnames"; +import { useAppContext } from "~/context/app-context"; +import { useCountdown } from "~/hooks/use-countdown"; +import { useLanguage } from "~/i18n/language-context"; +import type { TranslationKey } from "~/i18n/translations"; +import style from "./next-prayer-countdown.module.css"; +import { formatTime } from "~/utils/time-utils"; + + +export interface NextPrayerCountdownProps { + className?: string; +} + + +export function NextPrayerCountdown({ className }: NextPrayerCountdownProps) { + const { nextPrayer, timeFormat } = useAppContext(); + const { hours, minutes, seconds } = useCountdown(nextPrayer?.time ?? null); + const { t, locale } = useLanguage(); + + if (!nextPrayer) return null; + + const prayerKey = `prayer.${nextPrayer.name}` as TranslationKey; + + return ( +
+
+ + {t("home.nextPrayer")} +
+
{t(prayerKey)}
+
+ {hours !== "00" && ( + <> +
+ {hours} + {t("home.hours")} +
+ : + + )} + {(hours !== "00" || minutes !== "00") && ( + <> +
+ {minutes} + {t("home.minutes")} +
+ : + + )} +
+ {seconds} + {t("home.seconds")} +
+
+
+ {t("home.startsAt")} + + {(() => { + const timeStr = formatTime(nextPrayer.time, timeFormat, locale); + const parts = timeStr.split(" "); + if (parts.length > 1) { + // In RTL, the first element goes to the right and the second goes to the left. + // So parts[0] (time) is right, parts[1] (AM/PM) is left. + return ( + <> + {parts[0]} + {parts[1]} + + ); + } + return {timeStr}; + })()} + +
+
+ ); +} diff --git a/web/Miqaat/app/blocks/home/prayer-times-grid.module.css b/web/Miqaat/app/blocks/home/prayer-times-grid.module.css new file mode 100644 index 0000000..1d1df9d --- /dev/null +++ b/web/Miqaat/app/blocks/home/prayer-times-grid.module.css @@ -0,0 +1,152 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--space-5); + align-items: center; + width: 100%; +} + +.sectionTitle { + font-size: var(--text-base); + color: var(--color-text); + font-weight: 700; + text-align: center; + letter-spacing: 0.05em; + opacity: 0.9; +} + +.root { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--space-4); + width: 100%; + max-width: 1400px; +} + +.card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-2); + background: var(--color-bg-card); + border: 1px solid var(--color-border-light); + border-radius: var(--radius-lg); + padding: var(--space-8) var(--space-2); + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: var(--shadow-sm); + min-width: 160px; + flex: 1; + max-width: 200px; +} + +.card:hover { + transform: translateY(-6px); + box-shadow: var(--shadow-md); + border-color: var(--color-border); +} + +.cardActive { + background: linear-gradient(145deg, var(--color-accent), var(--color-accent-light)); + border-color: transparent; + color: #1a2b3c; + transform: scale(1.08); + box-shadow: var(--shadow-lg), var(--shadow-accent-glow); + z-index: 10; +} + +.cardActive:hover { + transform: scale(1.08) translateY(-4px); +} + + +.activeBadge { + position: absolute; + top: -10px; + background: var(--color-bg-card); + color: var(--color-accent); + font-size: 0.7rem; + font-weight: 700; + padding: 4px 12px; + border-radius: var(--radius-full); + border: 1px solid var(--color-accent); + box-shadow: var(--shadow-sm); +} + +.iconWrapper { + color: var(--color-primary); + margin-bottom: var(--space-2); +} + +.cardActive .iconWrapper { + color: #1a2b3c; +} + + +.prayerName { + font-family: var(--family-display); + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text); + margin-bottom: var(--space-1); +} + +.cardActive .prayerName { + color: #1a2b3c; +} + +.prayerTime { + font-family: var(--family-mono); + font-size: var(--text-2xl); + font-weight: 700; + color: var(--color-primary); + letter-spacing: -0.02em; + direction: ltr; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + line-height: 1.1; +} + +.periodPart { + font-size: 1rem; + font-weight: 500; + margin-top: 2px; +} + +:global([lang="ar"]) .periodPart { + font-family: var(--family-brand-ar); + font-size: 1.25rem; +} + +.cardActive .prayerTime { + color: #1a2b3c; +} + +@media (max-width: 900px) { + .root { + gap: var(--space-4); + } +} + +@media (max-width: 480px) { + .root { + gap: var(--space-3); + } + + .card { + min-width: 140px; + } + + .cardActive { + transform: scale(1.04); + } + + .cardActive:hover { + transform: scale(1.04) translateY(-3px); + } +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/home/prayer-times-grid.tsx b/web/Miqaat/app/blocks/home/prayer-times-grid.tsx new file mode 100644 index 0000000..588f1da --- /dev/null +++ b/web/Miqaat/app/blocks/home/prayer-times-grid.tsx @@ -0,0 +1,83 @@ +import { Moon, Sun, Cloud, Sunrise, Sunset, Star, SunDim } from "lucide-react"; +import classnames from "classnames"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import type { TranslationKey } from "~/i18n/translations"; +import style from "./prayer-times-grid.module.css"; +import { formatTime } from "~/utils/time-utils"; + + +export interface PrayerTimesGridProps { + className?: string; +} + +const PRAYER_ICONS: Record = { + moon: , + sun: , + "sun-dim": , + sunrise: , + sunset: , + "cloud-sun": , + star: , +}; + + +export function PrayerTimesGrid({ className }: PrayerTimesGridProps) { + const { prayerTimes, currentPrayer, timeFormat } = useAppContext(); + const { t, locale } = useLanguage(); + + // Filter for only the 5 obligatory prayers + Duha (excluding Sunrise) + const mainPrayers = ["Fajr", "Duha", "Dhuhr", "Asr", "Maghrib", "Isha"]; + const displayPrayers = prayerTimes.filter((p) => mainPrayers.includes(p.name)); + + return ( +
+
— {t("home.schedule")} —
+
+ {displayPrayers.map((prayer) => { + const isActive = prayer.isPrayer && currentPrayer?.name === prayer.name; + const prayerKey = `prayer.${prayer.name}` as TranslationKey; + + return ( +
+ {isActive && ( +
+ {t("home.now")} +
+ )} +
+ {PRAYER_ICONS[prayer.icon] ?? } +
+
+ {t(prayerKey)} +
+
+ {(() => { + const timeStr = formatTime(prayer.time, timeFormat, locale); + const parts = timeStr.split(" "); + if (parts.length > 1) { + return ( + <> + {parts[0]} + {parts[1]} + + ); + } + return {timeStr}; + })()} +
+
+ ); + })} +
+
+ ); +} diff --git a/web/Miqaat/app/blocks/location-selection/gps-location-button.module.css b/web/Miqaat/app/blocks/location-selection/gps-location-button.module.css new file mode 100644 index 0000000..34825a8 --- /dev/null +++ b/web/Miqaat/app/blocks/location-selection/gps-location-button.module.css @@ -0,0 +1,43 @@ +.root { +} + +.btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-3); + padding: var(--space-4) var(--space-6); + min-height: 56px; + background: var(--color-bg-card); + border: 2px dashed var(--color-primary); + border-radius: var(--radius-xl); + color: var(--color-primary); + font-size: var(--text-md); + font-weight: 700; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn:hover { + background: var(--color-highlight); +} + +.btnLoading { + opacity: 0.7; + cursor: wait; +} + +.btnSuccess { + border-color: var(--color-success); + color: var(--color-success); + border-style: solid; +} + +.spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/web/Miqaat/app/blocks/location-selection/gps-location-button.tsx b/web/Miqaat/app/blocks/location-selection/gps-location-button.tsx new file mode 100644 index 0000000..746fccf --- /dev/null +++ b/web/Miqaat/app/blocks/location-selection/gps-location-button.tsx @@ -0,0 +1,66 @@ +import { Navigation, Loader, CheckCircle } from "lucide-react"; +import classnames from "classnames"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./gps-location-button.module.css"; + +export interface GPSLocationButtonProps { + className?: string; + isDetecting: boolean; + isSuccess: boolean; + onDetect: () => void; + onClear?: () => void; +} + +export function GPSLocationButton({ + className, + isDetecting, + isSuccess, + onDetect, + onClear, +}: GPSLocationButtonProps) { + const { t } = useLanguage(); + + if (isSuccess) { + return ( +
+
+ + {t("location.gps.success")} +
+
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/web/Miqaat/app/blocks/location-selection/search-header.module.css b/web/Miqaat/app/blocks/location-selection/search-header.module.css new file mode 100644 index 0000000..db8c05d --- /dev/null +++ b/web/Miqaat/app/blocks/location-selection/search-header.module.css @@ -0,0 +1,24 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.title { + font-family: var(--family-display); + font-size: var(--text-3xl); + font-weight: 600; + color: var(--color-primary); +} + +:global([lang="ar"]) .title { + font-family: var(--family-brand-ar); + font-size: 3rem; + font-weight: 700; +} + +.subtitle { + font-size: var(--text-sm); + color: var(--color-text-muted); + line-height: 1.5; +} diff --git a/web/Miqaat/app/blocks/location-selection/search-header.tsx b/web/Miqaat/app/blocks/location-selection/search-header.tsx new file mode 100644 index 0000000..d9087f2 --- /dev/null +++ b/web/Miqaat/app/blocks/location-selection/search-header.tsx @@ -0,0 +1,24 @@ +import classnames from "classnames"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./search-header.module.css"; + +export interface SearchHeaderProps { + className?: string; +} + +export function SearchHeader({ className }: SearchHeaderProps) { + const { t, locale } = useLanguage(); + return ( +
+

+ {t("location.title")} +

+

+ {t("location.subtitle")} +

+
+ ); +} diff --git a/web/Miqaat/app/blocks/location-selection/wilaya-baladiya-selector.module.css b/web/Miqaat/app/blocks/location-selection/wilaya-baladiya-selector.module.css new file mode 100644 index 0000000..758f340 --- /dev/null +++ b/web/Miqaat/app/blocks/location-selection/wilaya-baladiya-selector.module.css @@ -0,0 +1,134 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--space-8); +} + +.fieldGroup { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.label { + font-weight: 600; + font-size: var(--text-sm); + color: var(--color-text); + display: flex; + align-items: center; + gap: var(--space-2); +} + +.labelIcon { + color: var(--color-primary); + flex-shrink: 0; +} + +.select { + width: 100%; + padding: var(--space-4) var(--space-5); + padding-right: 2.75rem; + min-height: 56px; + border: 2px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-bg-card); + color: var(--color-text); + font-size: var(--text-md); + font-family: var(--family-body); + outline: none; + transition: all 0.2s ease; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%235d7a8e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right var(--space-4) center; +} + +.select:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(26, 82, 118, 0.12); +} + +.select:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.loadingText { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: var(--text-sm); + color: var(--color-text-muted); + padding: var(--space-2) 0; +} + +.spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.confirmBtn { + width: 100%; + padding: var(--space-4) var(--space-6); + min-height: 56px; + background: var(--color-primary); + color: var(--color-text-inverse); + border: none; + border-radius: var(--radius-xl); + font-size: var(--text-md); + font-weight: 700; + font-family: var(--family-body); + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + margin-top: var(--space-4); + box-shadow: var(--shadow-md); +} + +.confirmBtn:hover:not(:disabled) { + background: var(--color-primary-dark); +} + +.confirmBtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.errorContainer { + margin-top: var(--space-3); + padding: var(--space-4); + background: rgba(231, 76, 60, 0.1); + border: 1px solid rgba(231, 76, 60, 0.2); + border-radius: var(--radius-md); + text-align: center; +} + +.errorText { + color: #e74c3c; + font-size: var(--text-sm); + font-weight: 500; + margin-bottom: var(--space-3); +} + +.retryBtn { + background: #e74c3c; + color: white; + border: none; + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-sm); + font-size: var(--text-xs); + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.retryBtn:hover { + background: #c0392b; +} diff --git a/web/Miqaat/app/blocks/location-selection/wilaya-baladiya-selector.tsx b/web/Miqaat/app/blocks/location-selection/wilaya-baladiya-selector.tsx new file mode 100644 index 0000000..e8c6a3f --- /dev/null +++ b/web/Miqaat/app/blocks/location-selection/wilaya-baladiya-selector.tsx @@ -0,0 +1,176 @@ +import { MapPin, Loader, CheckCircle, Search, ChevronDown } from "lucide-react"; +import classnames from "classnames"; +import { useState, useRef, useEffect } from "react"; +import type { Wilaya } from "~/data/prayer-data"; +import { useLanguage } from "~/i18n/language-context"; +import styles from "./wilaya-baladiya-selector.module.css"; + +export interface WilayaSelectorProps { + className?: string; + wilayas: Wilaya[]; + selectedWilaya: Wilaya | null; + isLoadingWilayas: boolean; + onWilayaChange: (wilaya: Wilaya) => void; + onConfirm: () => void; + canConfirm: boolean; + fetchError: string | null; + onRetry: () => void; + onClear: () => void; +} + +export function WilayaSelector({ + className, + wilayas, + selectedWilaya, + isLoadingWilayas, + onWilayaChange, + onConfirm, + canConfirm, + fetchError, + onRetry, + onClear, +}: WilayaSelectorProps) { + const { t } = useLanguage(); + const [isOpen, setIsOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const filteredWilayas = wilayas.filter(w => + w.name.toLowerCase().includes(searchQuery.toLowerCase()) || + w.nameAr.includes(searchQuery) || + w.id.toString().includes(searchQuery) + ); + + return ( +
+
+ + + {isLoadingWilayas ? ( +
+ {t("location.wilaya.loading")} + +
+ ) : ( +
+ + + {isOpen && ( +
+
+
+ + setSearchQuery(e.target.value)} + autoFocus + /> +
+
+
+ {filteredWilayas.length > 0 ? ( + filteredWilayas.map((w) => ( + + )) + ) : ( +
+ + لا توجد نتائج مطابقة لبحثك +
+ )} +
+
+ )} +
+ )} + + {fetchError && ( +
+

{fetchError}

+ +
+ )} +
+ + {canConfirm && ( +
+ + {onClear && ( + + )} +
+ )} +
+ ); +} diff --git a/web/Miqaat/app/blocks/settings/language-toggle.module.css b/web/Miqaat/app/blocks/settings/language-toggle.module.css new file mode 100644 index 0000000..68a722c --- /dev/null +++ b/web/Miqaat/app/blocks/settings/language-toggle.module.css @@ -0,0 +1,78 @@ +.root { + background: var(--color-bg-card); + border-radius: var(--radius-lg); + padding: var(--space-6); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border-light); + display: flex; + flex-direction: column; + gap: var(--space-4); + transition: transform 0.2s, box-shadow 0.2s; + height: 100%; +} + +.root:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.header { + display: flex; + align-items: center; + gap: var(--space-3); +} + +.iconBox { + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border-radius: var(--radius-sm); + background: var(--color-highlight); + color: var(--color-primary); + flex-shrink: 0; +} + +.title { + font-weight: 600; + font-size: var(--text-base); + color: var(--color-text); +} + +.options { + display: flex; + gap: var(--space-2); +} + +.option { + flex: 1; + padding: var(--space-3) var(--space-4); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-bg-card); + color: var(--color-text-muted); + font-size: var(--text-sm); + font-weight: 500; + font-family: var(--family-body); + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.option:hover { + border-color: var(--color-primary); + color: var(--color-primary); + background: var(--color-highlight); +} + +.optionActive { + border-color: var(--color-primary); + background: var(--color-primary); + color: var(--color-text-inverse); +} + +.optionActive:hover { + background: var(--color-primary-dark); + color: var(--color-text-inverse); +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/settings/language-toggle.tsx b/web/Miqaat/app/blocks/settings/language-toggle.tsx new file mode 100644 index 0000000..4548427 --- /dev/null +++ b/web/Miqaat/app/blocks/settings/language-toggle.tsx @@ -0,0 +1,47 @@ +import { Globe } from "lucide-react"; +import classnames from "classnames"; +import { useLanguage } from "~/i18n/language-context"; +import type { Locale } from "~/i18n/translations"; +import style from "./language-toggle.module.css"; + +export interface LanguageToggleProps { + className?: string; + value: Locale; + onChange: (locale: Locale) => void; +} + +export function LanguageToggle({ className, value, onChange }: LanguageToggleProps) { + const { t } = useLanguage(); + + const options: { value: Locale; label: string }[] = [ + { value: "en", label: "English" }, + { value: "ar", label: "العربية" }, + ]; + + return ( +
+
+
+ +
+ {t("settings.language")} +
+

+ {t("settings.language.desc" as any)} +

+
+ {options.map((opt) => ( + + ))} +
+
+ ); +} + diff --git a/web/Miqaat/app/blocks/settings/notification-toggle.module.css b/web/Miqaat/app/blocks/settings/notification-toggle.module.css new file mode 100644 index 0000000..2a52a3c --- /dev/null +++ b/web/Miqaat/app/blocks/settings/notification-toggle.module.css @@ -0,0 +1,241 @@ +.root { + background: var(--color-bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border-light); + display: flex; + flex-direction: column; + transition: transform 0.2s, box-shadow 0.2s; +} + +.root:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.mainRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-6); + border-radius: var(--radius-lg); + transition: background 0.2s; +} + +.mainRowClickable { + cursor: pointer; +} + +.mainRowClickable:hover { + background: var(--color-highlight); +} + +.header { + display: flex; + align-items: center; + gap: var(--space-4); +} + +.iconBox { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: var(--radius-md); + background: var(--color-highlight); + color: var(--color-primary); + flex-shrink: 0; +} + +.info { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.title { + font-weight: 600; + font-size: var(--text-md); + color: var(--color-text); +} + +.desc { + font-size: var(--text-sm); + color: var(--color-text-muted); +} + +.actions { + display: flex; + align-items: center; + gap: var(--space-3); +} + +.chevron { + color: var(--color-text-muted); + display: flex; + align-items: center; +} + +.toggle { + position: relative; + width: 3rem; + height: 1.625rem; + border-radius: var(--radius-full); + background: var(--color-border); + border: none; + cursor: pointer; + transition: background 0.25s; + flex-shrink: 0; +} + +.smallToggle { + width: 2.5rem; + height: 1.375rem; +} + +.toggle:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.toggleActive { + background: var(--color-primary); +} + +.toggleThumb { + position: absolute; + top: 3px; + left: 3px; + width: 1.125rem; + height: 1.125rem; + border-radius: 50%; + background: #fff; + transition: transform 0.25s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); +} + +.smallToggle .toggleThumb { + top: 2px; + left: 2px; + width: 1.125rem; + height: 1.125rem; +} + +.toggleActive .toggleThumb { + transform: translateX(1.375rem); +} + +.smallToggle.toggleActive .toggleThumb { + transform: translateX(1.125rem); +} + +.prayersList { + display: flex; + flex-direction: column; + border-top: 1px solid var(--color-border-light); + padding: var(--space-2) var(--space-6) var(--space-4); + background: var(--color-bg-body); + border-bottom-left-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); +} + +.prayerRow { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-3) 0; + border-bottom: 1px solid var(--color-border-light); +} + +.prayerRow:last-child { + border-bottom: none; +} + +.prayerName { + font-size: var(--text-base); + color: var(--color-text); + font-weight: 500; +} + +.iconBoxError { + background: rgba(231, 76, 60, 0.1); + color: var(--color-error); +} + +.toggleDisabled { + background: var(--color-border-light) !important; + opacity: 0.5; +} + +.successMessage { + padding: var(--space-4) var(--space-6); + background: rgba(39, 174, 96, 0.05); + color: #27ae60; + font-size: var(--text-sm); + border-top: 1px solid var(--color-border-light); + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.successContent { + display: flex; + align-items: center; + gap: var(--space-2); + font-weight: 600; +} + +.successMessage small { + display: block; + font-size: var(--text-xs); + opacity: 0.8; + font-weight: 400; + padding-left: 1.375rem; +} + +.blockedMessage { + padding: var(--space-4) var(--space-6); + background: rgba(231, 76, 60, 0.05); + color: var(--color-error); + font-size: var(--text-sm); + display: flex; + flex-direction: column; + gap: var(--space-2); + border-top: 1px solid var(--color-border-light); + animation: slideDown 0.3s ease; +} + +.blockedTitle { + font-weight: 700; + display: flex; + align-items: center; + gap: var(--space-2); +} + +.blockedDesc { + opacity: 0.9; + line-height: 1.5; + padding-left: 1.5rem; +} + +.unsupportedMessage { + padding: var(--space-4) var(--space-6); + background: var(--color-highlight); + color: var(--color-text-muted); + font-size: var(--text-sm); + text-align: center; + border-top: 1px solid var(--color-border-light); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/settings/notification-toggle.tsx b/web/Miqaat/app/blocks/settings/notification-toggle.tsx new file mode 100644 index 0000000..a69d448 --- /dev/null +++ b/web/Miqaat/app/blocks/settings/notification-toggle.tsx @@ -0,0 +1,150 @@ +import { Bell, BellOff, ChevronDown, ChevronUp, AlertCircle, Info } from "lucide-react"; +import classnames from "classnames"; +import { useState } from "react"; +import { useLanguage } from "~/i18n/language-context"; +import { useNotificationPermission } from "~/hooks/use-notification-permission"; +import style from "./notification-toggle.module.css"; +import type { PrayerName } from "~/data/prayer-data"; + +export interface NotificationToggleProps { + className?: string; + value: boolean; + onChange: (enabled: boolean) => void; + preferences?: Record; + onPreferenceChange?: (prayerName: string, enabled: boolean) => void; +} + +const PRAYERS: PrayerName[] = ["Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"]; + +export function NotificationToggle({ className, value, onChange, preferences = {}, onPreferenceChange }: NotificationToggleProps) { + const { t } = useLanguage(); + const [expanded, setExpanded] = useState(false); + const { permission, isGranted, isDenied, isUnsupported, requestPermission } = useNotificationPermission(); + + const handleToggle = async (e: React.MouseEvent) => { + e.stopPropagation(); + + if (isUnsupported) return; + + if (!value) { + // Turning on: Check permission first + if (permission === "default") { + const result = await requestPermission(); + if (result !== "granted") { + return; // Do not toggle on if denied or dismissed + } + } else if (isDenied) { + // We cannot request again if denied, UI should explain this + return; + } + } + + onChange(!value); + if (!value) { + setExpanded(true); + } + }; + + const showBlockedMessage = isDenied && value; // User had them on but now blocked + const showEnableMessage = !isGranted && !isDenied && !isUnsupported; + + return ( +
+
value && setExpanded(!expanded)} + > +
+
+ {value && isGranted ? : } +
+
+
{t("settings.notifications")}
+
+ {isDenied + ? t("settings.notifications.denied") + : isUnsupported + ? t("settings.notifications.unsupported") + : t("settings.notifications.desc")} +
+
+
+
+ {value && ( +
+ {expanded ? : } +
+ )} + +
+
+ + {isDenied && ( +
+
+ + {t("settings.notifications.blocked.title") || "Notifications Blocked"} +
+
+ {t("settings.notifications.blocked.guide") || "Your browser is blocking notifications. Please click the lock icon in your address bar to allow them for this site."} +
+
+ )} + + {isUnsupported && ( +
+ {t("settings.notifications.unsupported")} +
+ )} + + {value && isGranted && ( +
+
+ + {t("settings.notifications.scheduled")} +
+ {t("settings.notifications.limitation")} +
+ )} + + {value && expanded && isGranted && onPreferenceChange && ( +
+ {PRAYERS.map((prayer) => { + const isEnabled = preferences[prayer] !== false; // true by default + return ( +
+ {t(`prayer.${prayer}` as any)} + +
+ ); + })} +
+ )} +
+ ); +} + diff --git a/web/Miqaat/app/blocks/settings/settings-actions.module.css b/web/Miqaat/app/blocks/settings/settings-actions.module.css new file mode 100644 index 0000000..5438005 --- /dev/null +++ b/web/Miqaat/app/blocks/settings/settings-actions.module.css @@ -0,0 +1,110 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--space-3); + width: 100%; +} + +.buttons { + display: flex; + gap: var(--space-4); + width: 100%; + flex-direction: row; +} + +.saveBtn { + flex: 1; + padding: var(--space-4) var(--space-6); + background: var(--color-primary); + color: var(--color-text-inverse); + border-radius: var(--radius-md); + font-weight: 600; + font-size: var(--text-base); + border: none; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; + text-align: center; +} + +.saveBtn:hover:not(:disabled) { + background-color: var(--color-primary-dark); + transform: translateY(-1px); +} + +.saveBtn:disabled { + background: var(--color-border); + color: var(--color-text-muted); + cursor: not-allowed; + transform: none; +} + +.saveBtnLoading { + opacity: 0.8; + cursor: wait; +} + +.loaderContainer { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); +} + +.resetBtn { + flex: 1; + padding: var(--space-4) var(--space-6); + background: transparent; + color: var(--color-text-muted); + border-radius: var(--radius-md); + font-weight: 600; + font-size: var(--text-base); + border: 1.5px solid var(--color-border); + cursor: pointer; + transition: border-color 0.3s ease, color 0.3s ease, background-color 0.3s ease, transform 0.2s ease; + text-align: center; +} + +.resetBtn:hover:not(:disabled) { + border-color: var(--color-text); + color: var(--color-text); + background-color: rgba(0, 0, 0, 0.03); + transform: translateY(-1px); +} + +.resetBtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +@media (max-width: 600px) { + .buttons { + flex-direction: column; + } +} + +.dirtyIndicator { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + text-align: center; + font-size: var(--text-xs); + color: var(--color-warning); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: var(--space-1) 0; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/settings/settings-actions.tsx b/web/Miqaat/app/blocks/settings/settings-actions.tsx new file mode 100644 index 0000000..8ebcfc8 --- /dev/null +++ b/web/Miqaat/app/blocks/settings/settings-actions.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import { AlertCircle, CheckCircle, Loader } from "lucide-react"; +import classnames from "classnames"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./settings-actions.module.css"; + +export interface SettingsActionsProps { + className?: string; + onSave: () => void; + onReset: () => void; + isDirty?: boolean; + isSaving?: boolean; +} + +export function SettingsActions({ className, onSave, onReset, isDirty, isSaving }: SettingsActionsProps) { + const { t } = useLanguage(); + + return ( +
+
+ + +
+ {isDirty && !isSaving && ( +
+ + {t("settings.unsavedChanges")} +
+ )} +
+ ); +} + + diff --git a/web/Miqaat/app/blocks/settings/theme-toggle.module.css b/web/Miqaat/app/blocks/settings/theme-toggle.module.css new file mode 100644 index 0000000..90510eb --- /dev/null +++ b/web/Miqaat/app/blocks/settings/theme-toggle.module.css @@ -0,0 +1,92 @@ +.root { + background: var(--color-bg-card); + border-radius: var(--radius-lg); + padding: var(--space-6); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border-light); + transition: transform 0.2s, box-shadow 0.2s; + height: 100%; + display: flex; + flex-direction: column; +} + +.root:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.sectionTitle { + font-size: var(--text-md); + font-weight: 600; + color: var(--color-text); + margin-bottom: var(--space-2); +} + +.sectionDesc { + font-size: var(--text-sm); + color: var(--color-text-muted); + margin-bottom: var(--space-5); +} + +.options { + display: flex; + gap: var(--space-3); + flex-wrap: wrap; + flex: 1; + align-content: stretch; +} + +.option { + flex: 1; + min-width: 130px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-3); + padding: var(--space-5) var(--space-4); + border-radius: var(--radius-md); + border: 1.5px solid var(--color-border); + cursor: pointer; + transition: border-color 0.2s, background 0.2s; + text-align: center; +} + +.option:hover { + border-color: var(--color-primary); + background: var(--color-highlight); +} + +.optionSelected { + border-color: var(--color-primary); + background: var(--color-highlight); +} + +.previewBox { + width: 64px; + height: 40px; + border-radius: var(--radius-sm); + border: 1.5px solid var(--color-border); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.previewLight { + background: #f0f4f8; +} + +.previewDark { + background: #0d1b2a; +} + +.previewSystem { + background: linear-gradient(135deg, #f0f4f8 50%, #0d1b2a 50%); +} + +.label { + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-text); +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/settings/theme-toggle.tsx b/web/Miqaat/app/blocks/settings/theme-toggle.tsx new file mode 100644 index 0000000..cc7a65d --- /dev/null +++ b/web/Miqaat/app/blocks/settings/theme-toggle.tsx @@ -0,0 +1,46 @@ +import classnames from "classnames"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./theme-toggle.module.css"; + +type ColorScheme = "light" | "dark" | "system"; + +export interface ThemeToggleProps { + className?: string; + value: ColorScheme; + onChange: (scheme: ColorScheme) => void; +} + +export function ThemeToggle({ className, value, onChange }: ThemeToggleProps) { + const { t } = useLanguage(); + + const options = [ + { id: "light" as const, label: t("settings.theme.light"), emoji: "☀️", previewClass: style.previewLight }, + { id: "dark" as const, label: t("settings.theme.dark"), emoji: "🌙", previewClass: style.previewDark }, + { id: "system" as const, label: t("settings.theme.system"), emoji: "💻", previewClass: style.previewSystem }, + ]; + + return ( +
+
{t("settings.theme")}
+

{t("settings.theme.desc")}

+
+ {options.map((opt) => ( +
onChange(opt.id)} + role="radio" + aria-checked={value === opt.id} + aria-label={`${opt.label} theme`} + tabIndex={0} + onKeyDown={(e) => e.key === "Enter" && onChange(opt.id)} + > +
{opt.emoji}
+
{opt.label}
+
+ ))} +
+
+ ); +} + diff --git a/web/Miqaat/app/blocks/settings/time-format-toggle.module.css b/web/Miqaat/app/blocks/settings/time-format-toggle.module.css new file mode 100644 index 0000000..4c01cca --- /dev/null +++ b/web/Miqaat/app/blocks/settings/time-format-toggle.module.css @@ -0,0 +1,76 @@ +.root { + background: var(--color-bg-card); + border-radius: var(--radius-lg); + padding: var(--space-6); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border-light); + transition: transform 0.2s, box-shadow 0.2s; + height: 100%; +} + +.root:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.sectionTitle { + font-size: var(--text-md); + font-weight: 600; + color: var(--color-text); + margin-bottom: var(--space-2); +} + +.sectionDesc { + font-size: var(--text-sm); + color: var(--color-text-muted); + margin-bottom: var(--space-5); +} + +.options { + display: flex; + gap: var(--space-3); + flex-wrap: wrap; +} + +.option { + flex: 1; + min-width: 120px; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); + padding: var(--space-5) var(--space-4); + border-radius: var(--radius-md); + border: 1.5px solid var(--color-border); + cursor: pointer; + transition: border-color 0.2s, background 0.2s; + text-align: center; +} + +.option:hover { + border-color: var(--color-primary); + background: var(--color-highlight); +} + +.optionSelected { + border-color: var(--color-primary); + background: var(--color-highlight); +} + +.preview { + font-family: var(--family-display); + font-size: var(--text-xl); + font-weight: 700; + color: var(--color-primary); +} + +.label { + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-text); +} + +.subLabel { + font-size: var(--text-xs); + color: var(--color-text-muted); +} \ No newline at end of file diff --git a/web/Miqaat/app/blocks/settings/time-format-toggle.tsx b/web/Miqaat/app/blocks/settings/time-format-toggle.tsx new file mode 100644 index 0000000..4296e3c --- /dev/null +++ b/web/Miqaat/app/blocks/settings/time-format-toggle.tsx @@ -0,0 +1,47 @@ +import classnames from "classnames"; +import { useLanguage } from "~/i18n/language-context"; +import style from "./time-format-toggle.module.css"; + +export interface TimeFormatToggleProps { + className?: string; + value: "12h" | "24h"; + onChange: (format: "12h" | "24h") => void; +} + +export function TimeFormatToggle({ className, value, onChange }: TimeFormatToggleProps) { + const { t } = useLanguage(); + return ( +
+
{t("settings.timeFormat")}
+

{t("settings.timeFormat.desc")}

+
+
onChange("12h")} + role="radio" + aria-checked={value === "12h"} + aria-label="12-hour format (AM/PM)" + tabIndex={0} + onKeyDown={(e) => e.key === "Enter" && onChange("12h")} + > +
5:30 PM
+
{t("settings.timeFormat.12h")}
+
{t("settings.timeFormat.12h.desc")}
+
+
onChange("24h")} + role="radio" + aria-checked={value === "24h"} + aria-label="24-hour format (military)" + tabIndex={0} + onKeyDown={(e) => e.key === "Enter" && onChange("24h")} + > +
17:30
+
{t("settings.timeFormat.24h")}
+
{t("settings.timeFormat.24h.desc")}
+
+
+
+ ); +} diff --git a/web/Miqaat/app/components/error-boundary/error-boundary.module.css b/web/Miqaat/app/components/error-boundary/error-boundary.module.css new file mode 100644 index 0000000..7d7d46d --- /dev/null +++ b/web/Miqaat/app/components/error-boundary/error-boundary.module.css @@ -0,0 +1,59 @@ +.errorBoundary { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.errorContainer { + text-align: center; + max-width: 70%; + width: 100%; + padding: 2.5rem; +} + +.errorTitle { + font-weight: bold; + margin-bottom: 0.5rem; + font-size: 3rem; + color: #333; +} + +.errorStackWrapper { + position: relative; +} + +.copyButton { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding: 0.5rem 0.75rem; + background: #e5e5e5; + border: 1px solid #d4d4d4; + border-radius: 0.25rem; + cursor: pointer; + font-size: 0.75rem; + font-weight: 500; + color: #333; +} + +.copyButton:hover { + background: #d4d4d4; +} + +.copyButton:active { + background: #c4c4c4; +} + +.errorStack { + margin-top: 1rem; + text-align: start; + overflow: auto; + font-size: 0.875rem; + padding: 1rem; + background: #f5f5f5; + border-radius: 0.375rem; + border: 1px solid #e5e5e5; + max-height: 20rem; +} diff --git a/web/Miqaat/app/components/error-boundary/error-boundary.tsx b/web/Miqaat/app/components/error-boundary/error-boundary.tsx new file mode 100644 index 0000000..965d1f3 --- /dev/null +++ b/web/Miqaat/app/components/error-boundary/error-boundary.tsx @@ -0,0 +1,42 @@ +import { isRouteErrorResponse } from "react-router"; +import type { Route } from "../../+types/root"; +import styles from "./error-boundary.module.css"; + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = error.status === 404 ? "The requested page could not be found." : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+
+

{message}

+

{details}

+ + {stack && ( +
+ +
+              {stack}
+            
+
+ )} +
+
+ ); +} diff --git a/web/Miqaat/app/components/loading-skeleton/loading-skeleton.tsx b/web/Miqaat/app/components/loading-skeleton/loading-skeleton.tsx new file mode 100644 index 0000000..75b3187 --- /dev/null +++ b/web/Miqaat/app/components/loading-skeleton/loading-skeleton.tsx @@ -0,0 +1,24 @@ +import classnames from "classnames"; + +export interface LoadingSkeletonProps { + className?: string; + /** Number of skeleton rows to render */ + rows?: number; +} + +export function LoadingSkeleton({ className, rows = 6 }: LoadingSkeletonProps) { + return ( +
+
+ {Array.from({ length: rows }).map((_, i) => ( +
+ ))} +
+ ); +} diff --git a/web/Miqaat/app/components/toast/toast.module.css b/web/Miqaat/app/components/toast/toast.module.css new file mode 100644 index 0000000..7cb62ff --- /dev/null +++ b/web/Miqaat/app/components/toast/toast.module.css @@ -0,0 +1,88 @@ +.overlay { + position: fixed; + top: var(--space-4); + left: 50%; + transform: translateX(-50%); + z-index: 9999; + max-width: 90vw; + pointer-events: auto; +} + +.root { + display: flex; + align-items: flex-start; + gap: var(--space-3); + padding: var(--space-4) var(--space-5); + background: var(--color-bg-card); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + animation: slideIn 0.3s ease-out; + max-width: 420px; + width: max-content; +} + +.rootError { + border-color: var(--color-warning); +} + +.rootSuccess { + border-color: var(--color-success); +} + +.icon { + flex-shrink: 0; + margin-top: 0.125rem; +} + +.iconError { + color: var(--color-warning); +} + +.iconSuccess { + color: var(--color-success); +} + +.content { + flex: 1; + min-width: 0; +} + +.title { + font-weight: 600; + font-size: var(--text-base); + color: var(--color-text); + margin-bottom: var(--space-1); +} + +.message { + font-size: var(--text-sm); + color: var(--color-text-muted); + line-height: 1.5; +} + +.closeBtn { + flex-shrink: 0; + background: none; + border: none; + cursor: pointer; + color: var(--color-text-muted); + padding: var(--space-1); + border-radius: var(--radius-sm); + transition: color 0.2s; +} + +.closeBtn:hover { + color: var(--color-text); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-1rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/web/Miqaat/app/components/toast/toast.tsx b/web/Miqaat/app/components/toast/toast.tsx new file mode 100644 index 0000000..a27cac8 --- /dev/null +++ b/web/Miqaat/app/components/toast/toast.tsx @@ -0,0 +1,55 @@ +import { useEffect } from "react"; +import { AlertTriangle, CheckCircle, X } from "lucide-react"; +import classnames from "classnames"; +import styles from "./toast.module.css"; + +export interface ToastProps { + className?: string; + message: string; + title?: string; + type?: "error" | "success"; + duration?: number; + onClose: () => void; +} + +export function Toast({ + className, + message, + title, + type = "error", + duration = 5000, + onClose, +}: ToastProps) { + useEffect(() => { + const timer = setTimeout(onClose, duration); + return () => clearTimeout(timer); + }, [duration, onClose]); + + const isError = type === "error"; + + return ( +
+
+
+ {isError ? : } +
+
+ {title &&
{title}
} +
{message}
+
+ +
+
+ ); +} diff --git a/web/Miqaat/app/context/app-context.tsx b/web/Miqaat/app/context/app-context.tsx new file mode 100644 index 0000000..9995fb6 --- /dev/null +++ b/web/Miqaat/app/context/app-context.tsx @@ -0,0 +1,20 @@ +import { createContext, useContext, type ReactNode } from "react"; +import { usePrayerTimes, type PrayerAppState } from "~/hooks/use-prayer-times"; +import { useNotifications } from "~/hooks/use-notifications"; + +const AppContext = createContext(null); + +export function AppProvider({ children }: { children: ReactNode }) { + const state = usePrayerTimes(); + + // Wire up browser notifications + useNotifications(state.prayerTimes, state.notificationsEnabled, state.notificationPreferences); + + return {children}; +} + +export function useAppContext(): PrayerAppState { + const ctx = useContext(AppContext); + if (!ctx) throw new Error("useAppContext must be used within AppProvider"); + return ctx; +} diff --git a/web/Miqaat/app/data/prayer-data.ts b/web/Miqaat/app/data/prayer-data.ts new file mode 100644 index 0000000..d1f1237 --- /dev/null +++ b/web/Miqaat/app/data/prayer-data.ts @@ -0,0 +1,37 @@ +export type PrayerName = "Fajr" | "Sunrise" | "Duha" | "Dhuhr" | "Asr" | "Maghrib" | "Isha"; + +export interface PrayerTime { + name: PrayerName; + time: string; // "HH:MM" 24h + label: string; + icon: string; + isPrayer: boolean; // false for Sunrise +} + +export interface Location { + city: string; + cityAr: string; + country: string; + countryAr: string; + timezone: string; + latitude: number; + longitude: number; + cityId: number; +} + + + + + + + +export interface Wilaya { + id: number; + cityId: number; + name: string; + nameAr: string; + latitude: number; + longitude: number; +} + + diff --git a/web/Miqaat/app/hooks/use-clock.ts b/web/Miqaat/app/hooks/use-clock.ts new file mode 100644 index 0000000..0eb786b --- /dev/null +++ b/web/Miqaat/app/hooks/use-clock.ts @@ -0,0 +1,128 @@ +import { useState, useEffect } from "react"; +import { formatTime } from "~/utils/time-utils"; + + +export interface ClockState { + now: Date; + timeString: string; + timeOnly: string; + period: string; + dateString: string; + hijriDate: string; +} + +const HIJRI_MONTHS = { + en: [ + "Muharram", + "Safar", + "Rabi\u02bc al-Awwal", + "Rabi\u02bc al-Thani", + "Jumada al-Awwal", + "Jumada al-Thani", + "Rajab", + "Sha\u02bcban", + "Ramadan", + "Shawwal", + "Dhu al-Qi\u02bcda", + "Dhu al-Hijja", + ], + ar: [ + "محرم", + "صفر", + "ربيع الأول", + "ربيع الآخر", + "جمادى الأولى", + "جمادى الآخرة", + "رجب", + "شعبان", + "رمضان", + "شوال", + "ذو القعدة", + "ذو الحجة", + ] +}; + +/** + * Approximate Gregorian-to-Hijri conversion. + * Accurate to within ±1 day for modern dates. + */ +function toHijri(date: Date): { day: number; month: number; year: number } { + const jd = + Math.floor((14 + date.getMonth() + 1) / 12); + const y = date.getFullYear() + 4800 - jd; + const m = date.getMonth() + 1 + 12 * jd - 3; + let julianDay = + date.getDate() + + Math.floor((153 * m + 2) / 5) + + 365 * y + + Math.floor(y / 4) - + Math.floor(y / 100) + + Math.floor(y / 400) - + 32045; + + const l = julianDay - 1948440 + 10632; + const n = Math.floor((l - 1) / 10631); + const ll = l - 10631 * n + 354; + const j = + Math.floor((10985 - ll) / 5316) * Math.floor((50 * ll) / 17719) + + Math.floor(ll / 5670) * Math.floor((43 * ll) / 15238); + const l2 = + ll - + Math.floor((30 - j) / 15) * Math.floor((17719 * j) / 50) - + Math.floor(j / 16) * Math.floor((15238 * j) / 43) + + 29; + const hijriMonth = Math.floor((24 * l2) / 709); + const hijriDay = l2 - Math.floor((709 * hijriMonth) / 24); + const hijriYear = 30 * n + j - 30; + + return { day: hijriDay, month: hijriMonth, year: hijriYear }; +} + +export function useClock(timeFormat: "12h" | "24h", locale: "en" | "ar" = "en"): ClockState { + const [now, setNow] = useState(new Date()); + + useEffect(() => { + const tick = setInterval(() => setNow(new Date()), 1000); + return () => clearInterval(tick); + }, []); + + const timeParts = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; + const timeString = formatTime(timeParts, timeFormat, locale); + + const h = now.getHours(); + const m = now.getMinutes(); + const s = now.getSeconds(); + + let timeOnly = ""; + if (timeFormat === "24h") { + timeOnly = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; + } else { + const h12 = h % 12 || 12; + timeOnly = `${h12}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; + } + + let period = ""; + if (timeFormat === "12h") { + if (locale === "ar") { + period = h >= 12 ? "مساءً" : "صباحاً"; + } else { + period = h >= 12 ? "PM" : "AM"; + } + } + + const dateString = now.toLocaleDateString(locale === "ar" ? "ar-DZ" : "en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + + const hijri = toHijri(now); + const monthName = HIJRI_MONTHS[locale][hijri.month - 1] ?? ""; + const weekdayName = now.toLocaleDateString(locale === "ar" ? "ar-DZ" : "en-US", { weekday: "long" }); + const hijriDate = locale === "ar" + ? `${weekdayName}، ${hijri.day} ${monthName} ${hijri.year} هـ` + : `${weekdayName}, ${hijri.day} ${monthName} ${hijri.year} AH`; + + return { now, timeString, timeOnly, period, dateString, hijriDate }; +} diff --git a/web/Miqaat/app/hooks/use-countdown.ts b/web/Miqaat/app/hooks/use-countdown.ts new file mode 100644 index 0000000..f8740e8 --- /dev/null +++ b/web/Miqaat/app/hooks/use-countdown.ts @@ -0,0 +1,39 @@ +import { useState, useEffect } from "react"; + +export interface CountdownState { + hours: string; + minutes: string; + seconds: string; + totalSeconds: number; +} + +/** + * Calculates and updates countdown to a target time ("HH:MM" 24h). + */ +export function useCountdown(targetTime: string | null): CountdownState { + const [remaining, setRemaining] = useState(0); + + useEffect(() => { + if (!targetTime) return; + + const calc = () => { + const now = new Date(); + const [h, m] = targetTime.split(":").map(Number); + const target = new Date(); + target.setHours(h, m, 0, 0); + let diff = Math.floor((target.getTime() - now.getTime()) / 1000); + if (diff < 0) diff += 86400; // next day + setRemaining(diff); + }; + + calc(); + const interval = setInterval(calc, 1000); + return () => clearInterval(interval); + }, [targetTime]); + + const hours = String(Math.floor(remaining / 3600)).padStart(2, "0"); + const minutes = String(Math.floor((remaining % 3600) / 60)).padStart(2, "0"); + const seconds = String(remaining % 60).padStart(2, "0"); + + return { hours, minutes, seconds, totalSeconds: remaining }; +} diff --git a/web/Miqaat/app/hooks/use-location-selection.ts b/web/Miqaat/app/hooks/use-location-selection.ts new file mode 100644 index 0000000..f286405 --- /dev/null +++ b/web/Miqaat/app/hooks/use-location-selection.ts @@ -0,0 +1,121 @@ +import { useState, useEffect, useCallback } from "react"; +import type { Wilaya, Location } from "~/data/prayer-data"; +import { fetchWilayas, findNearestLocation } from "~/services/api"; + +export interface LocationSelectionState { + wilayas: Wilaya[]; + selectedWilaya: Wilaya | null; + isLoadingWilayas: boolean; + isDetectingGPS: boolean; + gpsError: string | null; + fetchError: string | null; + selectWilaya: (wilaya: Wilaya | null) => void; + clearSelection: () => void; + detectGPSLocation: () => void; + clearGpsError: () => void; + retryFetch: () => void; + getSelectedLocation: () => Location | null; +} + +export function useLocationSelection(): LocationSelectionState { + const [wilayas, setWilayas] = useState([]); + const [selectedWilaya, setSelectedWilaya] = useState(null); + const [isLoadingWilayas, setIsLoadingWilayas] = useState(true); + const [isDetectingGPS, setIsDetectingGPS] = useState(false); + const [gpsError, setGpsError] = useState(null); + const [fetchError, setFetchError] = useState(null); + + const loadWilayas = useCallback(async (cancelled = false) => { + setIsLoadingWilayas(true); + setFetchError(null); + try { + const data = await fetchWilayas(); + if (!cancelled) setWilayas(data); + } catch { + if (!cancelled) setFetchError("Failed to load cities. Please check your connection and try again."); + } finally { + if (!cancelled) setIsLoadingWilayas(false); + } + }, []); + + // Fetch wilayas on mount + useEffect(() => { + let cancelled = false; + loadWilayas(cancelled); + return () => { cancelled = true; }; + }, [loadWilayas]); + + const retryFetch = useCallback(() => { + loadWilayas(); + }, [loadWilayas]); + + const selectWilaya = useCallback((wilaya: Wilaya | null) => { + setSelectedWilaya(wilaya); + setGpsError(null); + }, []); + + const clearSelection = useCallback(() => { + setSelectedWilaya(null); + setGpsError(null); + }, []); + + const clearGpsError = useCallback(() => { + setGpsError(null); + }, []); + + const detectGPSLocation = useCallback(() => { + if (!navigator.geolocation) { + setGpsError("GPS is not available on this device. Please select your Wilaya manually."); + return; + } + setIsDetectingGPS(true); + setGpsError(null); + navigator.geolocation.getCurrentPosition( + async (pos) => { + try { + const result = await findNearestLocation(pos.coords.latitude, pos.coords.longitude); + if (result) { + setSelectedWilaya(result.wilaya); + } + } catch { + setGpsError("Could not determine your location. Please select your Wilaya manually."); + } finally { + setIsDetectingGPS(false); + } + }, + () => { + setIsDetectingGPS(false); + setGpsError("Location access was denied. Please select your Wilaya from the dropdown below."); + }, + ); + }, []); + + const getSelectedLocation = useCallback((): Location | null => { + if (!selectedWilaya) return null; + return { + city: selectedWilaya.name, + cityAr: selectedWilaya.nameAr, + country: "Algeria", + countryAr: "الجزائر", + timezone: "Africa/Algiers", + latitude: selectedWilaya.latitude, + longitude: selectedWilaya.longitude, + cityId: selectedWilaya.cityId, + }; + }, [selectedWilaya]); + + return { + wilayas, + selectedWilaya, + isLoadingWilayas, + isDetectingGPS, + gpsError, + fetchError, + selectWilaya, + clearSelection, + detectGPSLocation, + clearGpsError, + retryFetch, + getSelectedLocation, + }; +} diff --git a/web/Miqaat/app/hooks/use-notification-permission.ts b/web/Miqaat/app/hooks/use-notification-permission.ts new file mode 100644 index 0000000..6f7274c --- /dev/null +++ b/web/Miqaat/app/hooks/use-notification-permission.ts @@ -0,0 +1,52 @@ +import { useState, useEffect, useCallback } from "react"; + +export type PermissionStatus = NotificationPermission | "unsupported"; + +/** + * Hook to manage and request browser notification permissions. + * Provides a clean interface for UI components to handle different permission states. + */ +export function useNotificationPermission() { + const [permission, setPermission] = useState("default"); + + const updatePermission = useCallback(() => { + if (typeof window === "undefined" || !("Notification" in window)) { + setPermission("unsupported"); + return; + } + setPermission(Notification.permission); + }, []); + + useEffect(() => { + updatePermission(); + + // Some browsers allow changing permissions without reload (rare but possible via settings) + // We can't easily listen to permission changes, but we check on focus. + window.addEventListener("focus", updatePermission); + return () => window.removeEventListener("focus", updatePermission); + }, [updatePermission]); + + const requestPermission = useCallback(async () => { + if (typeof window === "undefined" || !("Notification" in window)) { + return "unsupported"; + } + + if (Notification.permission === "denied") { + return "denied"; + } + + const result = await Notification.requestPermission(); + setPermission(result); + return result; + }, []); + + return { + permission, + isGranted: permission === "granted", + isDenied: permission === "denied", + isUnsupported: permission === "unsupported", + isDefault: permission === "default", + requestPermission, + refresh: updatePermission, + }; +} diff --git a/web/Miqaat/app/hooks/use-notifications.ts b/web/Miqaat/app/hooks/use-notifications.ts new file mode 100644 index 0000000..46acf5f --- /dev/null +++ b/web/Miqaat/app/hooks/use-notifications.ts @@ -0,0 +1,85 @@ +import { useEffect, useRef, useCallback } from "react"; +import type { PrayerTime } from "~/data/prayer-data"; + +/** + * Robust frontend notification scheduler. + * Uses periodic polling and visibility checks to ensure notifications fire + * even if the browser throttles background timers. + * + * Note: Since this is a frontend-only React app without a Service Worker push implementation, + * notifications will only work while the tab is open. + */ +export function useNotifications( + prayerTimes: PrayerTime[], + enabled: boolean, + preferences: Record = {}, +): void { + const firedNotifications = useRef>(new Set()); + + const checkAndFireNotifications = useCallback(() => { + if (!enabled || prayerTimes.length === 0) return; + if (typeof window === "undefined" || !("Notification" in window)) return; + if (Notification.permission !== "granted") return; + + const now = new Date(); + const nowMs = now.getTime(); + const todayStr = now.toISOString().split("T")[0]; // Use local date string + + for (const prayer of prayerTimes) { + if (!prayer.isPrayer) continue; // skip Sunrise + if (preferences[prayer.name] === false) continue; + + const [h, m] = prayer.time.split(":").map(Number); + const target = new Date(); + target.setHours(h, m, 0, 0); + + // Notify 1 minute before + const notifyMs = target.getTime() - 60_000; + + // Delay is positive if we've passed the target notification time + const delay = nowMs - notifyMs; + + const uniqueKey = `${todayStr}-${prayer.name}`; + + // If we are past the notification time but within a 5-minute grace period + if (delay >= 0 && delay < 5 * 60_000) { + if (!firedNotifications.current.has(uniqueKey)) { + new Notification("Prayer Time Approaching", { + body: `${prayer.name} prayer in 1 minute`, + icon: "/favicon.svg", + tag: `prayer-${prayer.name}`, + }); + firedNotifications.current.add(uniqueKey); + } + } + } + }, [prayerTimes, enabled, preferences]); + + useEffect(() => { + if (!enabled) return; + + // Run an initial check immediately + checkAndFireNotifications(); + + // Use setInterval (10 seconds) instead of setTimeout to combat browser throttling + // This acts as a time-sync mechanism that naturally checks the system clock. + const intervalId = setInterval(checkAndFireNotifications, 10_000); + + // Add focus listeners to instantly catch up on missed notifications + // when the user switches back to the tab. + const handleVisibility = () => { + if (document.visibilityState === "visible") { + checkAndFireNotifications(); + } + }; + + window.addEventListener("focus", checkAndFireNotifications); + document.addEventListener("visibilitychange", handleVisibility); + + return () => { + clearInterval(intervalId); + window.removeEventListener("focus", checkAndFireNotifications); + document.removeEventListener("visibilitychange", handleVisibility); + }; + }, [checkAndFireNotifications, enabled]); +} diff --git a/web/Miqaat/app/hooks/use-prayer-status.ts b/web/Miqaat/app/hooks/use-prayer-status.ts new file mode 100644 index 0000000..69f20e6 --- /dev/null +++ b/web/Miqaat/app/hooks/use-prayer-status.ts @@ -0,0 +1,52 @@ +import { useState, useEffect } from "react"; +import type { PrayerTime } from "~/data/prayer-data"; + +export type PrayerStatus = "forbidden" | "duha" | null; + +/** + * Determines the current prayer status based on Sunrise and Dhuhr times. + * + * Timeline: + * - Sunrise → Sunrise + 15 min: "forbidden" (nafl prayer prohibited) + * - Sunrise + 15 min → Dhuhr - 15 min: "duha" (Duha prayer time) + * - Dhuhr - 15 min → Dhuhr: "forbidden" (nafl prayer prohibited) + * - Otherwise: null + */ +export function usePrayerStatus(prayerTimes: PrayerTime[]): PrayerStatus { + const [status, setStatus] = useState(null); + + useEffect(() => { + if (prayerTimes.length === 0) return; + + const sunrise = prayerTimes.find((p) => p.name === "Sunrise"); + const dhuhr = prayerTimes.find((p) => p.name === "Dhuhr"); + if (!sunrise || !dhuhr) return; + + const [sunH, sunM] = sunrise.time.split(":").map(Number); + const sunriseMinutes = sunH * 60 + sunM; + + const [dhuH, dhuM] = dhuhr.time.split(":").map(Number); + const dhuhrMinutes = dhuH * 60 + dhuM; + + const calc = () => { + const now = new Date(); + const nowMinutes = now.getHours() * 60 + now.getMinutes(); + + if (nowMinutes >= sunriseMinutes && nowMinutes < sunriseMinutes + 15) { + setStatus("forbidden"); + } else if (nowMinutes >= sunriseMinutes + 15 && nowMinutes < dhuhrMinutes - 15) { + setStatus("duha"); + } else if (nowMinutes >= dhuhrMinutes - 15 && nowMinutes < dhuhrMinutes) { + setStatus("forbidden"); + } else { + setStatus(null); + } + }; + + calc(); + const interval = setInterval(calc, 15000); // re-check every 15 seconds + return () => clearInterval(interval); + }, [prayerTimes]); + + return status; +} diff --git a/web/Miqaat/app/hooks/use-prayer-times.ts b/web/Miqaat/app/hooks/use-prayer-times.ts new file mode 100644 index 0000000..81bee20 --- /dev/null +++ b/web/Miqaat/app/hooks/use-prayer-times.ts @@ -0,0 +1,193 @@ +import { useState, useEffect, useCallback } from "react"; +import type { PrayerTime, Location } from "~/data/prayer-data"; +import { fetchPrayerTimes } from "~/services/api"; +import { useLanguage } from "~/i18n/language-context"; + +const STORAGE_KEY_LOCATION = "prayerApp_location"; +const STORAGE_KEY_FORMAT = "prayerApp_timeFormat"; +const STORAGE_KEY_NOTIFICATIONS = "prayerApp_notifications"; +const STORAGE_KEY_NOTIFICATION_PREFS = "prayerApp_notificationPrefs"; + +const DEFAULT_LOCATION: Location = { + city: "Algiers", + cityAr: "الجزائر", + country: "Algeria", + countryAr: "الجزائر", + timezone: "Africa/Algiers", + latitude: 36.75, + longitude: 3.06, + cityId: 27, +}; + + + +function loadFromStorage(key: string, fallback: T): T { + try { + const raw = localStorage.getItem(key); + if (!raw) return fallback; + const parsed = JSON.parse(raw); + + // For location, ensure new fields exist for backward compatibility + if (key === STORAGE_KEY_LOCATION) { + return { ...fallback, ...parsed } as T; + } + + return parsed as T; + } catch { + return fallback; + } +} + +function saveToStorage(key: string, value: T): void { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch { + // ignore + } +} + +export interface PrayerAppState { + location: Location; + timeFormat: "12h" | "24h"; + prayerTimes: PrayerTime[]; + currentPrayer: PrayerTime | null; + nextPrayer: PrayerTime | null; + isLoading: boolean; + error: string | null; + notificationsEnabled: boolean; + notificationPreferences: Record; + setLocation: (loc: Location) => void; + setTimeFormat: (f: "12h" | "24h") => void; + setNotificationsEnabled: (enabled: boolean) => void; + setNotificationPreference: (prayerName: string, enabled: boolean) => void; + refreshPrayerTimes: () => Promise; +} + +function determineCurrent( + times: PrayerTime[], +): { current: PrayerTime | null; next: PrayerTime | null } { + const now = new Date(); + const nowMinutes = now.getHours() * 60 + now.getMinutes(); + const prayers = times; // Use all events including Sunrise and Duha as timeline boundaries + let current: PrayerTime | null = null; + let next: PrayerTime | null = null; + + for (let i = 0; i < prayers.length; i++) { + const [h, m] = prayers[i].time.split(":").map(Number); + const prayerMinutes = h * 60 + m; + const nextPrayerMinutes = + i + 1 < prayers.length + ? (() => { + const [nh, nm] = prayers[i + 1].time.split(":").map(Number); + return nh * 60 + nm; + })() + : 24 * 60; + + if (nowMinutes >= prayerMinutes && nowMinutes < nextPrayerMinutes) { + current = prayers[i]; + next = i + 1 < prayers.length ? prayers[i + 1] : prayers[0]; + break; + } + } + + if (!current) { + current = prayers[prayers.length - 1] ?? null; + next = prayers[0] ?? null; + } + + return { current, next }; +} + +export function usePrayerTimes(): PrayerAppState { + const { t } = useLanguage(); + const [location, setLocationState] = useState( + () => loadFromStorage(STORAGE_KEY_LOCATION, DEFAULT_LOCATION), + ); + const [timeFormat, setFormatState] = useState<"12h" | "24h">( + () => loadFromStorage<"12h" | "24h">(STORAGE_KEY_FORMAT, "12h"), + ); + const [notificationsEnabled, setNotificationsEnabledState] = useState( + () => loadFromStorage(STORAGE_KEY_NOTIFICATIONS, false), + ); + const [notificationPreferences, setNotificationPreferencesState] = useState>( + () => loadFromStorage>(STORAGE_KEY_NOTIFICATION_PREFS, {}), + ); + + const [prayerTimes, setPrayerTimes] = useState([]); + const [currentPrayer, setCurrentPrayer] = useState(null); + const [nextPrayer, setNextPrayer] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const loadPrayerTimes = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const times = await fetchPrayerTimes(location.cityId); + setPrayerTimes(times); + const { current, next } = determineCurrent(times); + setCurrentPrayer(current); + setNextPrayer(next); + } catch { + setError(t("error.fetchPrayer") ?? "Failed to fetch prayer times. Please try again."); + } finally { + setIsLoading(false); + } + }, [location, t]); + + // Fetch on mount and when location changes + useEffect(() => { + loadPrayerTimes(); + }, [loadPrayerTimes]); + + // Recalculate current/next prayer every minute + useEffect(() => { + if (prayerTimes.length === 0) return; + const interval = setInterval(() => { + const { current, next } = determineCurrent(prayerTimes); + setCurrentPrayer(current); + setNextPrayer(next); + }, 60000); + return () => clearInterval(interval); + }, [prayerTimes]); + + const setLocation = (loc: Location) => { + setLocationState(loc); + saveToStorage(STORAGE_KEY_LOCATION, loc); + }; + + + + const setTimeFormat = (f: "12h" | "24h") => { + setFormatState(f); + saveToStorage(STORAGE_KEY_FORMAT, f); + }; + + const setNotificationsEnabled = (enabled: boolean) => { + setNotificationsEnabledState(enabled); + saveToStorage(STORAGE_KEY_NOTIFICATIONS, enabled); + }; + + const setNotificationPreference = (prayerName: string, enabled: boolean) => { + const newPrefs = { ...notificationPreferences, [prayerName]: enabled }; + setNotificationPreferencesState(newPrefs); + saveToStorage(STORAGE_KEY_NOTIFICATION_PREFS, newPrefs); + }; + + return { + location, + timeFormat, + prayerTimes, + currentPrayer, + nextPrayer, + isLoading, + error, + notificationsEnabled, + notificationPreferences, + setLocation, + setTimeFormat, + setNotificationsEnabled, + setNotificationPreference, + refreshPrayerTimes: loadPrayerTimes, + }; +} diff --git a/web/Miqaat/app/hooks/use-settings-draft.ts b/web/Miqaat/app/hooks/use-settings-draft.ts new file mode 100644 index 0000000..82707f0 --- /dev/null +++ b/web/Miqaat/app/hooks/use-settings-draft.ts @@ -0,0 +1,109 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import { useColorScheme } from "@dazl/color-scheme/react"; +import type { Locale } from "~/i18n/translations"; + +type ColorScheme = "light" | "dark" | "system"; + +/** + * Hook to manage draft settings state before persisting to global context/localStorage. + * Now includes Language and Theme to ensure a unified "Save" experience. + */ +export function useSettingsDraft() { + const context = useAppContext(); + const { locale, setLocale } = useLanguage(); + const { configScheme, setColorScheme } = useColorScheme(); + + // Draft state initialized from current values + const [draftFormat, setDraftFormat] = useState(context.timeFormat); + const [draftNotifications, setDraftNotifications] = useState(context.notificationsEnabled); + const [draftPrefs, setDraftPrefs] = useState(context.notificationPreferences); + const [draftLocale, setDraftLocale] = useState(locale); + const [draftTheme, setDraftTheme] = useState(configScheme); + + // Sync draft with global state if it changes externally + useEffect(() => { + setDraftFormat(context.timeFormat); + setDraftNotifications(context.notificationsEnabled); + setDraftPrefs(context.notificationPreferences); + setDraftLocale(locale); + setDraftTheme(configScheme); + }, [context.timeFormat, context.notificationsEnabled, context.notificationPreferences, locale, configScheme]); + + // Determine if there are unsaved changes across ALL settings + const isDirty = useMemo(() => { + const isFormatDirty = draftFormat !== context.timeFormat; + const isNotifDirty = draftNotifications !== context.notificationsEnabled; + const isLocaleDirty = draftLocale !== locale; + const isThemeDirty = draftTheme !== configScheme; + const isPrefsDirty = JSON.stringify(draftPrefs) !== JSON.stringify(context.notificationPreferences); + + return isFormatDirty || isNotifDirty || isLocaleDirty || isThemeDirty || isPrefsDirty; + }, [draftFormat, draftNotifications, draftPrefs, draftLocale, draftTheme, context, locale, configScheme]); + + /** + * Persists ALL draft settings to their respective global providers. + */ + const save = async () => { + try { + // Premium feedback delay + await new Promise((resolve) => setTimeout(resolve, 800)); + + // Persist to context / storage + context.setTimeFormat(draftFormat); + context.setNotificationsEnabled(draftNotifications); + setLocale(draftLocale); + setColorScheme(draftTheme); + + // Batch sync prayer-specific notification preferences + Object.entries(draftPrefs).forEach(([prayer, enabled]) => { + if (context.notificationPreferences[prayer] !== enabled) { + context.setNotificationPreference(prayer, enabled); + } + }); + + return { success: true }; + } catch (error) { + console.error("Failed to save settings:", error); + return { success: false, error: "Critical error: Could not save settings to storage." }; + } + }; + + const discard = useCallback(() => { + setDraftFormat(context.timeFormat); + setDraftNotifications(context.notificationsEnabled); + setDraftPrefs(context.notificationPreferences); + setDraftLocale(locale); + setDraftTheme(configScheme); + }, [context, locale, configScheme]); + + const resetToDefaults = useCallback(() => { + setDraftFormat("12h"); + setDraftNotifications(false); + setDraftPrefs({}); + setDraftLocale("en"); + setDraftTheme("system"); + }, []); + + const setNotificationPreference = useCallback((prayerName: string, enabled: boolean) => { + setDraftPrefs((prev) => ({ ...prev, [prayerName]: enabled })); + }, []); + + return { + draftFormat, + setDraftFormat, + draftNotifications, + setDraftNotifications, + draftPrefs, + setNotificationPreference, + draftLocale, + setDraftLocale, + draftTheme, + setDraftTheme, + isDirty, + save, + discard, + resetToDefaults, + }; +} diff --git a/web/Miqaat/app/i18n/language-context.tsx b/web/Miqaat/app/i18n/language-context.tsx new file mode 100644 index 0000000..b3b856e --- /dev/null +++ b/web/Miqaat/app/i18n/language-context.tsx @@ -0,0 +1,63 @@ +import { createContext, useContext, useState, useCallback, type ReactNode } from "react"; +import { translations, type Locale, type TranslationKey } from "./translations"; + +const STORAGE_KEY = "prayerApp_language"; + +interface LanguageContextValue { + locale: Locale; + dir: "ltr" | "rtl"; + setLocale: (locale: Locale) => void; + t: (key: TranslationKey, vars?: Record) => string; +} + +const LanguageContext = createContext(null); + +function loadLocale(): Locale { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw === "ar" || raw === "en") return raw; + } catch { + // ignore + } + return "en"; +} + +export function LanguageProvider({ children }: { children: ReactNode }) { + const [locale, setLocaleState] = useState(loadLocale); + + const setLocale = useCallback((l: Locale) => { + setLocaleState(l); + try { + localStorage.setItem(STORAGE_KEY, l); + } catch { + // ignore + } + }, []); + + const dir = locale === "ar" ? "rtl" : "ltr"; + + const t = useCallback( + (key: TranslationKey, vars?: Record): string => { + let str = translations[locale][key] ?? translations.en[key] ?? key; + if (vars) { + for (const [k, v] of Object.entries(vars)) { + str = str.replace(`{${k}}`, v); + } + } + return str; + }, + [locale], + ); + + return ( + + {children} + + ); +} + +export function useLanguage(): LanguageContextValue { + const ctx = useContext(LanguageContext); + if (!ctx) throw new Error("useLanguage must be used within LanguageProvider"); + return ctx; +} diff --git a/web/Miqaat/app/i18n/translations.ts b/web/Miqaat/app/i18n/translations.ts new file mode 100644 index 0000000..ac3c8a2 --- /dev/null +++ b/web/Miqaat/app/i18n/translations.ts @@ -0,0 +1,249 @@ +export type Locale = "en" | "ar"; + +export type TranslationKey = keyof typeof en; + +const en = { + // Navigation + "nav.home": "Home", + "nav.location": "Location", + "nav.calendar": "Calendar", + "nav.settings": "Settings", + "nav.logo.prayer": "miqaat", + "nav.logo.times": "", + + // Location page + "location.title": "Choose Location", + "location.subtitle": "Select your Wilaya in Algeria to get accurate prayer times.", + "location.wilaya.label": "Wilaya (Province)", + "location.wilaya.placeholder": "— Select a Wilaya —", + "location.wilaya.loading": "Loading wilayas…", + "location.confirm": "Confirm Location", + "location.or": "OR", + "location.gps.detect": "Use my GPS location", + "location.gps.detecting": "Detecting location...", + "location.gps.success": "Location detected!", + "location.gps.error.title": "GPS Unavailable", + "location.gps.error.unavailable": "GPS is not available on this device. Please select your Wilaya manually.", + "location.gps.error.failed": "Could not determine your location. Please select your Wilaya manually.", + "location.gps.error.denied": "Location access was denied. Please select your Wilaya from the dropdown below.", + "location.change": "Change Location", + + // Home page + "home.currentPrayer": "Current Prayer", + "home.prayerTime": "Prayer Time", + "home.nextPrayer": "Next Prayer", + "home.hours": "Hours", + "home.minutes": "Minutes", + "home.seconds": "Seconds", + "home.startsAt": "Starts at", + "home.schedule": "Today's Prayer Schedule", + "home.now": "Now", + "home.forbiddenPrayer": "Forbidden prayer times (except fard)", + "home.duha": "Prayer of Duha", + + // Prayer names + "prayer.Fajr": "Fajr", + "prayer.Sunrise": "Sunrise", + "prayer.Duha": "Duha", + "prayer.Dhuhr": "Dhuhr", + "prayer.Asr": "Asr", + "prayer.Maghrib": "Maghrib", + "prayer.Isha": "Isha", + + // Additional info + "info.sunrise": "Sunrise", + "info.sunset": "Sunset", + "info.hijriDate": "Hijri Date", + "info.timezone": "Timezone", + "info.midnight": "Midnight", + "info.lastThird": "Last Third of the Night", + + // Calendar page + "calendar.title": "Prayer Times Calendar", + "calendar.weekTitle": "Weekly Prayer Calendar", + "calendar.day": "Day", + "calendar.today": "Today", + "calendar.weekly": "Weekly", + "calendar.monthly": "Monthly", + "calendar.print": "Print", + "calendar.error": "Failed to load calendar data. Please check your connection.", + + // Settings page + "settings.title": "Settings", + + "settings.theme": "Appearance", + "settings.theme.desc": "Choose the app appearance. \"System\" follows your device settings.", + "settings.theme.light": "Light", + "settings.theme.dark": "Dark", + "settings.theme.system": "System", + "settings.timeFormat": "Time Format", + "settings.timeFormat.desc": "Choose how prayer times are displayed throughout the app.", + "settings.timeFormat.12h": "12-Hour", + "settings.timeFormat.12h.desc": "AM / PM format", + "settings.timeFormat.24h": "24-Hour", + "settings.timeFormat.24h.desc": "Military format", + "settings.notifications": "Prayer Notifications", + "settings.notifications.desc": "Receive a notification 1 minute before each prayer", + "settings.notifications.enabled": "Enabled", + "settings.notifications.disabled": "Disabled", + "settings.notifications.denied": "Notifications blocked by browser", + "settings.notifications.unsupported": "Notifications are not supported in this browser", + "settings.notifications.limitation": "Note: Notifications only work while the app is open in a tab.", + "settings.notifications.scheduled": "Notifications scheduled for today's prayers.", + "settings.notifications.blocked.title": "Notifications Blocked", + "settings.notifications.blocked.guide": "Your browser is blocking notifications. Please click the lock icon in your address bar to allow them for this site.", + + "settings.language": "Language", + "settings.language.desc": "Choose your preferred language for the app interface.", + "settings.save": "Save Settings", + "settings.reset": "Reset to Defaults", + "settings.unsavedChanges": "You have unsaved changes", + "settings.saving": "Saving...", + "settings.saved": "Settings saved successfully", + "settings.reset.success": "Settings reset to defaults", + + + // Footer + "footer.privacy": "Privacy Policy", + "footer.terms": "Terms of Service", + "footer.about": "About", + "footer.links": "Links", + "footer.contact": "Contact", + "footer.slogan": "A site to display prayer times in Algerian Wilayas", + "footer.contactDesc": "You can leave a message for me", + "footer.sendMessage": "Send a Message", + "footer.subCopy": "Miqaat - Prayer times in Algeria", + "footer.copy": "PrayerTimes App. All rights reserved.", + + // Notifications + "notification.title": "Prayer Time Approaching", + "notification.body": "{prayer} prayer in 1 minute", + + // Errors + "error.fetchPrayer": "Failed to fetch prayer times. Please check your connection.", + "error.retry": "Try Again", +} as const; + +const ar: Record = { + // Navigation + "nav.home": "الرئيسية", + "nav.location": "الموقع", + "nav.calendar": "التقويم", + "nav.settings": "الإعدادات", + "nav.logo.prayer": "ميقات", + "nav.logo.times": "", + + // Location page + "location.title": "اختر الموقع", + "location.subtitle": "اختر ولايتك لتحصل على اوقات الصلاة حسب مركز الولاية", + "location.wilaya.label": "الولاية", + "location.wilaya.placeholder": "— اختر ولاية —", + "location.wilaya.loading": "جاري تحميل الولايات…", + "location.confirm": "تأكيد الموقع", + "location.or": "أو", + "location.gps.detect": "استخدم موقعي GPS", + "location.gps.detecting": "جاري تحديد الموقع...", + "location.gps.success": "تم تحديد الموقع!", + "location.gps.error.title": "GPS غير متوفر", + "location.gps.error.unavailable": "خدمة GPS غير متوفرة على هذا الجهاز. يرجى اختيار ولايتك يدوياً.", + "location.gps.error.failed": "تعذر تحديد موقعك. يرجى اختيار ولايتك يدوياً.", + "location.gps.error.denied": "تم رفض الوصول إلى الموقع. يرجى اختيار ولايتك من القائمة أدناه.", + "location.change": "تغيير الموقع", + + // Home page + "home.currentPrayer": "الصلاة الحالية", + "home.prayerTime": "وقت الصلاة", + "home.nextPrayer": "الصلاة القادمة", + "home.hours": "ساعات", + "home.minutes": "دقائق", + "home.seconds": "ثواني", + "home.startsAt": "تبدأ في", + "home.schedule": "مواقيت الصلاة اليوم", + "home.now": "الآن", + "home.forbiddenPrayer": "وقت نهي (الا الفريضة)", + "home.duha": "صلاة الضحى", + + // Prayer names + "prayer.Fajr": "الفجر", + "prayer.Sunrise": "الشروق", + "prayer.Duha": "الضحى", + "prayer.Dhuhr": "الظهر", + "prayer.Asr": "العصر", + "prayer.Maghrib": "المغرب", + "prayer.Isha": "العشاء", + + // Additional info + "info.sunrise": "الشروق", + "info.sunset": "الغروب", + "info.hijriDate": "التاريخ الهجري", + "info.timezone": "المنطقة الزمنية", + "info.midnight": "منتصف الليل", + "info.lastThird": "ثلث الليل الأخير", + + // Calendar page + "calendar.title": "تقويم أوقات الصلاة", + "calendar.weekTitle": "تقويم الصلاة الأسبوعي", + "calendar.day": "اليوم", + "calendar.today": "اليوم", + "calendar.weekly": "أسبوعي", + "calendar.monthly": "شهري", + "calendar.print": "طباعة", + "calendar.error": "فشل في تحميل بيانات التقويم. يرجى التحقق من الاتصال.", + + // Settings page + "settings.title": "الإعدادات", + + "settings.theme": "المظهر", + "settings.theme.desc": "اختر مظهر التطبيق. \"النظام\" يتبع إعدادات جهازك.", + "settings.theme.light": "فاتح", + "settings.theme.dark": "داكن", + "settings.theme.system": "النظام", + "settings.timeFormat": "تنسيق الوقت", + "settings.timeFormat.desc": "اختر كيف يتم عرض أوقات الصلاة في التطبيق.", + "settings.timeFormat.12h": "12 ساعة", + "settings.timeFormat.12h.desc": "تنسيق صباحي/مسائي", + "settings.timeFormat.24h": "24 ساعة", + "settings.timeFormat.24h.desc": "تنسيق 24 ساعة", + "settings.notifications": "إشعارات الصلاة", + "settings.notifications.desc": "تلقي إشعار قبل دقيقة واحدة من كل صلاة", + "settings.notifications.enabled": "مفعّل", + "settings.notifications.disabled": "معطّل", + "settings.notifications.denied": "الإشعارات محظورة من المتصفح", + "settings.notifications.unsupported": "الإشعارات غير مدعومة في هذا المتصفح", + "settings.notifications.limitation": "ملاحظة: تعمل الإشعارات فقط أثناء فتح التطبيق في علامة تبويب.", + "settings.notifications.scheduled": "تمت جدولة الإشعارات لصلوات اليوم.", + "settings.notifications.blocked.title": "الإشعارات محظورة", + "settings.notifications.blocked.guide": "متصفحك يحظر الإشعارات. يرجى الضغط على أيقونة القفل في شريط العنوان للسماح بالتنبيهات لهذا الموقع.", + + "settings.language": "اللغة", + "settings.language.desc": "اختر لغتك المفضلة لواجهة التطبيق.", + "settings.save": "حفظ الإعدادات", + "settings.reset": "إعادة تعيين", + "settings.unsavedChanges": "لديك تغييرات غير محفوظة", + "settings.saving": "جاري الحفظ...", + "settings.saved": "تم حفظ الإعدادات بنجاح", + "settings.reset.success": "تمت إعادة الإعدادات الافتراضية", + + + // Footer + "footer.privacy": "سياسة الخصوصية", + "footer.terms": "شروط الخدمة", + "footer.about": "حول", + "footer.links": "روابط", + "footer.contact": "تواصل", + "footer.slogan": "موقع لعرض أوقات الصلاة في ولايات الجزائر", + "footer.contactDesc": "يمكنك ترك رسالة لي", + "footer.sendMessage": "أرسل رسالة", + "footer.subCopy": "ميقات - أوقات الصلاة في الجزائر", + "footer.copy": "جميع الحقوق محفوظة.", + + // Notifications + "notification.title": "اقتراب وقت الصلاة", + "notification.body": "صلاة {prayer} بعد دقيقة واحدة", + + // Errors + "error.fetchPrayer": "فشل في تحميل بيانات التقويم. يرجى التحقق من الاتصال.", + "error.retry": "إعادة المحاولة", +}; + +export const translations: Record> = { en, ar }; diff --git a/web/Miqaat/app/root.module.css b/web/Miqaat/app/root.module.css new file mode 100644 index 0000000..c1e6227 --- /dev/null +++ b/web/Miqaat/app/root.module.css @@ -0,0 +1,12 @@ +.body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--family-body); + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.pageContent { + flex: 1; +} diff --git a/web/Miqaat/app/root.tsx b/web/Miqaat/app/root.tsx new file mode 100644 index 0000000..4a4ef4d --- /dev/null +++ b/web/Miqaat/app/root.tsx @@ -0,0 +1,69 @@ +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; +import type { Route } from "./+types/root"; +import colorSchemeApi from "@dazl/color-scheme/client?url"; +import { ErrorBoundary as ErrorBoundaryRoot } from "~/components/error-boundary/error-boundary"; +import "./styles/reset.css"; +import "./styles/global.css"; +import "./styles/theme.css"; +import { useColorScheme } from "@dazl/color-scheme/react"; + +import styles from "./root.module.css"; +import { NavigationHeader } from "./blocks/__global/navigation-header"; +import { FooterInformation } from "./blocks/__global/footer-information"; +import { AppProvider } from "./context/app-context"; +import { LanguageProvider, useLanguage } from "./i18n/language-context"; + +export const links: Route.LinksFunction = () => [ + { rel: "icon", href: "/logo.jpg", type: "image/jpeg" }, + { rel: "manifest", href: "/manifest.json" }, + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600;700&family=DM+Sans:wght@400;500;600&family=Roboto+Mono:wght@400;500;600;700&family=Noto+Sans+Arabic:wght@400;500;600;700&family=Changa:wght@200..800&display=swap", + }, +]; + +function InnerLayout({ children }: { children: React.ReactNode }) { + const { locale, dir } = useLanguage(); + const { rootCssClass, resolvedScheme } = useColorScheme(); + return ( + + + + + {locale === "ar" ? "ميقات" : "miqaat"} + + + + + + +
+ +
+
{children}
+
+ +
+
+ + + + + ); +} + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +export default function App() { + return ; +} + +export const ErrorBoundary = ErrorBoundaryRoot; diff --git a/web/Miqaat/app/routes.ts b/web/Miqaat/app/routes.ts new file mode 100644 index 0000000..4501445 --- /dev/null +++ b/web/Miqaat/app/routes.ts @@ -0,0 +1,8 @@ +import { type RouteConfig, index, route } from "@react-router/dev/routes"; + +export default [ + index("routes/home.tsx"), + route("/location", "routes/location-selection.tsx"), + route("/settings", "routes/settings.tsx"), + route("/calendar", "routes/calendar.tsx"), +] satisfies RouteConfig; diff --git a/web/Miqaat/app/routes/calendar.module.css b/web/Miqaat/app/routes/calendar.module.css new file mode 100644 index 0000000..eb62d8a --- /dev/null +++ b/web/Miqaat/app/routes/calendar.module.css @@ -0,0 +1,228 @@ +.root { + max-width: 1400px; + margin: 0 auto; + padding: var(--space-6) var(--space-8); + display: flex; + flex-direction: column; + gap: var(--space-6); + width: 100%; +} + +.pageTitle { + font-family: var(--family-display); + font-size: var(--text-3xl); + font-weight: 600; + color: var(--color-primary); +} + +:global([lang="ar"]) .pageTitle { + font-family: var(--family-brand-ar); + font-size: 3rem; + font-weight: 700; +} + +/* ── Toolbar: Toggle + Print ─────────────────────────── */ +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + flex-wrap: wrap; +} + +.viewToggle { + display: flex; + background: var(--color-bg-muted); + border-radius: var(--radius-lg); + overflow: hidden; + border: 1px solid var(--color-border-light); + padding: 2px; +} + +.toggleBtn { + padding: var(--space-2) var(--space-6); + font-size: var(--text-sm); + font-weight: 600; + background: transparent; + color: var(--color-text-muted); + border: none; + cursor: pointer; + transition: all 200ms ease-in-out; + white-space: nowrap; + border-radius: var(--radius-md); +} + +.toggleBtn:hover { + color: var(--color-text); + background: rgba(0, 0, 0, 0.05); + transform: scale(1.02); +} + +.toggleBtnActive { + background: var(--color-primary); + color: var(--color-text-inverse); + box-shadow: var(--shadow-sm); +} + +.toggleBtnActive:hover { + background: var(--color-primary-dark); + color: var(--color-text-inverse); +} + +.printBtn { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-6); + font-size: var(--text-sm); + font-weight: 600; + border: none; + border-radius: var(--radius-lg); + background: var(--color-primary); + color: var(--color-text-inverse); + cursor: pointer; + transition: all 200ms ease-in-out; + white-space: nowrap; + box-shadow: var(--shadow-md); +} + +.printBtn:hover { + background: var(--color-primary-dark); + transform: translateY(-2px) scale(1.02); + box-shadow: var(--shadow-lg); +} + +/* ── Month Navigation ────────────────────────────────── */ +.monthNav { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-6); + background: var(--color-bg-card); + padding: var(--space-4); + border-radius: var(--radius-full); + width: fit-content; + margin: 0 auto; + box-shadow: var(--shadow-sm); +} + +.monthBtn { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border: none; + border-radius: var(--radius-full); + background: var(--color-bg-muted); + color: var(--color-text); + font-size: var(--text-xl); + cursor: pointer; + transition: all 200ms ease-in-out; +} + +.monthBtn:hover { + background: var(--color-highlight); + color: var(--color-primary); + transform: scale(1.15); +} + +.monthLabel { + font-family: var(--family-display); + font-size: var(--text-xl); + font-weight: 600; + color: var(--color-text); + min-width: 200px; + text-align: center; +} + +/* ── Responsive ──────────────────────────────────────── */ +@media (max-width: 480px) { + .root { + padding: var(--space-4) var(--space-2); + gap: var(--space-3); + } + .pageTitle { + font-size: var(--text-xl); + } + .toolbar { + flex-direction: column; + align-items: stretch; + } + .viewToggle { + width: 100%; + } + .toggleBtn { + flex: 1; + text-align: center; + } + .printBtn { + justify-content: center; + } +} + +/* ── Print Styles ────────────────────────────────────── */ +@media print { + .toolbar, + .monthBtn, + .printBtn { + display: none !important; + } + + .root { + padding: 0; + max-width: 100%; + } + + .pageTitle { + font-size: 1.5rem; + text-align: center; + margin-bottom: 0.5rem; + } + + .monthNav { + justify-content: center; + margin-bottom: 0.5rem; + } + + .monthLabel { + font-size: 1.25rem; + } +} + +.errorContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-4); + padding: var(--space-10) var(--space-4); + background: var(--color-bg-card); + border: 1.5px solid var(--color-border-light); + border-radius: var(--radius-lg); + margin-top: var(--space-4); +} + +.errorText { + color: #e74c3c; + font-size: var(--text-base); + font-weight: 500; + text-align: center; +} + +.retryBtn { + background: var(--color-primary); + color: var(--color-text-inverse); + border: none; + padding: var(--space-3) var(--space-6); + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 600; + cursor: pointer; + transition: all 200ms ease-in-out; +} + +.retryBtn:hover { + background: var(--color-primary-dark); + transform: scale(1.05); +} diff --git a/web/Miqaat/app/routes/calendar.tsx b/web/Miqaat/app/routes/calendar.tsx new file mode 100644 index 0000000..df855b4 --- /dev/null +++ b/web/Miqaat/app/routes/calendar.tsx @@ -0,0 +1,150 @@ +import { useState, useEffect, useCallback } from "react"; +import { Printer } from "lucide-react"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import { fetchMonthlyPrayerTimes, type DayPrayerTimes } from "~/services/api"; +import { CalendarGrid } from "../blocks/calendar/calendar-grid"; +import { LoadingSkeleton } from "~/components/loading-skeleton/loading-skeleton"; +import styles from "./calendar.module.css"; + +type ViewMode = "weekly" | "monthly"; + +function getWeekRange(date: Date): { start: number; end: number } { + const dayOfWeek = date.getDay(); // 0=Sun + const dateNum = date.getDate(); + const start = dateNum - dayOfWeek; + const end = start + 6; + return { start, end }; +} + +export default function Calendar() { + const { location, timeFormat } = useAppContext(); + const { t, locale } = useLanguage(); + const [monthData, setMonthData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [viewMode, setViewMode] = useState("monthly"); + const [currentDate, setCurrentDate] = useState(() => new Date()); + + useEffect(() => { + let cancelled = false; + setIsLoading(true); + setError(null); + fetchMonthlyPrayerTimes( + location.cityId, + currentDate.getFullYear(), + currentDate.getMonth(), + ).then((data) => { + if (!cancelled) { + setMonthData(data); + setIsLoading(false); + } + }).catch(() => { + if (!cancelled) { + setError(t("calendar.error") ?? "Failed to load calendar data."); + setIsLoading(false); + } + }); + return () => { cancelled = true; }; + }, [location, currentDate.getFullYear(), currentDate.getMonth(), retryCount, t]); + + const monthName = currentDate.toLocaleDateString( + locale === "ar" ? "ar-DZ" : "en-US", + { + month: "long", + year: "numeric", + } + ); + + const handlePrev = () => { + setCurrentDate((prev) => { + const d = new Date(prev); + if (viewMode === "weekly") { + d.setDate(d.getDate() - 7); + } else { + d.setMonth(d.getMonth() - 1); + } + return d; + }); + }; + + const handleNext = () => { + setCurrentDate((prev) => { + const d = new Date(prev); + if (viewMode === "weekly") { + d.setDate(d.getDate() + 7); + } else { + d.setMonth(d.getMonth() + 1); + } + return d; + }); + }; + + const handlePrint = useCallback(() => { + window.print(); + }, []); + + // Filter data based on view mode + const displayData = viewMode === "weekly" + ? (() => { + const { start, end } = getWeekRange(currentDate); + return monthData.filter((d) => d.day >= start && d.day <= end); + })() + : monthData; + + const title = viewMode === "weekly" ? t("calendar.weekTitle") : t("calendar.title"); + + return ( +
+

{title}

+ + {/* View mode toggle + Print */} +
+
+ + +
+ +
+ +
+ + {monthName} + +
+ {isLoading ? ( + + ) : error ? ( +
+

{error}

+ +
+ ) : ( + + )} +
+ ); +} diff --git a/web/Miqaat/app/routes/home.module.css b/web/Miqaat/app/routes/home.module.css new file mode 100644 index 0000000..7f7bbcd --- /dev/null +++ b/web/Miqaat/app/routes/home.module.css @@ -0,0 +1,65 @@ +.root { + max-width: 1400px; + margin: 0 auto; + padding: var(--space-6) var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-6); +} + +.topGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-6); + align-items: stretch; +} + +.errorContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-8); + background: var(--color-bg-card); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-light); + box-shadow: var(--shadow-sm); + margin-top: var(--space-4); + gap: var(--space-4); +} + +.errorText { + color: var(--color-warning); + font-size: var(--text-base); + text-align: center; +} + +.retryBtn { + background: var(--color-primary); + color: var(--color-bg); + border: none; + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-6); + font-family: var(--family-display); + font-size: var(--text-base); + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.retryBtn:hover { + background: var(--color-primary-light); + transform: translateY(-2px); +} + +@media (max-width: 768px) { + .root { + padding: var(--space-4) var(--space-3); + gap: var(--space-4); + } + + .topGrid { + grid-template-columns: 1fr; + gap: var(--space-4); + } +} \ No newline at end of file diff --git a/web/Miqaat/app/routes/home.tsx b/web/Miqaat/app/routes/home.tsx new file mode 100644 index 0000000..640d764 --- /dev/null +++ b/web/Miqaat/app/routes/home.tsx @@ -0,0 +1,44 @@ +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import { LoadingSkeleton } from "~/components/loading-skeleton/loading-skeleton"; +import { LocationDisplay } from "../blocks/home/location-display"; +import { CurrentTimeCard } from "../blocks/home/current-time-card"; +import { CurrentPrayerHighlight } from "../blocks/home/current-prayer-highlight"; +import { NextPrayerCountdown } from "../blocks/home/next-prayer-countdown"; +import { PrayerTimesGrid } from "../blocks/home/prayer-times-grid"; +import { AdditionalInfoSection } from "../blocks/home/additional-info-section"; +import styles from "./home.module.css"; + +export default function Home() { + const { isLoading, error, refreshPrayerTimes } = useAppContext(); + const { t } = useLanguage(); + + return ( +
+ + + {isLoading ? ( + + ) : error ? ( +
+

{error}

+ +
+ ) : ( + <> +
+ + +
+ + + + )} +
+ ); +} diff --git a/web/Miqaat/app/routes/location-selection.tsx b/web/Miqaat/app/routes/location-selection.tsx new file mode 100644 index 0000000..c95f787 --- /dev/null +++ b/web/Miqaat/app/routes/location-selection.tsx @@ -0,0 +1,89 @@ +import { useState, useCallback } from "react"; +import { useNavigate } from "react-router"; +import { SearchHeader } from "../blocks/location-selection/search-header"; +import { GPSLocationButton } from "../blocks/location-selection/gps-location-button"; +import { WilayaSelector } from "../blocks/location-selection/wilaya-baladiya-selector"; +import { Toast } from "~/components/toast/toast"; +import { useAppContext } from "~/context/app-context"; +import { useLocationSelection } from "~/hooks/use-location-selection"; +import { useLanguage } from "~/i18n/language-context"; + +export default function LocationSelection() { + const navigate = useNavigate(); + const { t } = useLanguage(); + const { setLocation } = useAppContext(); + const locationState = useLocationSelection(); + const [gpsSuccess, setGpsSuccess] = useState(false); + + const handleGPSDetect = useCallback(() => { + locationState.detectGPSLocation(); + }, [locationState]); + + // Show success state when GPS finds a location + const isGPSSuccess = + gpsSuccess || + (!locationState.isDetectingGPS && + !locationState.gpsError && + locationState.selectedWilaya !== null); + + const handleConfirm = () => { + const loc = locationState.getSelectedLocation(); + if (loc) { + setLocation(loc); + setGpsSuccess(true); + navigate("/"); + } + }; + + const canConfirm = locationState.selectedWilaya !== null; + + return ( +
+ {/* Decorative backgrounds */} +
+
+ +
+ + +
+ + +
+
+ + {t("location.or") || "أو"} + +
+
+ + +
+ + {locationState.gpsError && ( + + )} +
+
+ ); +} diff --git a/web/Miqaat/app/routes/settings.module.css b/web/Miqaat/app/routes/settings.module.css new file mode 100644 index 0000000..c1163b7 --- /dev/null +++ b/web/Miqaat/app/routes/settings.module.css @@ -0,0 +1,81 @@ +.root { + max-width: 1400px; + margin: 0 auto; + padding: var(--space-6) var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-6); +} + +.pageTitle { + font-family: var(--family-display); + font-size: var(--text-3xl); + font-weight: 600; + color: var(--color-primary); + text-align: center; +} + +:global([lang="ar"]) .pageTitle { + font-family: var(--family-brand-ar); + font-size: 3rem; + font-weight: 700; +} + +.grid { + display: flex; + flex-direction: column; + gap: var(--space-6); +} + +.topSection { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "time theme" + "lang theme"; + gap: var(--space-6); +} + +.timeFormatArea { + grid-area: time; +} + +.languageArea { + grid-area: lang; +} + +.themeArea { + grid-area: theme; +} + +.bottomSection { + width: 100%; +} + +.actionsBox { + margin-top: var(--space-4); + background: var(--color-bg-card); + padding: var(--space-6); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border-light); +} + +@media (max-width: 768px) { + .topSection { + grid-template-columns: 1fr; + grid-template-areas: + "time" + "theme" + "lang"; + } +} + +@media (max-width: 480px) { + .root { + padding: var(--space-4) var(--space-3); + } + .actionsBox { + padding: var(--space-4); + } +} \ No newline at end of file diff --git a/web/Miqaat/app/routes/settings.tsx b/web/Miqaat/app/routes/settings.tsx new file mode 100644 index 0000000..0f6cdf8 --- /dev/null +++ b/web/Miqaat/app/routes/settings.tsx @@ -0,0 +1,121 @@ +import { useState } from "react"; +import { useAppContext } from "~/context/app-context"; +import { useLanguage } from "~/i18n/language-context"; +import { useSettingsDraft } from "~/hooks/use-settings-draft"; +import { Toast } from "~/components/toast/toast"; + +import { TimeFormatToggle } from "../blocks/settings/time-format-toggle"; +import { ThemeToggle } from "../blocks/settings/theme-toggle"; +import { NotificationToggle } from "../blocks/settings/notification-toggle"; +import { LanguageToggle } from "../blocks/settings/language-toggle"; +import { SettingsActions } from "../blocks/settings/settings-actions"; +import styles from "./settings.module.css"; + +const DEFAULT_LOCATION = { + city: "Algiers", + cityAr: "الجزائر", + country: "Algeria", + countryAr: "الجزائر", + timezone: "Africa/Algiers", + latitude: 36.75, + longitude: 3.06, + cityId: 27, +}; + +export default function Settings() { + const { setLocation } = useAppContext(); + const { t } = useLanguage(); + + const { + draftFormat, + setDraftFormat, + draftNotifications, + setDraftNotifications, + draftPrefs, + setNotificationPreference, + draftLocale, + setDraftLocale, + draftTheme, + setDraftTheme, + isDirty, + save, + resetToDefaults, + } = useSettingsDraft(); + + const [isSaving, setIsSaving] = useState(false); + const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); + + const handleSave = async () => { + setIsSaving(true); + const result = await save(); + setIsSaving(false); + + if (result.success) { + setToast({ message: t("settings.saved") || "Settings saved successfully", type: "success" }); + } else { + setToast({ message: result.error || "Failed to save settings", type: "error" }); + } + }; + + const handleReset = () => { + resetToDefaults(); + setLocation(DEFAULT_LOCATION); + setToast({ message: t("settings.reset.success") || "Settings reset to defaults", type: "success" }); + }; + + return ( +
+

{t("settings.title")}

+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ + {toast && ( + setToast(null)} + /> + )} +
+ ); +} + diff --git a/web/Miqaat/app/services/api.ts b/web/Miqaat/app/services/api.ts new file mode 100644 index 0000000..2e150bb --- /dev/null +++ b/web/Miqaat/app/services/api.ts @@ -0,0 +1,225 @@ +import type { PrayerTime, Wilaya } from "~/data/prayer-data"; +import { + fetchCities, + fetchDailyPrayerTimes, + fetchPrayerTimesRange, + formatDate, + type ApiPrayerTimesEntry, +} from "./prayerApi"; + +/* ------------------------------------------------------------------ */ +/* Static Wilaya Metadata */ +/* The API only returns Arabic city names and IDs. */ +/* This table maps API city IDs → English names + coordinates */ +/* (needed for GPS nearest-city matching and bilingual display). */ +/* ------------------------------------------------------------------ */ + +interface WilayaMeta { + cityId: number; + name: string; + nameAr: string; + latitude: number; + longitude: number; +} + +const WILAYA_META: Record = { + 1: { cityId: 1, name: "Djelfa", nameAr: "الجلفة", latitude: 34.67, longitude: 3.25 }, + 2: { cityId: 2, name: "Tébessa", nameAr: "تبسة", latitude: 35.4, longitude: 8.12 }, + 3: { cityId: 3, name: "Bir El Ater", nameAr: "بئر العاتر", latitude: 34.75, longitude: 8.06 }, + 4: { cityId: 4, name: "Khenchela", nameAr: "خنشلة", latitude: 35.44, longitude: 7.14 }, + 5: { cityId: 5, name: "El Oued", nameAr: "الوادي", latitude: 33.37, longitude: 6.85 }, + 6: { cityId: 6, name: "Batna", nameAr: "باتنة", latitude: 35.56, longitude: 6.17 }, + 7: { cityId: 7, name: "Touggourt", nameAr: "تقرت", latitude: 33.1, longitude: 6.06 }, + 8: { cityId: 8, name: "Biskra", nameAr: "بسكرة", latitude: 34.85, longitude: 5.73 }, + 10: { cityId: 10, name: "Bou Saâda", nameAr: "بوسعادة", latitude: 35.21, longitude: 4.17 }, + 11: { cityId: 11, name: "Ain El Melh", nameAr: "عين الملح", latitude: 34.84, longitude: 2.89 }, + 12: { cityId: 12, name: "Hassi R'Mel", nameAr: "حاسي الرمل", latitude: 32.93, longitude: 3.27 }, + 13: { cityId: 13, name: "Laghouat", nameAr: "الأغواط", latitude: 33.8, longitude: 2.88 }, + 14: { cityId: 14, name: "Ain Oussera", nameAr: "عين وسارة", latitude: 35.45, longitude: 2.9 }, + 15: { cityId: 15, name: "Tissemsilt", nameAr: "تيسمسيلت", latitude: 35.61, longitude: 1.81 }, + 16: { cityId: 16, name: "Tiaret", nameAr: "تيارت", latitude: 35.37, longitude: 1.32 }, + 17: { cityId: 17, name: "El Bayadh", nameAr: "البيض", latitude: 33.69, longitude: 1.02 }, + 18: { cityId: 18, name: "Saïda", nameAr: "سعيدة", latitude: 34.83, longitude: 0.15 }, + 19: { cityId: 19, name: "Mascara", nameAr: "معسكر", latitude: 35.4, longitude: 0.14 }, + 20: { cityId: 20, name: "Naâma", nameAr: "النعامة", latitude: 33.27, longitude: -0.31 }, + 21: { cityId: 21, name: "Sidi Bel Abbès", nameAr: "سيدي بلعباس", latitude: 35.19, longitude: -0.63 }, + 22: { cityId: 22, name: "Ibn Badis", nameAr: "ابن باديس", latitude: 35.22, longitude: -0.71 }, + 23: { cityId: 23, name: "Tlemcen", nameAr: "تلمسان", latitude: 34.88, longitude: -1.32 }, + 24: { cityId: 24, name: "Aïn Témouchent", nameAr: "عين تموشنت", latitude: 35.3, longitude: -1.14 }, + 25: { cityId: 25, name: "Sebdou", nameAr: "سبدو", latitude: 34.64, longitude: -1.33 }, + 26: { cityId: 26, name: "Maghnia", nameAr: "مغنية", latitude: 34.86, longitude: -1.73 }, + 27: { cityId: 27, name: "Algiers", nameAr: "الجزائر", latitude: 36.75, longitude: 3.06 }, + 28: { cityId: 28, name: "Oran", nameAr: "وهران", latitude: 35.7, longitude: -0.63 }, + 29: { cityId: 29, name: "Mostaganem", nameAr: "مستغانم", latitude: 35.93, longitude: 0.09 }, + 30: { cityId: 30, name: "Relizane", nameAr: "غليزان", latitude: 35.74, longitude: 0.56 }, + 31: { cityId: 31, name: "Chlef", nameAr: "الشلف", latitude: 36.16, longitude: 1.33 }, + 32: { cityId: 32, name: "Aïn Defla", nameAr: "عين الدفلى", latitude: 36.26, longitude: 1.97 }, + 33: { cityId: 33, name: "Tipaza", nameAr: "تيبازة", latitude: 36.59, longitude: 2.45 }, + 34: { cityId: 34, name: "Médéa", nameAr: "المدية", latitude: 36.26, longitude: 2.75 }, + 35: { cityId: 35, name: "Blida", nameAr: "البليدة", latitude: 36.47, longitude: 2.83 }, + 36: { cityId: 36, name: "Boumerdès", nameAr: "بومرداس", latitude: 36.76, longitude: 3.47 }, + 37: { cityId: 37, name: "Bouira", nameAr: "البويرة", latitude: 36.38, longitude: 3.9 }, + 38: { cityId: 38, name: "Dellys", nameAr: "دلس", latitude: 36.91, longitude: 3.91 }, + 39: { cityId: 39, name: "Tizi Ouzou", nameAr: "تيزي وزو", latitude: 36.71, longitude: 4.05 }, + 40: { cityId: 40, name: "M'Sila", nameAr: "المسيلة", latitude: 35.7, longitude: 4.54 }, + 41: { cityId: 41, name: "Bordj Bou Arréridj", nameAr: "برج بوعريريج", latitude: 36.07, longitude: 4.76 }, + 42: { cityId: 42, name: "Béjaïa", nameAr: "بجاية", latitude: 36.75, longitude: 5.08 }, + 43: { cityId: 43, name: "Sétif", nameAr: "سطيف", latitude: 36.19, longitude: 5.41 }, + 44: { cityId: 44, name: "Jijel", nameAr: "جيجل", latitude: 36.82, longitude: 5.77 }, + 45: { cityId: 45, name: "Mila", nameAr: "ميلة", latitude: 36.45, longitude: 6.26 }, + 46: { cityId: 46, name: "Constantine", nameAr: "قسنطينة", latitude: 36.37, longitude: 6.61 }, + 47: { cityId: 47, name: "Skikda", nameAr: "سكيكدة", latitude: 36.88, longitude: 6.91 }, + 48: { cityId: 48, name: "Oum El Bouaghi", nameAr: "أم البواقي", latitude: 35.88, longitude: 7.11 }, + 49: { cityId: 49, name: "Guelma", nameAr: "قالمة", latitude: 36.46, longitude: 7.43 }, + 50: { cityId: 50, name: "Annaba", nameAr: "عنابة", latitude: 36.9, longitude: 7.77 }, + 51: { cityId: 51, name: "Souk Ahras", nameAr: "سوق أهراس", latitude: 36.29, longitude: 7.95 }, + 52: { cityId: 52, name: "El Tarf", nameAr: "الطارف", latitude: 36.77, longitude: 8.31 }, + 53: { cityId: 53, name: "Adrar", nameAr: "أدرار", latitude: 27.87, longitude: -0.29 }, + 54: { cityId: 54, name: "Tindouf", nameAr: "تندوف", latitude: 27.67, longitude: -8.15 }, + 55: { cityId: 55, name: "Béchar", nameAr: "بشار", latitude: 31.62, longitude: -2.22 }, + 56: { cityId: 56, name: "Timimoun", nameAr: "تميمون", latitude: 29.26, longitude: 0.23 }, + 57: { cityId: 57, name: "Reggane", nameAr: "رقان", latitude: 26.72, longitude: 0.17 }, + 58: { cityId: 58, name: "Bordj Badji Mokhtar", nameAr: "برج باجي مختار", latitude: 21.33, longitude: 0.95 }, + 59: { cityId: 59, name: "Beni Abbès", nameAr: "بني عباس", latitude: 30.13, longitude: -2.17 }, + 60: { cityId: 60, name: "Ain Salah", nameAr: "عين صالح", latitude: 27.2, longitude: 2.47 }, + 61: { cityId: 61, name: "El Meniaa", nameAr: "المنيعة", latitude: 30.58, longitude: 2.88 }, + 62: { cityId: 62, name: "Ghardaïa", nameAr: "غرداية", latitude: 32.49, longitude: 3.67 }, + 63: { cityId: 63, name: "Ouargla", nameAr: "ورقلة", latitude: 31.95, longitude: 5.33 }, + 64: { cityId: 64, name: "Tamanrasset", nameAr: "تمنراست", latitude: 22.79, longitude: 5.53 }, + 65: { cityId: 65, name: "Ain Guezzam", nameAr: "عين قزام", latitude: 19.57, longitude: 5.77 }, + 66: { cityId: 66, name: "Illizi", nameAr: "إليزي", latitude: 26.51, longitude: 8.48 }, + 67: { cityId: 67, name: "Djanet", nameAr: "جانت", latitude: 24.55, longitude: 9.48 }, + 68: { cityId: 68, name: "Ain Amenas", nameAr: "عين أمناس", latitude: 28.05, longitude: 9.63 }, + 69: { cityId: 69, name: "Beni Ounif", nameAr: "بني ونيف", latitude: 32.05, longitude: -1.25 }, +}; + +/* ------------------------------------------------------------------ */ +/* API → App Type Mappers */ +/* ------------------------------------------------------------------ */ + +/** + * Normalize time strings from API (e.g. "4:32") to "HH:MM" format. + */ +function normalizeTime(t: string): string { + const [h, m] = t.split(":").map(Number); + return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; +} + +/** + * Calculate Duha time as 15 minutes after Sunrise. + */ +function calculateDuha(sunriseTime: string): string { + const [h, m] = sunriseTime.split(":").map(Number); + const totalMinutes = h * 60 + m + 15; + const newH = Math.floor(totalMinutes / 60); + const newM = totalMinutes % 60; + return `${String(newH).padStart(2, "0")}:${String(newM).padStart(2, "0")}`; +} + +/** + * Map an API prayer times entry to the app's PrayerTime[] format. + * Shurooq → Sunrise, Duha → Sunrise + 15 mins. + */ +function mapApiEntryToPrayerTimes(entry: ApiPrayerTimesEntry): PrayerTime[] { + const sunrise = normalizeTime(entry.Shurooq); + const duha = calculateDuha(entry.Shurooq); + + return [ + { name: "Fajr", label: "Fajr", time: normalizeTime(entry.Fajr), icon: "moon", isPrayer: true }, + { name: "Sunrise", label: "Sunrise", time: sunrise, icon: "sunrise", isPrayer: false }, + { name: "Duha", label: "Duha", time: duha, icon: "sun-dim", isPrayer: true }, + { name: "Dhuhr", label: "Dhuhr", time: normalizeTime(entry.Dhuhr), icon: "sun", isPrayer: true }, + { name: "Asr", label: "Asr", time: normalizeTime(entry.Asr), icon: "cloud-sun", isPrayer: true }, + { name: "Maghrib", label: "Maghrib", time: normalizeTime(entry.Maghrib), icon: "sunset", isPrayer: true }, + { name: "Isha", label: "Isha", time: normalizeTime(entry.Isha), icon: "star", isPrayer: true }, + ]; +} + +/* ------------------------------------------------------------------ */ +/* Public API Functions */ +/* ------------------------------------------------------------------ */ + +/** Fetch all wilayas (cities) from the API, enriched with metadata. */ +export async function fetchWilayas(): Promise { + const cities = await fetchCities(); + return cities.map((city, index) => { + const meta = WILAYA_META[city._id]; + return { + id: index + 1, + cityId: city._id, + name: meta?.name ?? city.MADINA_NAME, + nameAr: meta?.nameAr ?? city.MADINA_NAME, + latitude: meta?.latitude ?? 36.75, + longitude: meta?.longitude ?? 3.06, + }; + }); +} + +/** Fetch today's prayer times for a given city. */ +export async function fetchPrayerTimes(cityId: number): Promise { + const entries = await fetchDailyPrayerTimes(cityId); + if (!entries || entries.length === 0) { + throw new Error("No prayer times data returned from API."); + } + return mapApiEntryToPrayerTimes(entries[0]); +} + +/** Fetch prayer times for an entire month. */ +export interface DayPrayerTimes { + date: string; // "YYYY-MM-DD" + day: number; + times: PrayerTime[]; +} + +export async function fetchMonthlyPrayerTimes( + cityId: number, + year: number, + month: number, // 0-indexed +): Promise { + const startDate = `${year}-${String(month + 1).padStart(2, "0")}-01`; + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const endDate = `${year}-${String(month + 1).padStart(2, "0")}-${String(daysInMonth).padStart(2, "0")}`; + + const entries = await fetchPrayerTimesRange(cityId, startDate, endDate); + + return entries.map((entry) => { + const d = new Date(entry.GeoDate); + return { + date: entry.GeoDate, + day: d.getDate(), + times: mapApiEntryToPrayerTimes(entry), + }; + }); +} + +/** Find the nearest wilaya for GPS coordinates using static metadata. */ +export async function findNearestLocation( + lat: number, + lng: number, +): Promise<{ wilaya: Wilaya } | null> { + // Use the static metadata table for distance calculations + const entries = Object.values(WILAYA_META); + let nearest: WilayaMeta | null = null; + let minDist = Infinity; + + for (const meta of entries) { + const dist = Math.hypot(meta.latitude - lat, meta.longitude - lng); + if (dist < minDist) { + minDist = dist; + nearest = meta; + } + } + + if (!nearest) return null; + + return { + wilaya: { + id: nearest.cityId, + cityId: nearest.cityId, + name: nearest.name, + nameAr: nearest.nameAr, + latitude: nearest.latitude, + longitude: nearest.longitude, + }, + }; +} diff --git a/web/Miqaat/app/services/prayerApi.ts b/web/Miqaat/app/services/prayerApi.ts new file mode 100644 index 0000000..a41dc36 --- /dev/null +++ b/web/Miqaat/app/services/prayerApi.ts @@ -0,0 +1,118 @@ +/** + * Adhan DZ API Client + * Raw fetch layer for https://adhan-dz.dexter21767.com/ + * + * This module handles all HTTP communication with the API. + * Response mapping to app types is done in api.ts. + */ + +const API_BASE = "https://adhan-dz.dexter21767.com"; + +/* ------------------------------------------------------------------ */ +/* API Response Types */ +/* ------------------------------------------------------------------ */ + +export interface ApiCity { + _id: number; + ParentId: number | null; + MADINA_NAME: string; +} + +export interface ApiPrayerTimesEntry { + _id: number; + MADINA_ID: number; + GeoDate: string; // "YYYY-MM-DD" + Fajr: string; // "H:MM" or "HH:MM" + Shurooq: string; // Sunrise + Kibla: string; // Duha / sun zenith + Dhuhr: string; + Asr: string; + Maghrib: string; + Isha: string; +} + +/* ------------------------------------------------------------------ */ +/* Error Handling */ +/* ------------------------------------------------------------------ */ + +export class ApiError extends Error { + constructor( + message: string, + public status?: number, + public endpoint?: string, + ) { + super(message); + this.name = "ApiError"; + } +} + +async function handleResponse(res: Response, endpoint: string): Promise { + if (!res.ok) { + let message = `API request failed: ${res.status} ${res.statusText}`; + try { + const body = await res.json(); + if (body?.error) message = body.error; + } catch { + // ignore parse errors + } + throw new ApiError(message, res.status, endpoint); + } + return res.json() as Promise; +} + +/* ------------------------------------------------------------------ */ +/* API Functions */ +/* ------------------------------------------------------------------ */ + +/** + * Fetch all cities from the API. + * Returns the raw city list with Arabic names and parent relationships. + */ +export async function fetchCities(): Promise { + const endpoint = "/cities"; + const res = await fetch(`${API_BASE}${endpoint}`); + return handleResponse(res, endpoint); +} + +/** + * Fetch prayer times for a specific city and single date. + * @param cityId - The city ID from the /cities endpoint + * @param date - Date in YYYY-MM-DD format. Defaults to today. + */ +export async function fetchDailyPrayerTimes( + cityId: number, + date?: string, +): Promise { + const d = date ?? formatDate(new Date()); + const endpoint = `/prayerTimes?cityId=${cityId}&date=${d}`; + const res = await fetch(`${API_BASE}${endpoint}`); + return handleResponse(res, endpoint); +} + +/** + * Fetch prayer times for a city over a date range (max 30 days). + * @param cityId - The city ID + * @param startDate - Start date in YYYY-MM-DD format + * @param endDate - End date in YYYY-MM-DD format + */ +export async function fetchPrayerTimesRange( + cityId: number, + startDate: string, + endDate: string, +): Promise { + const endpoint = `/prayerTimes?cityId=${cityId}&startDate=${startDate}&endDate=${endDate}`; + const res = await fetch(`${API_BASE}${endpoint}`); + return handleResponse(res, endpoint); +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/** Format a Date as YYYY-MM-DD */ +export function formatDate(d: Date): string { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} diff --git a/web/Miqaat/app/styles/global.css b/web/Miqaat/app/styles/global.css new file mode 100644 index 0000000..bca998a --- /dev/null +++ b/web/Miqaat/app/styles/global.css @@ -0,0 +1,124 @@ +@import "tailwindcss/theme" layer(theme); +@import "tailwindcss/utilities" layer(utilities); + +/* + * ── Tailwind ↔ Design Token Bridge ────────────────────── + * Preflight is intentionally excluded — the project already + * has its own reset.css. + * + * Spacing uses @theme (not inline) so --spacing-* variables + * are emitted and utilities like gap-5, px-6 work at runtime. + * + * Colors use @theme inline because theme.css already defines + * the --color-* variables — no need to emit duplicates. + */ + +/* Spacing must be emitted since no other file defines --spacing-* */ +@theme { + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + --spacing-3: 0.75rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-8: 2rem; + --spacing-10: 2.5rem; + --spacing-12: 3rem; + --spacing-16: 4rem; + + /* ── Border Radius ── */ + --radius-sm: 0.5rem; + --radius-md: 0.75rem; + --radius-lg: 1rem; + --radius-xl: 1.5rem; + --radius-full: 9999px; + + /* ── Font Families ── */ + --font-display: "Cormorant Garamond", "Noto Sans Arabic", Georgia, serif; + --font-body: "DM Sans", "Noto Sans Arabic", system-ui, sans-serif; + --font-mono: "Roboto Mono", "Courier New", monospace; + --font-brand-ar: "Changa", system-ui, sans-serif; + + /* ── Font Sizes ── */ + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-md: 1.125rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --text-2xl: 2rem; + --text-3xl: 2.75rem; + --text-4xl: 3.5rem; +} + +/* Colors reference existing theme.css vars — use inline to avoid duplication */ +@theme inline { + --color-primary: var(--color-primary); + --color-primary-light: var(--color-primary-light); + --color-primary-dark: var(--color-primary-dark); + --color-accent: var(--color-accent); + --color-accent-light: var(--color-accent-light); + --color-accent-muted: var(--color-accent-muted); + --color-bg: var(--color-bg); + --color-bg-card: var(--color-bg-card); + --color-bg-card-alt: var(--color-bg-card-alt); + --color-bg-muted: var(--color-bg-muted); + --color-text: var(--color-text); + --color-text-muted: var(--color-text-muted); + --color-text-inverse: var(--color-text-inverse); + --color-text-accent: var(--color-text-accent); + --color-border: var(--color-border); + --color-border-light: var(--color-border-light); + --color-success: var(--color-success); + --color-warning: var(--color-warning); +} + +/* ── Dark mode: class strategy matching @dazl/color-scheme ── */ +@custom-variant dark (&:where(.dark-theme, .dark-theme *)); + +/* ── RTL variant ── */ +@custom-variant rtl (&:where([dir="rtl"], [dir="rtl"] *)); + +html, body { + overflow-x: hidden; + position: relative; +} + +/* Hide an element visually without hiding it from screen readers. */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* ── Print: hide nav + footer globally ───────────────── */ +/* ── Global Micro-interactions ───────────────────────── */ +button { + transition: all 200ms ease-in-out; + cursor: pointer; +} + +button:hover { + transform: scale(1.02); +} + +button:active { + transform: scale(0.98); +} + +@media print { + header, + footer { + display: none !important; + } + + body { + background: #fff !important; + } +} diff --git a/web/Miqaat/app/styles/reset.css b/web/Miqaat/app/styles/reset.css new file mode 100644 index 0000000..a9e50fb --- /dev/null +++ b/web/Miqaat/app/styles/reset.css @@ -0,0 +1,72 @@ +/* Josh Comeau CSS Reset + * Source: https://www.joshwcomeau.com/css/custom-css-reset/ + * Description: A modern CSS reset to improve consistency and control over default styles. + */ + +/* 1. Use a more-intuitive box-sizing model */ +*, +*::before, +*::after { + box-sizing: border-box; +} +/* 2. Remove default margin */ +* { + margin: 0; +} +body { + /* 3. Add accessible line-height */ + line-height: 1.5; + /* 4. Improve text rendering */ + -webkit-font-smoothing: antialiased; +} +/* 5. Improve media defaults */ +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} +/* 6. Inherit fonts for form controls */ +input, +button, +textarea, +select { + font: inherit; +} +/* 7. Avoid text overflows */ +p, +h1, +h2, +h3, +h4, +h5, +h6 { + overflow-wrap: break-word; +} +/* 8. Improve line wrapping */ +p { + text-wrap: pretty; +} +h1, +h2, +h3, +h4, +h5, +h6 { + text-wrap: balance; +} + +/* Custom additions to the original Josh Comeau reset by Dazl */ + +a { + text-decoration: none; + color: inherit; +} + +button { + border: none; + cursor: pointer; + background: none; +} diff --git a/web/Miqaat/app/styles/theme.css b/web/Miqaat/app/styles/theme.css new file mode 100644 index 0000000..80a79f4 --- /dev/null +++ b/web/Miqaat/app/styles/theme.css @@ -0,0 +1,104 @@ +/* Prayer Times App – Design Tokens */ + +/* Google Fonts are loaded via root.tsx links */ + +:root { + --family-display: "Cormorant Garamond", "Noto Sans Arabic", Georgia, serif; + --family-brand-ar: "Changa", system-ui, sans-serif; + --family-body: "DM Sans", "Noto Sans Arabic", system-ui, sans-serif; + --family-mono: "Roboto Mono", "Courier New", monospace; + + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-md: 1.125rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --text-2xl: 2rem; + --text-3xl: 2.75rem; + --text-4xl: 3.5rem; + + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + --space-16: 4rem; + + --radius-sm: 0.5rem; + --radius-md: 0.75rem; + --radius-lg: 1rem; + --radius-xl: 1.5rem; + --radius-full: 9999px; +} + +/* ── Light Theme ───────────────────────────────────────────────── */ +:root, +:root.light-theme { + --color-primary: #1a5276; + --color-primary-light: #2980b9; + --color-primary-dark: #154360; + --color-accent: #c9a84c; + --color-accent-light: #f4d03f; + --color-accent-muted: #f9e4a0; + + --color-bg: #f0f4f8; + --color-bg-card: #ffffff; + --color-bg-card-alt: #eaf2f8; + --color-bg-muted: #dde8f0; + + --color-text: #1a2b3c; + --color-text-muted: #455a64; + --color-text-inverse: #ffffff; + --color-text-accent: #c9a84c; + + --color-border: #cad8e4; + --color-border-light: #e0eaf0; + + --color-success: #27ae60; + --color-warning: #e67e22; + --color-highlight: rgba(26, 82, 118, 0.08); + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.06); + --shadow-glow: 0 0 20px rgba(26, 82, 118, 0.25); + --shadow-accent-glow: 0 0 16px rgba(201, 168, 76, 0.4); +} + +/* ── Dark Theme ────────────────────────────────────────────────── */ +:root.dark-theme { + --color-primary: #2e86c1; + --color-primary-light: #5dade2; + --color-primary-dark: #1a5276; + --color-accent: #f4d03f; + --color-accent-light: #f9e4a0; + --color-accent-muted: rgba(244, 208, 63, 0.15); + + --color-bg: #0d1b2a; + --color-bg-card: #1a2b3c; + --color-bg-card-alt: #152232; + --color-bg-muted: #0f2030; + + --color-text: #e8f1f8; + --color-text-muted: #a0c4d9; + --color-text-inverse: #0d1b2a; + --color-text-accent: #f4d03f; + + --color-border: #2a4055; + --color-border-light: #1e3248; + + --color-success: #2ecc71; + --color-warning: #f39c12; + --color-highlight: rgba(46, 134, 193, 0.12); + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.2); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.4), 0 4px 8px rgba(0, 0, 0, 0.25); + --shadow-glow: 0 0 20px rgba(46, 134, 193, 0.3); + --shadow-accent-glow: 0 0 16px rgba(244, 208, 63, 0.35); +} diff --git a/web/Miqaat/app/utils/time-utils.ts b/web/Miqaat/app/utils/time-utils.ts new file mode 100644 index 0000000..7f5d21a --- /dev/null +++ b/web/Miqaat/app/utils/time-utils.ts @@ -0,0 +1,42 @@ + +export function formatTime( + timeStr: string, + format: "12h" | "24h", + locale: string = "en" +): string { + const parts = timeStr.split(":"); + const h = Number(parts[0]); + const m = Number(parts[1]); + const s = parts[2] ? Number(parts[2]) : undefined; + + if (format === "24h") { + let res = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; + if (s !== undefined) res += `:${String(s).padStart(2, "0")}`; + return res; + } + + const h12 = h % 12 || 12; + const mm = String(m).padStart(2, "0"); + const ss = s !== undefined ? `:${String(s).padStart(2, "0")}` : ""; + + let period = h >= 12 ? "PM" : "AM"; + if (locale === "ar") { + period = h >= 12 ? "مساءً" : "صباحاً"; + } + + return `${h12}:${mm}${ss} ${period}`; +} + +export function formatMinutesToTime( + totalMinutes: number, + format: "12h" | "24h", + locale: string = "en" +): string { + const roundedMinutes = Math.round(totalMinutes); + const normalized = ((roundedMinutes % 1440) + 1440) % 1440; + const h = Math.floor(normalized / 60); + const m = normalized % 60; + + const timeStr = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; + return formatTime(timeStr, format, locale); +} diff --git a/web/Miqaat/package-lock.json b/web/Miqaat/package-lock.json new file mode 100644 index 0000000..fe2b424 --- /dev/null +++ b/web/Miqaat/package-lock.json @@ -0,0 +1,4174 @@ +{ + "name": "project", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "project", + "dependencies": { + "@dazl/color-scheme": "^1.2.1", + "@react-router/node": "^7.14.1", + "@react-router/serve": "^7.14.1", + "classnames": "^2.5.1", + "isbot": "^5.1.38", + "lucide-react": "^1.8.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-router": "^7.14.1" + }, + "devDependencies": { + "@dazl-lib/radix-ui": "^0.1.0", + "@dazl/component-gallery": "^2.0.1", + "@dazl/dev": "^1.4.0", + "@react-router/dev": "^7.14.1", + "@tailwindcss/vite": "^4.2.4", + "@types/node": "24", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "prettier": "^3.8.2", + "tailwindcss": "^4.2.4", + "typescript": "~6.0.2", + "vite": "^7.3.2", + "vite-tsconfig-paths": "^6.1.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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/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.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "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-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dazl-lib/radix-ui": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@dazl-lib/radix-ui/-/radix-ui-0.1.0.tgz", + "integrity": "sha512-mKJ3yGJx1vgI+pEgSWArO0Z45MqQdYzOan9wvBUmg9Xz5yhYNGjTlkajN4KOMJnhas8B5h7NGZ2WyPfJdX4SIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@dazl/color-scheme": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@dazl/color-scheme/-/color-scheme-1.2.1.tgz", + "integrity": "sha512-56wbP+lcDUIEnhxIS7cWPXdePXjuzsa6NIUKrsp2cLmu87dPP3VYbrlU3FGg8iT0ezwArrB+gJalwyD8U3qN5Q==", + "license": "MIT" + }, + "node_modules/@dazl/component-gallery": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@dazl/component-gallery/-/component-gallery-2.0.1.tgz", + "integrity": "sha512-dLbjfbFCzYXrgtO7GLga1AGeZLZPaAHhWG9FicoTBEkPKIBP8GKz/Tm+1kknmFVJJudA175vJfxihgbYFv0QCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^4.3.6" + }, + "peerDependencies": { + "react": ">=19.0.0" + } + }, + "node_modules/@dazl/dev": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@dazl/dev/-/dev-1.4.0.tgz", + "integrity": "sha512-lDGt7CFT63MFgEuSFczlSDPvxBO8hXEl1Q9F3fGcz+tlIjCQYIefOQSAJ06kD3u/oNYUpvkL6FJa5pXAUNWd7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mjackson/node-fetch-server": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==", + "license": "MIT" + }, + "node_modules/@react-router/dev": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.14.1.tgz", + "integrity": "sha512-ZBEwods1TxqPVY2SrXDuDCfoaE5VoTMBYrfa/+3MesprY3foSo1jhin9mh4FwmXPXhhmDYKXi2z5UR+oMj8Qjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.7", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "@react-router/node": "7.14.1", + "@remix-run/node-fetch-server": "^0.13.0", + "arg": "^5.0.1", + "babel-dead-code-elimination": "^1.0.6", + "chokidar": "^4.0.0", + "dedent": "^1.5.3", + "es-module-lexer": "^1.3.1", + "exit-hook": "2.2.1", + "isbot": "^5.1.11", + "jsesc": "3.0.2", + "lodash": "^4.17.21", + "p-map": "^7.0.3", + "pathe": "^1.1.2", + "picocolors": "^1.1.1", + "pkg-types": "^2.3.0", + "prettier": "^3.6.2", + "react-refresh": "^0.14.0", + "semver": "^7.3.7", + "tinyglobby": "^0.2.14", + "valibot": "^1.2.0", + "vite-node": "^3.2.2" + }, + "bin": { + "react-router": "bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-router/serve": "^7.14.1", + "@vitejs/plugin-rsc": "~0.5.21", + "react-router": "^7.14.1", + "react-server-dom-webpack": "^19.2.3", + "typescript": "^5.1.0 || ^6.0.0", + "vite": "^5.1.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "wrangler": "^3.28.2 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@react-router/serve": { + "optional": true + }, + "@vitejs/plugin-rsc": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wrangler": { + "optional": true + } + } + }, + "node_modules/@react-router/express": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.14.1.tgz", + "integrity": "sha512-XX/R+/JIIbwTfaXHz1WAJbiPfkd56y7PN9Czg7h6Tvos9TZlmMXmRhxWKRdzfsa8Lp8sq42JjKOBCEEPyH4V1Q==", + "license": "MIT", + "dependencies": { + "@react-router/node": "7.14.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "express": "^4.17.1 || ^5", + "react-router": "7.14.1", + "typescript": "^5.1.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/node": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.14.1.tgz", + "integrity": "sha512-SthTjCwW7otzEAcZwF0RAPMRrDT47B4qHDxZM45rM5K1Gp86ANK/xlXF+DgpLq9qKZf9FbKzxS9hT7FqDeBAOg==", + "license": "MIT", + "dependencies": { + "@mjackson/node-fetch-server": "^0.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.14.1", + "typescript": "^5.1.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/serve": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.14.1.tgz", + "integrity": "sha512-3oSNEQqU4ekIQTMqc7c9MJMHzSUAl4knG5mF9+1HaLqvUaYAfZPidqd4JWQKeYwe6Tw6fa79lcvUXqfCSXiEUg==", + "license": "MIT", + "dependencies": { + "@mjackson/node-fetch-server": "^0.2.0", + "@react-router/express": "7.14.1", + "@react-router/node": "7.14.1", + "compression": "^1.8.1", + "express": "^4.19.2", + "get-port": "5.1.1", + "morgan": "^1.10.1", + "source-map-support": "^0.5.21" + }, + "bin": { + "react-router-serve": "bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.14.1" + } + }, + "node_modules/@remix-run/node-fetch-server": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@remix-run/node-fetch-server/-/node-fetch-server-0.13.0.tgz", + "integrity": "sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "tailwindcss": "4.2.4" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", + "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", + "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "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/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.336", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", + "integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "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/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isbot": { + "version": "5.1.38", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.38.tgz", + "integrity": "sha512-Cus2702JamTNMEY4zTP+TShgq/3qzjvGcBC4XMOV45BLaxD4iUFENkqu7ZhFeSzwNsCSZLjnGlihDQznnpnEEA==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "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": "1.8.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", + "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", + "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/valibot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.3.1.tgz", + "integrity": "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-tsconfig-paths": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", + "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + } + }, + "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/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/web/Miqaat/package.json b/web/Miqaat/package.json new file mode 100644 index 0000000..716cf7f --- /dev/null +++ b/web/Miqaat/package.json @@ -0,0 +1,50 @@ +{ + "name": "project", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@dazl/color-scheme": "^1.2.1", + + "@react-router/node": "^7.14.1", + "@react-router/serve": "^7.14.1", + "classnames": "^2.5.1", + "isbot": "^5.1.38", + "lucide-react": "^1.8.0", + + "react": "^19.2.5", + "react-dom": "^19.2.5", + + "react-router": "^7.14.1" + }, + "devDependencies": { + "@dazl-lib/radix-ui": "^0.1.0", + "@dazl/component-gallery": "^2.0.1", + "@dazl/dev": "^1.4.0", + "@react-router/dev": "^7.14.1", + "@tailwindcss/vite": "^4.2.4", + "@types/node": "24", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "prettier": "^3.8.2", + "tailwindcss": "^4.2.4", + "typescript": "~6.0.2", + "vite": "^7.3.2", + "vite-tsconfig-paths": "^6.1.1" + }, + "prettier": { + "printWidth": 120, + "endOfLine": "auto" + }, + "overrides": { + "vite-tsconfig-paths": { + "typescript": "~6.0.2" + }, + "@radix-ui/react-primitive": "2.1.3" + } +} diff --git a/web/Miqaat/pictures/Pasted image (2).png b/web/Miqaat/pictures/Pasted image (2).png new file mode 100644 index 0000000..e5d53c4 Binary files /dev/null and b/web/Miqaat/pictures/Pasted image (2).png differ diff --git a/web/Miqaat/pictures/Pasted image (3).png b/web/Miqaat/pictures/Pasted image (3).png new file mode 100644 index 0000000..d68eb74 Binary files /dev/null and b/web/Miqaat/pictures/Pasted image (3).png differ diff --git a/web/Miqaat/pictures/Pasted image.png b/web/Miqaat/pictures/Pasted image.png new file mode 100644 index 0000000..82dfeb4 Binary files /dev/null and b/web/Miqaat/pictures/Pasted image.png differ diff --git a/web/Miqaat/pictures/photo_2026-04-21_06-54-32.jpg b/web/Miqaat/pictures/photo_2026-04-21_06-54-32.jpg new file mode 100644 index 0000000..1dbe159 Binary files /dev/null and b/web/Miqaat/pictures/photo_2026-04-21_06-54-32.jpg differ diff --git a/web/Miqaat/public/logo.jpg b/web/Miqaat/public/logo.jpg new file mode 100644 index 0000000..1dbe159 Binary files /dev/null and b/web/Miqaat/public/logo.jpg differ diff --git a/web/Miqaat/public/manifest.json b/web/Miqaat/public/manifest.json new file mode 100644 index 0000000..9a675b3 --- /dev/null +++ b/web/Miqaat/public/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "Prayer Times Algeria", + "short_name": "PrayerTimes", + "description": "Accurate prayer times for all Algerian wilayas and baladiyas.", + "start_url": "/", + "display": "standalone", + "background_color": "#0d1b2a", + "theme_color": "#1a5276", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/logo.jpg", + "sizes": "192x192 512x512", + "type": "image/jpeg", + "purpose": "any maskable" + } + ] +} diff --git a/web/Miqaat/public/service-worker.js b/web/Miqaat/public/service-worker.js new file mode 100644 index 0000000..76eb7b4 --- /dev/null +++ b/web/Miqaat/public/service-worker.js @@ -0,0 +1,31 @@ +/* Service Worker – Prayer Times PWA */ +/* This is a placeholder. Replace with workbox or custom caching strategy. */ + +const CACHE_NAME = "prayer-times-v1"; +const STATIC_ASSETS = ["/", "/manifest.json", "/favicon.svg"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)) + ) + ) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + event.respondWith( + caches.match(event.request).then((cached) => { + return cached || fetch(event.request); + }) + ); +}); diff --git a/web/Miqaat/react-router.config.ts b/web/Miqaat/react-router.config.ts new file mode 100644 index 0000000..d792978 --- /dev/null +++ b/web/Miqaat/react-router.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, + future: { + unstable_optimizeDeps: true, + }, +} satisfies Config; diff --git a/web/Miqaat/tsconfig.json b/web/Miqaat/tsconfig.json new file mode 100644 index 0000000..438d26f --- /dev/null +++ b/web/Miqaat/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/web/Miqaat/vite.config.ts b/web/Miqaat/vite.config.ts new file mode 100644 index 0000000..e6ee3c7 --- /dev/null +++ b/web/Miqaat/vite.config.ts @@ -0,0 +1,8 @@ +import { reactRouter } from "@react-router/dev/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], +});