diff --git a/.env.template b/.env.template
new file mode 100644
index 0000000..35fc376
--- /dev/null
+++ b/.env.template
@@ -0,0 +1,8 @@
+#FIREBASE SETUP
+VITE_FIREBASE_API_KEY=""
+VITE_FIREBASE_AUTH_DOMAIN=""
+VITE_FIREBASE_PROJECT_ID=""
+VITE_FIREBASE_STORAGE_BUCKET=""
+VITE_FIREBASE_MESSAGING_SENDER_ID=""
+VITE_FIREBASE_APP_ID=""
+VITE_FIREBASE_MEASUREMENT_ID=""
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9b7c041..a4afddb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,6 @@
# React Router
/.react-router/
/build/
+
+#environment files
+.env
\ No newline at end of file
diff --git a/app/app.css b/app/app.css
index 242d4f3..e17ea9a 100644
--- a/app/app.css
+++ b/app/app.css
@@ -8,7 +8,7 @@
html,
body {
- @apply bg-white dark:bg-gray-950;
+ @apply bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
diff --git a/app/firebase.js b/app/firebase.js
new file mode 100644
index 0000000..1de2e7b
--- /dev/null
+++ b/app/firebase.js
@@ -0,0 +1,15 @@
+import { initializeApp } from "firebase/app";
+import { getFirestore } from "firebase/firestore";
+
+const firebaseConfig = {
+ apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
+ authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
+ projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
+ storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
+ appId: import.meta.env.VITE_FIREBASE_APP_ID,
+ measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
+};
+
+const app = initializeApp(firebaseConfig);
+export const db = getFirestore(app);
diff --git a/app/root.tsx b/app/root.tsx
index e35a017..7bd0f41 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -9,6 +9,7 @@ import {
import type { Route } from "./+types/root";
import "./app.css";
+import { AuthProvider } from "./services/firebase_provider";
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
@@ -36,7 +37,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
- {children}
+ {children}
diff --git a/app/routes.ts b/app/routes.ts
index 47ab806..62043ff 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -1,3 +1,16 @@
-import { type RouteConfig, index } from "@react-router/dev/routes";
+import { type RouteConfig, index, route } from "@react-router/dev/routes";
-export default [index("routes/starter.tsx")] satisfies RouteConfig;
+export default [
+ //protected routes
+ route("", "routes/require_auth.tsx", [index("routes/_index.tsx")]),
+
+ //routes require user to not be logged in
+ route("", "routes/require_guest.tsx", [
+ route("login", "routes/login.tsx"),
+ route("create-account", "routes/create_account.tsx"),
+ ]),
+
+ //routes available to anyone
+ route("forgot-pass", "routes/forgot_pass.tsx"),
+ route("verify-email", "routes/verify_email.tsx"),
+] satisfies RouteConfig;
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
new file mode 100644
index 0000000..ffa6814
--- /dev/null
+++ b/app/routes/_index.tsx
@@ -0,0 +1,337 @@
+//This is the home page
+import React, { useMemo, useState } from "react";
+import { ChevronDown, ChevronUp, Search } from "lucide-react";
+import { useAuth } from "~/services/firebase_provider";
+
+// --- Temporary JSON data ---
+type Status = "Active" | "Follow-up" | "Pending";
+type SortKey = "name" | "socialWorker" | "lastContact" | "status";
+
+type Patient = {
+ id: string;
+ name: string;
+ socialWorker: string;
+ lastContact: string; // ISO date (international standard)
+ status: Status;
+};
+
+// Sample patient data
+const PATIENTS: Patient[] = [
+ {
+ id: "1",
+ name: "David Thompson",
+ socialWorker: "Emily Chen",
+ lastContact: "2025-10-18",
+ status: "Active",
+ },
+ {
+ id: "2",
+ name: "James Brown",
+ socialWorker: "Jake Morrison",
+ lastContact: "2025-10-17",
+ status: "Follow-up",
+ },
+ {
+ id: "3",
+ name: "Jennifer Wilson",
+ socialWorker: "Emily Chen",
+ lastContact: "2025-10-19",
+ status: "Follow-up",
+ },
+ {
+ id: "4",
+ name: "Linda Anderson",
+ socialWorker: "Jake Morrison",
+ lastContact: "2025-10-21",
+ status: "Pending",
+ },
+ {
+ id: "5",
+ name: "Maria Garcia",
+ socialWorker: "Sarah Thompson",
+ lastContact: "2025-10-24",
+ status: "Active",
+ },
+];
+
+// --- Utilities ---
+function formatDate(iso: string) {
+ const d = new Date(iso);
+ return d.toLocaleDateString("en-US", {
+ month: "short",
+ day: "2-digit",
+ year: "numeric",
+ });
+}
+
+function statusStyles(status: Status) {
+ // recommended by someone on Figma document
+ switch (status) {
+ case "Active":
+ return "bg-green-50 text-green-700 ring-1 ring-green-200";
+ case "Follow-up":
+ return "bg-yellow-50 text-yellow-800 ring-1 ring-yellow-200";
+ case "Pending":
+ return "bg-gray-100 text-gray-700 ring-1 ring-gray-200";
+ }
+}
+
+// --- Component ---
+export default function HomePage() {
+ const { user, logout } = useAuth();
+ const [query, setQuery] = useState("");
+ const [sortKey, setSortKey] = useState("name");
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
+
+ // Compute totals
+ const totals = useMemo(() => {
+ const total = PATIENTS.length;
+ const active = PATIENTS.filter((p) => p.status === "Active").length;
+ const followUps = PATIENTS.filter(
+ (p) => p.status === "Follow-up"
+ ).length;
+ return { total, active, followUps };
+ }, []);
+
+ // Filtered patients
+ const filtered = useMemo(() => {
+ if (!query.trim()) return PATIENTS;
+ const q = query.toLowerCase();
+ return PATIENTS.filter(
+ (p) =>
+ p.name.toLowerCase().includes(q) ||
+ p.socialWorker.toLowerCase().includes(q)
+ );
+ }, [query]);
+
+ // Sorted patients
+ const sorted = useMemo(() => {
+ const copy = [...filtered];
+
+ const statusOrder: Record = {
+ Active: 0,
+ "Follow-up": 1,
+ Pending: 2,
+ };
+
+ // Function to get the value to sort by
+ const val = (p: Patient, key: SortKey): string | number => {
+ switch (key) {
+ case "lastContact":
+ return new Date(p.lastContact).getTime();
+ case "status":
+ return statusOrder[p.status];
+ case "name":
+ return p.name.toLowerCase();
+ case "socialWorker":
+ return p.socialWorker.toLowerCase();
+ }
+ };
+
+ copy.sort((a, b) => {
+ const av = val(a, sortKey);
+ const bv = val(b, sortKey);
+ if (av < bv) return sortDir === "asc" ? -1 : 1;
+ if (av > bv) return sortDir === "asc" ? 1 : -1;
+ return 0;
+ });
+
+ return copy;
+ }, [filtered, sortDir, sortKey]);
+
+ // Toggle sort direction
+ function toggleSort(key: SortKey) {
+ if (sortKey === key) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
+ else {
+ setSortKey(key);
+ setSortDir("asc");
+ }
+ }
+
+ // Sort Icon Component (uses lucide icons for up/down arrows)
+ const SortIcon = ({ active }: { active: boolean }) =>
+ active ? (
+ sortDir === "asc" ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ );
+
+ return (
+
+ {/* Top Bar */}
+
+
+ {/* Logo */}
+
+
+
+
+ {/* Search */}
+
+
+ setQuery(e.target.value)}
+ placeholder="Search patients or social workers..."
+ className="w-full rounded-2xl border border-gray-200 bg-white py-2.5 pl-10 pr-4 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-green-600"
+ />
+
+
+ {/* Welcome / Logout */}
+
+ Welcome, {user?.displayName} {" "}
+
+ Logout
+
+
+
+
+
+ {/* Main */}
+
+
+ Patient Dashboard
+
+
+ Manage and view all patients and their assigned social
+ workers. Click on any patient name to view their detailed
+ profile.
+
+
+ {/* Stat Cards */}
+
+
+ {/* Table */}
+
+
+
+
+
+
+ toggleSort("name")}
+ className={`inline-flex items-center hover:text-gray-900 ${sortKey === "name" ? "underline underline-offset-2 decoration-1 font-bold" : ""}`}
+ >
+ Patient Name
+
+
+
+
+
+ toggleSort("socialWorker")
+ }
+ className={`inline-flex items-center hover:text-gray-900 ${sortKey === "socialWorker" ? "underline underline-offset-2 decoration-1 font-bold" : ""}`}
+ >
+ Assigned Social Worker
+
+
+
+
+
+ toggleSort("lastContact")
+ }
+ className={`inline-flex items-center hover:text-gray-900 ${sortKey === "lastContact" ? "underline underline-offset-2 decoration-1 font-bold" : ""}`}
+ >
+ Last Contact
+
+
+
+
+ Status
+
+
+ Actions
+
+
+
+
+ {sorted.map((p) => (
+
+
+
+ {p.name}
+
+
+
+ {p.socialWorker}
+
+
+ {formatDate(p.lastContact)}
+
+
+
+ {p.status}
+
+
+
+
+ alert(
+ `Open profile for ${p.name}`
+ )
+ }
+ className="rounded-xl bg-black px-4 py-2 text-sm font-medium text-white shadow hover:bg-gray-800"
+ >
+ View Profile
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+// --- Small components ---
+function StatCard({ label, value }: { label: string; value: number }) {
+ return (
+
+
{label}
+
+ {value}
+
+
+ );
+}
diff --git a/app/routes/change_password.tsx b/app/routes/change_password.tsx
new file mode 100644
index 0000000..b961551
--- /dev/null
+++ b/app/routes/change_password.tsx
@@ -0,0 +1,67 @@
+import React from "react";
+import { Link, useNavigate } from "react-router";
+
+/*
+ * Currently not in use. Password reset is handled by Firebase's built in email flow, and users are expected to click the link in their email to reset their password. This page could be used as a custom password reset flow if we wanted to implement that in the future.
+ */
+
+export default function ChangePassword() {
+ const navigate = useNavigate();
+ return (
+
+
+
+
+ {"<"} Back to login
+
+
+
+
+
+
+
+
Change Password
+
+ Create a new password. It must include 12 characters and
+ at least one number.
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/create_account.tsx b/app/routes/create_account.tsx
new file mode 100644
index 0000000..f5de7e5
--- /dev/null
+++ b/app/routes/create_account.tsx
@@ -0,0 +1,177 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router";
+import { signUpWithEmail } from "~/services/auth_service";
+
+export default function CreateAccount() {
+ const navigate = useNavigate();
+
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [displayName, setDisplayName] = useState("");
+ const [acceptedTerms, setAcceptedTerms] = useState(false);
+
+ const [error, setError] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (isSubmitting) return;
+
+ setError(null);
+
+ if (password !== confirmPassword) {
+ setError("Passwords do not match.");
+ return;
+ }
+
+ if (!acceptedTerms) {
+ setError("You must accept the Terms and Conditions.");
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ await signUpWithEmail({
+ email,
+ password,
+ displayName,
+ remember: false,
+ });
+ navigate("/verify-email");
+ } catch (err: unknown) {
+ const message =
+ err instanceof Error ? err.message : "Failed to create account";
+ setError(message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
+
+ {"<"} Back to login
+
+
+
+
+
+
+
+
+
+
Create Account
+
+
+
+
+
+ );
+}
diff --git a/app/routes/forgot_pass.tsx b/app/routes/forgot_pass.tsx
new file mode 100644
index 0000000..53b121b
--- /dev/null
+++ b/app/routes/forgot_pass.tsx
@@ -0,0 +1,72 @@
+import React, { useState } from "react";
+import { Link } from "react-router";
+import { sendPasswordResetEmail } from "firebase/auth";
+import { auth } from "~/services/firebase_app";
+
+export default function ForgotPassword() {
+ const [email, setEmail] = useState("");
+ const [sending, setSending] = useState(false);
+
+ const onClickContinue = async () => {
+ const trimmedEmail = email.trim();
+ if (!trimmedEmail) {
+ alert("Please enter your email.");
+ return;
+ }
+
+ setSending(true);
+ try {
+ await sendPasswordResetEmail(auth, trimmedEmail);
+ alert("Password reset email sent. Please check your inbox.");
+ } finally {
+ setSending(false);
+ }
+ };
+ return (
+
+
+
+
+ {"<"} Back to login
+
+
+
+
+
+
+
+
Forgot Password?
+
+
+
+
+
+ );
+}
diff --git a/app/routes/login.tsx b/app/routes/login.tsx
new file mode 100644
index 0000000..707403f
--- /dev/null
+++ b/app/routes/login.tsx
@@ -0,0 +1,139 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router";
+import { loginWithEmail } from "../services/auth_service";
+
+export default function Login() {
+ const navigate = useNavigate();
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [remember, setRemember] = useState(false);
+
+ const [error, setError] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ async function onClickLogin(e: React.FormEvent) {
+ e.preventDefault();
+ if (isSubmitting) return;
+
+ setError(null);
+ setIsSubmitting(true);
+ try {
+ await loginWithEmail({
+ email,
+ password,
+ remember,
+ });
+ navigate("/");
+ } catch (err: unknown) {
+ const message =
+ err instanceof Error ? err.message : "Failed to login";
+ setError(message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
+
+
Login
+
+ Enter your email address and password to access your
+ account.
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/require_auth.tsx b/app/routes/require_auth.tsx
new file mode 100644
index 0000000..e327374
--- /dev/null
+++ b/app/routes/require_auth.tsx
@@ -0,0 +1,34 @@
+import { useEffect, useState } from "react";
+import { Outlet, useLocation, useNavigate } from "react-router";
+import { useAuth } from "../services/firebase_provider";
+
+export default function RequireAuth() {
+ const { user, loading } = useAuth();
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ // Prevent redirects during the server/StaticRouter render
+ const [hydrated, setHydrated] = useState(false);
+ useEffect(() => setHydrated(true), []);
+
+ useEffect(() => {
+ if (!hydrated) return;
+ if (loading) return;
+
+ if (!user) {
+ navigate("/login", { replace: true, state: { from: location } });
+ }
+ if (user && !user.emailVerified) {
+ navigate("/verify-email", {
+ replace: true,
+ state: { from: location },
+ });
+ }
+ }, [hydrated, loading, user, navigate, location]);
+
+ if (!hydrated || loading) return Loading...
;
+ if (!user) return null; // navigation effect will run
+ if (user && !user.emailVerified) return null;
+
+ return ;
+}
diff --git a/app/routes/require_guest.tsx b/app/routes/require_guest.tsx
new file mode 100644
index 0000000..698f6e5
--- /dev/null
+++ b/app/routes/require_guest.tsx
@@ -0,0 +1,33 @@
+import { Outlet, useLocation, useNavigate } from "react-router";
+import { useAuth } from "../services/firebase_provider";
+import { useEffect, useState } from "react";
+
+export default function RequireGuest() {
+ const { user, loading } = useAuth();
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ // Prevent redirects during the server/StaticRouter render
+ const [hydrated, setHydrated] = useState(false);
+ useEffect(() => setHydrated(true), []);
+
+ useEffect(() => {
+ if (!hydrated) return;
+ if (loading) return;
+
+ if (user && user.emailVerified) {
+ navigate("/", { replace: true, state: { from: location } });
+ }
+ if (user && !user.emailVerified) {
+ navigate("/verify-email", {
+ replace: true,
+ state: { from: location },
+ });
+ }
+ }, [hydrated, loading, user, navigate, location]);
+
+ if (!hydrated || loading) return Loading...
;
+ if (user) return null; // navigation effect will run
+
+ return ;
+}
diff --git a/app/routes/starter.tsx b/app/routes/starter.tsx
deleted file mode 100644
index d82256c..0000000
--- a/app/routes/starter.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-import React from "react";
-import {
- SiVite,
- SiReact,
- SiTypescript,
- SiDocker,
- SiEslint,
- SiPrettier,
- SiTailwindcss,
- SiReactrouter,
-} from "react-icons/si";
-import { VscExtensions } from "react-icons/vsc";
-
-export function meta() {
- return [
- { title: "Vite + React Starter Template" },
- {
- name: "description",
- content:
- "A modern React starter template built with Vite, featuring TypeScript, ESLint, Prettier, and more.",
- },
- ];
-}
-
-export default function Home() {
- const features = [
- {
- icon: SiVite,
- name: "Vite",
- description: "Lightning fast build tool and dev server",
- color: "#646CFF",
- },
- {
- icon: SiReact,
- name: "React 18",
- description: "Latest React with concurrent features",
- color: "#61DAFB",
- },
- {
- icon: SiTypescript,
- name: "TypeScript",
- description: "Type-safe JavaScript development",
- color: "#3178C6",
- },
- {
- icon: SiDocker,
- name: "Docker",
- description: "Containerized development and deployment",
- color: "#2496ED",
- },
- {
- icon: SiEslint,
- name: "ESLint",
- description: "Code linting and quality enforcement",
- color: "#4B32C3",
- },
- {
- icon: SiPrettier,
- name: "Prettier",
- description: "Automatic code formatting",
- color: "#F7B93E",
- },
- {
- icon: SiTailwindcss,
- name: "Tailwind CSS",
- description: "Utility-first CSS framework",
- color: "#06B6D4",
- },
- {
- icon: SiReactrouter,
- name: "React Router",
- description: "Modern routing for React applications",
- color: "#CA4245",
- },
- {
- icon: VscExtensions,
- name: "VS Code Extensions",
- description: "Pre-configured development extensions",
- color: "#007ACC",
- },
- ];
-
- return (
-
- {/* Header Section */}
-
-
- {/* Features Grid */}
-
- {features.map((feature, index) => {
- const IconComponent = feature.icon;
- return (
- {
- e.currentTarget.style.transform =
- "translateY(-2px)";
- e.currentTarget.style.boxShadow =
- "0 8px 25px rgba(5, 150, 105, 0.25), 0 4px 10px rgba(5, 150, 105, 0.15)";
- }}
- onMouseOut={(e) => {
- e.currentTarget.style.transform =
- "translateY(0)";
- e.currentTarget.style.boxShadow =
- "0 4px 6px rgba(5, 150, 105, 0.15), 0 1px 3px rgba(5, 150, 105, 0.1)";
- }}
- >
-
-
-
-
- {feature.name}
-
-
- {feature.description}
-
-
- );
- })}
-
-
- {/* Getting Started */}
-
-
- Ready to Start Building?
-
-
- This template includes everything you need for modern React
- development. Just clone, install, and start coding!
-
-
- npm install && npm run dev
-
-
-
- );
-}
diff --git a/app/routes/verify_email.tsx b/app/routes/verify_email.tsx
new file mode 100644
index 0000000..753754a
--- /dev/null
+++ b/app/routes/verify_email.tsx
@@ -0,0 +1,95 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router";
+import { useAuth } from "~/services/firebase_provider";
+import { sendEmailVerification } from "firebase/auth";
+
+export default function VerifyEmail() {
+ const navigate = useNavigate();
+ const { user } = useAuth();
+ const [checkingVerification, setCheckingVerification] = useState(false);
+ const [resendingEmail, setResendingEmail] = useState(false);
+
+ const onClickContinue = async () => {
+ if (!user) {
+ alert("You’re not signed in. Please log in again.");
+ navigate("/login");
+ return;
+ }
+
+ setCheckingVerification(true);
+ try {
+ // Firebase doesn't automatically refresh `emailVerified` just because
+ // the user clicked the verification link in a different tab/window.
+ await user.reload();
+
+ if (!user.emailVerified) {
+ alert("Please verify your email before continuing.");
+ return;
+ }
+
+ navigate("/");
+ } finally {
+ setCheckingVerification(false);
+ }
+ };
+
+ const onClickResendEmail = async () => {
+ if (!user) {
+ alert("You’re not signed in. Please log in again.");
+ navigate("/login");
+ return;
+ }
+
+ setResendingEmail(true);
+ try {
+ await sendEmailVerification(user);
+ alert("Verification email sent. Please check your inbox.");
+ } finally {
+ setResendingEmail(false);
+ }
+ };
+
+ return (
+
+
+
+
+ {"<"} Back to login
+
+
+
+
+
+
+
+
Verify Email
+
+ A verification link was sent to your email. Please click
+ the link to verify your account.
+
+
+
+
+ After clicking the link, click below.
+
+
+
+ {checkingVerification ? "CHECKING…" : "CONTINUE"}
+
+
+ {resendingEmail ? "SENDING…" : "Send New Link"}
+
+
+
+ );
+}
diff --git a/app/services/auth_service.ts b/app/services/auth_service.ts
new file mode 100644
index 0000000..c189831
--- /dev/null
+++ b/app/services/auth_service.ts
@@ -0,0 +1,68 @@
+import {
+ browserLocalPersistence,
+ browserSessionPersistence,
+ createUserWithEmailAndPassword,
+ sendEmailVerification,
+ setPersistence,
+ updateProfile,
+ type UserCredential,
+ signInWithEmailAndPassword,
+} from "firebase/auth";
+
+import { auth } from "./firebase_app";
+
+export type SignUpInput = {
+ email: string;
+ password: string;
+ displayName?: string;
+ remember?: boolean; // if true: persist across browser restarts
+};
+
+export type LoginInput = {
+ email: string;
+ password: string;
+ remember?: boolean; // if true: persist across browser restarts
+};
+
+export async function signUpWithEmail(
+ input: SignUpInput
+): Promise {
+ const remember = input.remember ?? true;
+
+ await setPersistence(
+ auth,
+ remember ? browserLocalPersistence : browserSessionPersistence
+ );
+
+ const cred = await createUserWithEmailAndPassword(
+ auth,
+ input.email,
+ input.password
+ );
+
+ if (input.displayName) {
+ await updateProfile(cred.user, { displayName: input.displayName });
+ }
+
+ // verify email
+ await sendEmailVerification(cred.user);
+
+ return cred;
+}
+
+export async function loginWithEmail(input: LoginInput) {
+ const remember = input.remember ?? true;
+
+ await setPersistence(
+ auth,
+ remember ? browserLocalPersistence : browserSessionPersistence
+ );
+
+ const cred = await signInWithEmailAndPassword(
+ auth,
+ input.email,
+ input.password
+ );
+
+ return cred;
+}
diff --git a/app/services/firebase_app.ts b/app/services/firebase_app.ts
new file mode 100644
index 0000000..9bed28b
--- /dev/null
+++ b/app/services/firebase_app.ts
@@ -0,0 +1,25 @@
+// Import the functions you need from the SDKs you need
+import { initializeApp } from "firebase/app";
+//import { getAnalytics } from "firebase/analytics";
+import { getAuth } from "firebase/auth";
+// TODO: Add SDKs for Firebase products that you want to use
+// https://firebase.google.com/docs/web/setup#available-libraries
+
+// Your web app's Firebase configuration
+// For Firebase JS SDK v7.20.0 and later, measurementId is optional
+const firebaseConfig = {
+ apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
+ authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
+ projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
+ storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
+ appId: import.meta.env.VITE_FIREBASE_APP_ID,
+ measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
+};
+
+// Initialize Firebase
+const app = initializeApp(firebaseConfig);
+//const analytics = getAnalytics(app);
+const auth = getAuth(app);
+
+export { app, auth };
diff --git a/app/services/firebase_provider.tsx b/app/services/firebase_provider.tsx
new file mode 100644
index 0000000..a199a65
--- /dev/null
+++ b/app/services/firebase_provider.tsx
@@ -0,0 +1,66 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import type { User } from "firebase/auth";
+import { onAuthStateChanged, signOut } from "firebase/auth";
+
+import { auth } from "./firebase_app";
+
+type AuthContextValue = {
+ user: User | null;
+ loading: boolean;
+ logout: () => Promise;
+};
+
+const AuthContext = createContext(undefined);
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ // `user` = the currently signed-in Firebase user (or null if signed out)
+ const [user, setUser] = useState(null);
+
+ // `loading` = true until Firebase tells us what the auth state is
+ // (prevents “flash of logged-out UI”)
+ const [loading, setLoading] = useState(() => {
+ // If your app server-renders, `useEffect` won't run on the server.
+ // Avoid getting stuck in loading=true on the server render:
+ return typeof window === "undefined" ? false : true;
+ });
+
+ useEffect(() => {
+ // Subscribe once. Firebase calls this immediately with the current user
+ // (after it restores the session), and again whenever auth changes.
+ const unsubscribe = onAuthStateChanged(auth, (nextUser) => {
+ setUser(nextUser);
+ setLoading(false);
+ });
+
+ // Cleanup so we don't leak listeners during hot reload / route changes
+ return unsubscribe;
+ }, []);
+
+ // Memoize so consumers don't re-render unnecessarily
+ const value = useMemo(
+ () => ({
+ user,
+ loading,
+ logout: () => signOut(auth),
+ }),
+ [user, loading]
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useAuth() {
+ const ctx = useContext(AuthContext);
+ if (!ctx) {
+ throw new Error("useAuth must be used within an .");
+ }
+ return ctx;
+}
diff --git a/eslint.config.ts b/eslint.config.ts
index c4cdf61..0b5ed62 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -5,6 +5,11 @@ import pluginReact from "eslint-plugin-react";
import { defineConfig } from "eslint/config";
export default defineConfig([
+ // Ignore generated output
+ {
+ ignores: ["build/**"],
+ },
+
// Base configuration for all relevant files
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
diff --git a/package-lock.json b/package-lock.json
index 33c37bd..faca918 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,9 @@
"dependencies": {
"@react-router/node": "^7.7.1",
"@react-router/serve": "^7.7.1",
+ "firebase": "^12.9.0",
"isbot": "^5.1.27",
+ "lucide-react": "^0.552.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
@@ -1096,6 +1098,645 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@firebase/ai": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.8.0.tgz",
+ "integrity": "sha512-grWYGFPsSo+pt+6CYeKR0kWnUfoLLS3xgWPvNrhAS5EPxl6xWq7+HjDZqX24yLneETyl45AVgDsTbVgxeWeRfg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@firebase/app-types": "0.x"
+ }
+ },
+ "node_modules/@firebase/analytics": {
+ "version": "0.10.19",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz",
+ "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/installations": "0.6.19",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/analytics-compat": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz",
+ "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/analytics": "0.10.19",
+ "@firebase/analytics-types": "0.8.3",
+ "@firebase/component": "0.7.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/analytics-types": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz",
+ "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/app": {
+ "version": "0.14.8",
+ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.8.tgz",
+ "integrity": "sha512-WiE9uCGRLUnShdjb9iP20sA3ToWrBbNXr14/N5mow7Nls9dmKgfGaGX5cynLvrltxq2OrDLh1VDNaUgsnS/k/g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@firebase/app-check": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz",
+ "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/app-check-compat": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz",
+ "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check": "0.11.0",
+ "@firebase/app-check-types": "0.5.3",
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/app-check-interop-types": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz",
+ "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/app-check-types": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz",
+ "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/app-compat": {
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.8.tgz",
+ "integrity": "sha512-4De6SUZ36zozl9kh5rZSxKWULpgty27rMzZ6x+xkoo7+NWyhWyFdsdvhFsWhTw/9GGj0wXIcbTjwHYCUIUuHyg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app": "0.14.8",
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@firebase/app-types": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
+ "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/auth": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz",
+ "integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@react-native-async-storage/async-storage": "^2.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-async-storage/async-storage": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@firebase/auth-compat": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.2.tgz",
+ "integrity": "sha512-8UhCzF6pav9bw/eXA8Zy1QAKssPRYEYXaWagie1ewLTwHkXv6bKp/j6/IwzSYQP67sy/BMFXIFaCCsoXzFLr7A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/auth": "1.12.0",
+ "@firebase/auth-types": "0.13.0",
+ "@firebase/component": "0.7.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/auth-interop-types": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz",
+ "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/auth-types": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz",
+ "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/component": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz",
+ "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@firebase/data-connect": {
+ "version": "0.3.12",
+ "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.12.tgz",
+ "integrity": "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/auth-interop-types": "0.2.4",
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/database": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz",
+ "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/auth-interop-types": "0.2.4",
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "faye-websocket": "0.11.4",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@firebase/database-compat": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz",
+ "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/database": "1.1.0",
+ "@firebase/database-types": "1.0.16",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@firebase/database-types": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz",
+ "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-types": "0.9.3",
+ "@firebase/util": "1.13.0"
+ }
+ },
+ "node_modules/@firebase/firestore": {
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.11.0.tgz",
+ "integrity": "sha512-Zb88s8rssBd0J2Tt+NUXMPt2sf+Dq7meatKiJf5t9oto1kZ8w9gK59Koe1uPVbaKfdgBp++N/z0I4G/HamyEhg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "@firebase/webchannel-wrapper": "1.0.5",
+ "@grpc/grpc-js": "~1.9.0",
+ "@grpc/proto-loader": "^0.7.8",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/firestore-compat": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.5.tgz",
+ "integrity": "sha512-yVX1CkVvqBI4qbA56uZo42xFA4TNU0ICQ+9AFDvYq9U9Xu6iAx9lFDAk/tN+NGereQQXXCSnpISwc/oxsQqPLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/firestore": "4.11.0",
+ "@firebase/firestore-types": "3.0.3",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/firestore-types": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz",
+ "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/functions": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz",
+ "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/auth-interop-types": "0.2.4",
+ "@firebase/component": "0.7.0",
+ "@firebase/messaging-interop-types": "0.2.3",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/functions-compat": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz",
+ "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/functions": "0.13.1",
+ "@firebase/functions-types": "0.6.3",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/functions-types": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz",
+ "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/installations": {
+ "version": "0.6.19",
+ "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz",
+ "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/util": "1.13.0",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/installations-compat": {
+ "version": "0.2.19",
+ "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz",
+ "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/installations": "0.6.19",
+ "@firebase/installations-types": "0.5.3",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/installations-types": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz",
+ "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x"
+ }
+ },
+ "node_modules/@firebase/logger": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz",
+ "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@firebase/messaging": {
+ "version": "0.12.23",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz",
+ "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/installations": "0.6.19",
+ "@firebase/messaging-interop-types": "0.2.3",
+ "@firebase/util": "1.13.0",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/messaging-compat": {
+ "version": "0.2.23",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz",
+ "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/messaging": "0.12.23",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/messaging-interop-types": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz",
+ "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/performance": {
+ "version": "0.7.9",
+ "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz",
+ "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/installations": "0.6.19",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0",
+ "web-vitals": "^4.2.4"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/performance-compat": {
+ "version": "0.2.22",
+ "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz",
+ "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/performance": "0.7.9",
+ "@firebase/performance-types": "0.2.3",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/performance-types": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz",
+ "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/remote-config": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.0.tgz",
+ "integrity": "sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/installations": "0.6.19",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/remote-config-compat": {
+ "version": "0.2.21",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.21.tgz",
+ "integrity": "sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/remote-config": "0.8.0",
+ "@firebase/remote-config-types": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/remote-config-types": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz",
+ "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/storage": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz",
+ "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/storage-compat": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz",
+ "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/storage": "0.14.0",
+ "@firebase/storage-types": "0.8.3",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/storage-types": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz",
+ "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/util": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz",
+ "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@firebase/webchannel-wrapper": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz",
+ "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.9.15",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
+ "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/proto-loader": "^0.7.8",
+ "@types/node": ">=12.12.47"
+ },
+ "engines": {
+ "node": "^8.13.0 || >=10.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.7.15",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
+ "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1359,6 +2000,70 @@
"url": "https://opencollective.com/pkgr"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@react-router/dev": {
"version": "7.9.2",
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.2.tgz",
@@ -2101,7 +2806,6 @@
"version": "20.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
"integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -2464,7 +3168,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -2952,11 +3655,82 @@
"node": ">=18"
}
},
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -2969,7 +3743,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
"license": "MIT"
},
"node_modules/compressible": {
@@ -3575,7 +4348,6 @@
"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"
@@ -4009,6 +4781,18 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -4103,6 +4887,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/firebase": {
+ "version": "12.9.0",
+ "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.9.0.tgz",
+ "integrity": "sha512-CwwTYoqZg6KxygPOaaJqIc4aoLvo0RCRrXoln9GoxLE8QyAwTydBaSLGVlR4WPcuOgN3OEL0tJLT1H4IU/dv7w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/ai": "2.8.0",
+ "@firebase/analytics": "0.10.19",
+ "@firebase/analytics-compat": "0.2.25",
+ "@firebase/app": "0.14.8",
+ "@firebase/app-check": "0.11.0",
+ "@firebase/app-check-compat": "0.4.0",
+ "@firebase/app-compat": "0.5.8",
+ "@firebase/app-types": "0.9.3",
+ "@firebase/auth": "1.12.0",
+ "@firebase/auth-compat": "0.6.2",
+ "@firebase/data-connect": "0.3.12",
+ "@firebase/database": "1.1.0",
+ "@firebase/database-compat": "2.1.0",
+ "@firebase/firestore": "4.11.0",
+ "@firebase/firestore-compat": "0.4.5",
+ "@firebase/functions": "0.13.1",
+ "@firebase/functions-compat": "0.4.1",
+ "@firebase/installations": "0.6.19",
+ "@firebase/installations-compat": "0.2.19",
+ "@firebase/messaging": "0.12.23",
+ "@firebase/messaging-compat": "0.2.23",
+ "@firebase/performance": "0.7.9",
+ "@firebase/performance-compat": "0.2.22",
+ "@firebase/remote-config": "0.8.0",
+ "@firebase/remote-config-compat": "0.2.21",
+ "@firebase/storage": "0.14.0",
+ "@firebase/storage-compat": "0.4.0",
+ "@firebase/util": "1.13.0"
+ }
+ },
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@@ -4240,6 +5060,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -4561,6 +5390,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/http-parser-js": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
+ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
+ "license": "MIT"
+ },
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@@ -4589,6 +5424,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
+ "license": "ISC"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4821,7 +5662,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5503,6 +6343,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -5510,6 +6356,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -5533,6 +6385,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.552.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz",
+ "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
@@ -6286,6 +7147,30 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/protobufjs": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
+ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -6505,6 +7390,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -7418,6 +8312,12 @@
}
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -7583,7 +8483,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unpipe": {
@@ -7815,6 +8714,35 @@
}
}
},
+ "node_modules/web-vitals": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
+ "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/which": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
@@ -8025,6 +8953,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -8032,6 +8969,74 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 7466851..1d3cc94 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,9 @@
"dependencies": {
"@react-router/node": "^7.7.1",
"@react-router/serve": "^7.7.1",
+ "firebase": "^12.9.0",
"isbot": "^5.1.27",
+ "lucide-react": "^0.552.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
diff --git a/public/CancerLINC-Logo-1.png b/public/CancerLINC-Logo-1.png
new file mode 100644
index 0000000..f84f594
Binary files /dev/null and b/public/CancerLINC-Logo-1.png differ
diff --git a/vite.config.ts b/vite.config.ts
index f1bc5f2..b4aabfd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,4 +5,10 @@ import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
+ server: {
+ watch: {
+ usePolling: true,
+ interval: 100,
+ },
+ },
});