diff --git a/app/components/shell.tsx b/app/components/shell.tsx new file mode 100644 index 0000000..8c29657 --- /dev/null +++ b/app/components/shell.tsx @@ -0,0 +1,110 @@ +"use client"; + +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; +import type { ReactNode } from "react"; + +const NAV = [ + { href: "/", label: "Gutscheine" }, + { href: "/vouchers/new", label: "Neu" }, + { href: "/contacts", label: "Kontakte" }, + { href: "/settings", label: "Einstellungen" }, +] as const; + +function isActive(pathname: string, href: string) { + if (href === "/") return pathname === "/"; + return pathname === href || pathname.startsWith(`${href}/`); +} + +export default function Shell({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + + async function logout() { + await fetch("/api/auth/logout", { method: "POST" }).catch(() => {}); + router.push("/login"); + router.refresh(); + } + + return ( +
+ + +
+
+

+ + Gutscheintracker +

+ +
+ +
+ {children} +
+ + +
+
+ ); +} diff --git a/app/contacts/page.tsx b/app/contacts/page.tsx index 2bdefa3..61694aa 100644 --- a/app/contacts/page.tsx +++ b/app/contacts/page.tsx @@ -1,10 +1,14 @@ "use client"; import { useEffect, useState } from "react"; import Link from "next/link"; +import Shell from "../components/shell"; import { validateContact } from "@/lib/contacts"; type Contact = { id: number; name: string; email: string | null; telegramChatId: string | null }; +const inputClass = + "mt-1.5 h-11 w-full rounded-field border border-line bg-white px-3.5 text-sm text-ink placeholder:text-muted/60"; + export default function ContactsPage() { const [contacts, setContacts] = useState([]); const [name, setName] = useState(""); @@ -39,34 +43,78 @@ export default function ContactsPage() { } return ( -
- ← Zurück -

Kontakte

-
-
- - setName(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" /> + + + ← Zurück + +

Kontakte

+

Mindestens E-Mail oder Telegram pro Kontakt.

+ +
+
+ + setName(e.target.value)} + autoComplete="off" + placeholder="z. B. Mama" + aria-invalid={errors.includes("name")} + className={`${inputClass} ${errors.includes("name") ? "border-danger" : ""}`} + /> +
+
+ + setEmail(e.target.value)} + autoComplete="email" + placeholder="name@beispiel.de" + aria-invalid={errors.includes("email")} + className={`${inputClass} ${errors.includes("email") ? "border-danger" : ""}`} + /> +
+
+ + setTelegramChatId(e.target.value)} + autoComplete="off" + placeholder="z. B. 123456789" + className={inputClass} + /> +
-
- - setEmail(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" /> -
-
- - setTelegramChatId(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" /> -
- {errors.length > 0 &&

Ungültig: {errors.join(", ")}

} - + {errors.length > 0 && ( +

+ Bitte prüfe die Eingaben: {errors.join(", ")} +

+ )} + -
    - {contacts.map((c) => ( -
  • - {c.name} - {c.email && · {c.email}} - {c.telegramChatId && · Telegram {c.telegramChatId}} -
  • - ))} -
-
+
+ {contacts.length === 0 ? ( +

Noch keine Kontakte angelegt.

+ ) : ( + + )} +
+ ); } diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..63b96ab --- /dev/null +++ b/app/globals.css @@ -0,0 +1,41 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + + --color-brand-50: #eef5f0; + --color-brand-100: #dcebe1; + --color-brand-600: #397b58; + --color-brand-700: #306e4e; + --color-brand-800: #285a41; + + --color-ink: #202823; + --color-muted: #5f6d63; + --color-line: #dde5de; + --color-wash: #f2f5f2; + --color-danger: #9c3838; + + --radius-card: 16px; + --radius-field: 10px; +} + +body { + background-color: var(--color-wash); + color: var(--color-ink); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; +} + +:where(a, button, input, select, textarea):focus-visible { + outline: 3px solid rgb(57 123 88 / 0.35); + outline-offset: 2px; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition: none !important; + animation: none !important; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 907f255..0183c17 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,10 +1,17 @@ import type { ReactNode } from "react"; +import { Geist } from "next/font/google"; +import "./globals.css"; -export const metadata = { title: "Gutscheintracker" }; +const geist = Geist({ subsets: ["latin"], display: "swap" }); + +export const metadata = { + title: "Gutscheintracker", + description: "Gutscheine verwalten und rechtzeitig an das Ablaufdatum erinnern.", +}; export default function RootLayout({ children }: { children: ReactNode }) { return ( - + {children} ); diff --git a/app/login/page.tsx b/app/login/page.tsx index 995f2bd..3a17c66 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -33,27 +33,73 @@ export default function LoginPage() { } return ( -
-

Anmelden

-
- - - {error &&

{error}

} - -
+
+
+

+ + Gutscheintracker +

+
+

+ Willkommen zurück +

+

+ Melde dich mit eurem gemeinsamen Haushaltszugang an. +

+
+
+ + setUser(e.target.value)} + autoComplete="username" + placeholder="haushalt" + className="h-[46px] rounded-field border border-line bg-white px-3.5 text-sm text-ink placeholder:text-muted/70" + /> +
+
+ + setPassword(e.target.value)} + autoComplete="current-password" + placeholder="Dein Passwort" + className="h-[46px] rounded-field border border-line bg-white px-3.5 text-sm text-ink placeholder:text-muted/70" + /> +
+ {error && ( +

+ {error} +

+ )} + +
+

+ Privater Zugang für euren Haushalt +

+
+
); } diff --git a/app/page.tsx b/app/page.tsx index e37eb6d..7720bb6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,8 @@ import Link from "next/link"; import { Suspense } from "react"; +import Shell from "./components/shell"; import { getDb } from "@/lib/db"; -import { getVoucherStatus } from "@/lib/voucher-status"; +import { getVoucherStatus, type VoucherStatus } from "@/lib/voucher-status"; import { formatEuro, isExpiringSoon } from "@/lib/vouchers"; const FILTERS = [ @@ -12,13 +13,25 @@ const FILTERS = [ { key: "eingeloest", label: "Eingelöst" }, ] as const; +const STATUS_STYLE: Record = { + aktiv: "bg-brand-50 text-brand-800", + abgelaufen: "bg-danger/10 text-danger", + eingeloest: "bg-wash text-muted", +}; + +function statusLabel(s: VoucherStatus) { + if (s === "aktiv") return "Aktiv"; + if (s === "abgelaufen") return "Abgelaufen"; + return "Eingelöst"; +} + function Skeleton() { return ( -
    +
      {[0, 1, 2].map((i) => ( -
    • -
      -
      +
    • +
      +
    • ))}
    @@ -38,26 +51,38 @@ async function VoucherList({ filter }: { filter: string }) { }); if (filtered.length === 0) { return ( -
    -

    Keine Gutscheine gefunden.

    - +
    +

    Keine Gutscheine gefunden.

    + Gutschein anlegen
    ); } return ( -
      - {filtered.map((v) => ( -
    • - - {v.name} - -

      - {v.anbieter} · {formatEuro(v.restCents)} / {formatEuro(v.wertCents)} · {getVoucherStatus(v)} -

      -
    • - ))} +
        + {filtered.map((v) => { + const s = getVoucherStatus(v); + return ( +
      • +
        + + {v.name} + + + {statusLabel(s)} + +
        +

        + {v.anbieter} · {formatEuro(v.restCents)} von {formatEuro(v.wertCents)} + {v.ablaufdatum ? ` · bis ${v.ablaufdatum.toLocaleDateString("de-DE")}` : ""} +

        +
      • + ); + })}
      ); } @@ -65,20 +90,30 @@ async function VoucherList({ filter }: { filter: string }) { export default async function Home({ searchParams }: { searchParams: Promise<{ filter?: string }> }) { const { filter = "" } = await searchParams; return ( -
      -
      -

      Gutscheine

      - + +
      +
      +

      Gutscheine

      +

      Alle Gutscheine eures Haushalts im Blick.

      +
      + Neu
      -
      + ); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index e35bd25..9d9a1e9 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,39 +1,50 @@ import Link from "next/link"; +import Shell from "../components/shell"; import { getDb } from "@/lib/db"; import { DEFAULT_TAGE_VORHER } from "@/lib/settings"; import { TestButtons } from "./test-buttons"; export const dynamic = "force-dynamic"; +function Card({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
      +

      {title}

      +
      {children}
      +
      + ); +} + export default async function SettingsPage() { const db = getDb(); const last = await db.notificationLog.findFirst({ orderBy: { sentAt: "desc" } }); return ( -
      - ← Zurück -

      Einstellungen

      -
      -

      Standard-Erinnerungstage

      -

      Neue Gutscheine erhalten diese Erinnerungsregeln: {DEFAULT_TAGE_VORHER.join(", ")} Tage vorher.

      -
      - -
      -

      Letzter Cron-Lauf

      - {last ? ( -

      - {last.sentAt.toLocaleString("de-DE")} · {last.kanal} an {last.empfaenger} · {last.status} - {last.fehler ? ` · Fehler: ${last.fehler}` : ""} -

      - ) : ( -

      Noch keine Benachrichtigung versendet.

      - )} -
      -
      -

      Konfiguration (.env)

      -

      SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, MAIL_FROM, TELEGRAM_BOT_TOKEN, CRON_SECRET, APP_URL.

      -

      Backup

      -

      SQLite-Datei sichern: Volume gutschein-data bzw. Datei /data/gutscheine.db regelmäßig kopieren.

      -
      -
      + + + ← Zurück + +

      Einstellungen

      +

      Erinnerungen und Benachrichtigungen prüfen.

      +
      + +

      Neue Gutscheine erhalten diese Erinnerungsregeln: {DEFAULT_TAGE_VORHER.join(", ")} Tage vorher.

      +
      + + + {last ? ( +

      + {last.sentAt.toLocaleString("de-DE")} · {last.kanal} an {last.empfaenger} · {last.status} + {last.fehler ? ` · Fehler: ${last.fehler}` : ""} +

      + ) : ( +

      Noch keine Benachrichtigung versendet.

      + )} +
      + +

      SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, MAIL_FROM, TELEGRAM_BOT_TOKEN, CRON_SECRET, APP_URL.

      +

      SQLite-Datei sichern: Volume gutschein-data bzw. Datei /data/gutscheine.db regelmäßig kopieren.

      +
      +
      +
      ); } diff --git a/app/settings/test-buttons.tsx b/app/settings/test-buttons.tsx index c398506..4f92774 100644 --- a/app/settings/test-buttons.tsx +++ b/app/settings/test-buttons.tsx @@ -29,19 +29,43 @@ export function TestButtons() { } return ( -
      -

      Versand testen

      -
      - - setMail(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" /> +
      +

      Versand testen

      +
      +
      + + setMail(e.target.value)} + className="mt-1.5 h-11 w-full rounded-field border border-line bg-white px-3.5 text-sm text-ink" + /> + +
      +
      + + setChatId(e.target.value)} + className="mt-1.5 h-11 w-full rounded-field border border-line bg-white px-3.5 text-sm text-ink" + /> + +
      - -
      - - setChatId(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" /> -
      - - {result &&

      {result}

      } + {result &&

      {result}

      }
      ); } diff --git a/app/vouchers/[id]/page.tsx b/app/vouchers/[id]/page.tsx index 48dafbe..7bd85c1 100644 --- a/app/vouchers/[id]/page.tsx +++ b/app/vouchers/[id]/page.tsx @@ -1,6 +1,7 @@ "use client"; import { use, useEffect, useState } from "react"; import Link from "next/link"; +import Shell from "../../components/shell"; import { formatEuro } from "@/lib/vouchers"; import { getVoucherStatus } from "@/lib/voucher-status"; @@ -18,6 +19,15 @@ type Voucher = { contacts?: { contact: { id: number; name: string; email: string | null; telegramChatId: string | null } }[]; }; +function Row({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
      +
      {label}
      +
      {children}
      +
      + ); +} + export default function VoucherDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); const [v, setV] = useState(null); @@ -41,37 +51,61 @@ export default function VoucherDetailPage({ params }: { params: Promise<{ id: st if (res.ok) setV(await res.json()); } - if (loading) return
      ; + if (loading) + return ( + +
      +
      +
      +
      + + ); if (error || !v) return ( -
      -

      {error}

      - Zurück zur Liste -
      + +
      +

      {error}

      + + Zurück zur Liste + +
      +
      ); + const status = getVoucherStatus({ + status: v.status, + ablaufdatum: v.ablaufdatum ? new Date(v.ablaufdatum) : null, + }); + return ( -
      - ← Zurück -

      {v.name}

      -
      -
      Anbieter
      {v.anbieter}
      -
      Wert
      {formatEuro(v.wertCents)}
      -
      Rest
      {formatEuro(v.restCents)}
      -
      Status
      {getVoucherStatus({ status: v.status, ablaufdatum: v.ablaufdatum ? new Date(v.ablaufdatum) : null })}
      - {v.ablaufdatum &&
      Ablaufdatum
      {new Date(v.ablaufdatum).toLocaleDateString("de-DE")}
      } - {v.codeText &&
      Code
      {v.codeText}
      } - {v.kategorie &&
      Kategorie
      {v.kategorie}
      } - {v.notiz &&
      Notiz
      {v.notiz}
      } - {v.contacts && v.contacts.length > 0 && ( -
      Empfänger
      {v.contacts.map((c) => c.contact.name).join(", ")}
      + + + ← Zurück + +

      {v.name}

      +
      + {v.anbieter} + {formatEuro(v.wertCents)} + {formatEuro(v.restCents)} + {status === "aktiv" ? "Aktiv" : status === "abgelaufen" ? "Abgelaufen" : "Eingelöst"} + {v.ablaufdatum && ( + {new Date(v.ablaufdatum).toLocaleDateString("de-DE")} )} -
      + {v.codeText && {v.codeText}} + {v.kategorie && {v.kategorie}} + {v.notiz && {v.notiz}} + {v.contacts && v.contacts.length > 0 && ( + {v.contacts.map((c) => c.contact.name).join(", ")} + )} +
      {v.status !== "eingeloest" && ( - )} -
+ ); } diff --git a/app/vouchers/new/page.tsx b/app/vouchers/new/page.tsx index 00cfaac..3a469ac 100644 --- a/app/vouchers/new/page.tsx +++ b/app/vouchers/new/page.tsx @@ -1,21 +1,26 @@ "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Shell from "../../components/shell"; import { validateVoucher } from "@/lib/vouchers"; // Erweiterbar für Task 5 (Kontakte): zusätzliches Feld contactIds + // ohne bestehende Felder anzufassen — Felder als Konfiguration belassen. const FIELDS = [ - { name: "name", label: "Name", type: "text" }, - { name: "anbieter", label: "Anbieter", type: "text" }, - { name: "wertCents", label: "Wert (Cent)", type: "number" }, - { name: "restCents", label: "Rest (Cent, leer = Wert)", type: "number" }, - { name: "ablaufdatum", label: "Ablaufdatum", type: "date" }, - { name: "codeText", label: "Code", type: "text" }, - { name: "kategorie", label: "Kategorie", type: "text" }, - { name: "notiz", label: "Notiz", type: "text" }, + { name: "name", label: "Name", type: "text", placeholder: "z. B. Therme Erding" }, + { name: "anbieter", label: "Anbieter", type: "text", placeholder: "z. B. Therme Erding" }, + { name: "wertCents", label: "Wert (Cent)", type: "number", placeholder: "5000" }, + { name: "restCents", label: "Rest (Cent, leer = Wert)", type: "number", placeholder: "" }, + { name: "ablaufdatum", label: "Ablaufdatum", type: "date", placeholder: "" }, + { name: "codeText", label: "Code", type: "text", placeholder: "Code oder Nummer" }, + { name: "kategorie", label: "Kategorie", type: "text", placeholder: "z. B. Erlebnis" }, + { name: "notiz", label: "Notiz", type: "text", placeholder: "Optionale Notiz" }, ] as const; +const inputClass = + "mt-1.5 h-11 w-full rounded-field border border-line bg-white px-3.5 text-sm text-ink placeholder:text-muted/60"; + export default function NewVoucherPage() { const router = useRouter(); const [values, setValues] = useState>({}); @@ -69,27 +74,37 @@ export default function NewVoucherPage() { } return ( -
-

Gutschein anlegen

-
- {FIELDS.map((f) => ( -
- - set(f.name, e.target.value)} - className="mt-1 w-full rounded border px-3 py-2" - /> - {errors.includes(f.name) &&

Ungültig: {f.name}

} -
- ))} -
-
+ ); } diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package-lock.json b/package-lock.json index 998053e..9d5e832 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,13 +7,15 @@ "name": "gutscheintracker", "dependencies": { "@prisma/client": "6.5.0", + "@tailwindcss/postcss": "^4.3.3", "bcryptjs": "2.4.3", "jose": "5.9.6", "next": "15.3.0", "nodemailer": "6.9.14", "prisma": "6.5.0", "react": "19.0.0", - "react-dom": "19.0.0" + "react-dom": "19.0.0", + "tailwindcss": "^4.3.3" }, "devDependencies": { "@types/bcryptjs": "2.4.6", @@ -25,6 +27,18 @@ "vitest": "3.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@emnapi/runtime": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", @@ -965,13 +979,51 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -1619,6 +1671,302 @@ "tslib": "^2.8.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/postcss/node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "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.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -1941,11 +2289,23 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2058,6 +2418,21 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "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==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "5.9.6", "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", @@ -2067,6 +2442,267 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2078,7 +2714,6 @@ "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" @@ -2456,6 +3091,25 @@ } } }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/package.json b/package.json index fd60786..109faf2 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,15 @@ }, "dependencies": { "@prisma/client": "6.5.0", + "@tailwindcss/postcss": "^4.3.3", "bcryptjs": "2.4.3", "jose": "5.9.6", "next": "15.3.0", "nodemailer": "6.9.14", "prisma": "6.5.0", "react": "19.0.0", - "react-dom": "19.0.0" + "react-dom": "19.0.0", + "tailwindcss": "^4.3.3" }, "devDependencies": { "@types/bcryptjs": "2.4.6", diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..c2ddf74 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000..bd2ea78 Binary files /dev/null and b/prisma/dev.db differ