diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ab5f8d8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Nuxt build artifacts +.output +.nuxt +dist + +# Node modules +node_modules + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile +.dockerignore +docker-compose.yml + +# Testing +coverage +.nyc_output + +# Misc +*.md +!README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1fbd7ed --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Firebase Configuration +# Get these values from your Firebase Console -> Project Settings -> General -> Your apps +NUXT_PUBLIC_FIREBASE_API_KEY=your_firebase_api_key_here +NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_firebase_auth_domain_here +NUXT_PUBLIC_FIREBASE_PROJECT_ID=your_firebase_project_id_here +# Use the bucket ID (e.g. project-id.appspot.com), not the web URL. +NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_firebase_storage_bucket_here +NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id_here +NUXT_PUBLIC_FIREBASE_APP_ID=your_firebase_app_id_here +NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID=your_firebase_measurement_id_here + +# Stripe Configuration +NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx +STRIPE_SECRET_KEY=sk_test_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx + +# Backend Configuration +NUXT_PUBLIC_BACKEND_URL=http://localhost:3000 +NUXT_PUBLIC_STORAGE_URL=http://localhost:9000 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..76e27e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies with clean cache +RUN npm ci --prefer-offline --no-audit + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install only production dependencies with clean cache +RUN npm ci --prefer-offline --no-audit --omit=dev + +# Copy built application from builder stage +COPY --from=builder /app/.output ./.output + +# Expose port +EXPOSE 3000 + +# Set environment to production +ENV NODE_ENV=production + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start the application +CMD ["node", ".output/server/index.mjs"] diff --git a/app/components/AppNavbar.vue b/app/components/AppNavbar.vue new file mode 100644 index 0000000..66b0786 --- /dev/null +++ b/app/components/AppNavbar.vue @@ -0,0 +1,157 @@ + + + diff --git a/app/components/LoginModal.vue b/app/components/LoginModal.vue new file mode 100644 index 0000000..830bcc2 --- /dev/null +++ b/app/components/LoginModal.vue @@ -0,0 +1,122 @@ + + + diff --git a/app/components/designer/DesignerPreview.vue b/app/components/designer/DesignerPreview.vue index 4f614a1..bb7902b 100644 --- a/app/components/designer/DesignerPreview.vue +++ b/app/components/designer/DesignerPreview.vue @@ -1,117 +1,36 @@ diff --git a/app/components/designer/DesignerToolbar.vue b/app/components/designer/DesignerToolbar.vue index 17843f0..ba39e76 100644 --- a/app/components/designer/DesignerToolbar.vue +++ b/app/components/designer/DesignerToolbar.vue @@ -9,8 +9,10 @@ const props = defineProps<{ onImportImage: (file: File) => Promise; onFillChange: (fill: string) => void; onStrokeChange: (stroke: string) => void; + onBackgroundChange: (background: string) => void; activeFill: string | null; activeStroke: string | null; + activeBackground: string; canStyleSelection: boolean; zoom: number; minZoom: number; @@ -28,6 +30,7 @@ const emit = defineEmits<{ const fileInput = ref(null); const fillValue = ref(props.activeFill ?? "#111827"); const strokeValue = ref(props.activeStroke ?? "#3b82f6"); +const backgroundValue = ref(props.activeBackground ?? "#ffffff"); const zoomSliderValue = ref(Math.round(props.zoom * 100)); watch( @@ -44,6 +47,13 @@ watch( } ); +watch( + () => props.activeBackground, + (next) => { + backgroundValue.value = next ?? "#ffffff"; + } +); + watch( () => props.zoom, (next) => { @@ -88,6 +98,13 @@ const handleStrokeChange = (event: Event) => { props.onStrokeChange(value); }; +const handleBackgroundChange = (event: Event) => { + const input = event.target as HTMLInputElement; + const value = input.value; + backgroundValue.value = value; + props.onBackgroundChange(value); +}; + const handleZoomInput = (event: Event) => { const input = event.target as HTMLInputElement; const value = Number(input.value); @@ -102,53 +119,117 @@ const zoomLabel = computed(() => `${Math.round(props.zoom * 100)}%`); diff --git a/app/components/designer/TemplatePicker.vue b/app/components/designer/TemplatePicker.vue index e4c777a..1c4a521 100644 --- a/app/components/designer/TemplatePicker.vue +++ b/app/components/designer/TemplatePicker.vue @@ -21,7 +21,7 @@ const handleSelect = (templateId: string) => {

Pick the vinyl size and print spec that matches this order.

-
+
-
+
Diameter
{{ template.diameterInches }}"
diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts new file mode 100644 index 0000000..bea0128 --- /dev/null +++ b/app/composables/useAuth.ts @@ -0,0 +1,290 @@ +import { + signInWithEmailAndPassword, + signInWithPopup, + GoogleAuthProvider, + signOut as firebaseSignOut, + onAuthStateChanged, + getAuth, + createUserWithEmailAndPassword, +} from "firebase/auth"; +import { getApp, getApps, initializeApp } from "firebase/app"; +import type { User } from "firebase/auth"; + +export const useAuth = () => { + const user = useState("auth-user", () => null); + const isLoading = useState("auth-loading", () => true); + const error = useState("auth-error", () => null); + const firebaseReady = useState("firebase-ready", () => false); + const listenerRegistered = useState( + "auth-listener-registered", + () => false + ); + const backendUser = useState | null>( + "auth-backend-user", + () => null + ); + + const config = useRuntimeConfig(); + + const ensureFirebaseApp = () => { + if (!process.client) { + return null; + } + + if (!firebaseReady.value) { + const firebaseConfig = { + apiKey: config.public.firebaseApiKey, + authDomain: config.public.firebaseAuthDomain, + projectId: config.public.firebaseProjectId, + storageBucket: config.public.firebaseStorageBucket, + messagingSenderId: config.public.firebaseMessagingSenderId, + appId: config.public.firebaseAppId, + ...(config.public.firebaseMeasurementId + ? { measurementId: config.public.firebaseMeasurementId } + : {}), + }; + + if (getApps().length === 0) { + initializeApp(firebaseConfig); + } + + firebaseReady.value = true; + } + + try { + return getApp(); + } catch (err) { + console.error("[useAuth] Failed to get Firebase app instance:", err); + return null; + } + }; + + const getAuthInstance = () => { + const app = ensureFirebaseApp(); + return app ? getAuth(app) : null; + }; + + const authenticateWithBackend = async (idToken: string) => { + try { + const response = await $fetch<{ + token?: string; + user?: Record | null; + }>("/auth/login", { + baseURL: config.public.backendUrl, + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { + idToken, + }, + }); + + backendUser.value = response?.user ?? null; + return response; + } catch (err) { + console.error("Backend authentication failed:", err); + backendUser.value = null; + throw err; + } + }; + + const syncBackendWithToken = async (tokenProvider: () => Promise) => { + try { + const idToken = await tokenProvider(); + await authenticateWithBackend(idToken); + } catch (err) { + console.warn("[useAuth] Failed to sync backend session", err); + } + }; + + const registerListener = () => { + if (!process.client || listenerRegistered.value) { + return; + } + + try { + const auth = getAuthInstance(); + if (auth) { + const existingUser = auth.currentUser; + if (existingUser && !user.value) { + user.value = existingUser; + isLoading.value = false; + syncBackendWithToken(() => existingUser.getIdToken()); + } + + listenerRegistered.value = true; + onAuthStateChanged(auth, (firebaseUser) => { + user.value = firebaseUser; + isLoading.value = false; + if (firebaseUser) { + syncBackendWithToken(() => firebaseUser.getIdToken()); + } else { + backendUser.value = null; + } + }); + } + } catch (err) { + console.error("[useAuth] Failed to initialize auth listener:", err); + isLoading.value = false; + } + }; + + const initAuth = () => { + registerListener(); + }; + + // Eagerly register listener when composable is used in client context + if (process.client) { + registerListener(); + } + + // Sign in with email and password + const signInWithEmail = async (email: string, password: string) => { + try { + error.value = null; + isLoading.value = true; + + const auth = getAuthInstance(); + if (!auth) { + throw new Error("Firebase not initialized"); + } + + const userCredential = await signInWithEmailAndPassword( + auth, + email, + password + ); + const idToken = await userCredential.user.getIdToken(); + + try { + await authenticateWithBackend(idToken); + } catch (backendErr) { + console.warn( + "[useAuth] Backend authentication failed after email login:", + backendErr + ); + } + + user.value = userCredential.user; + registerListener(); + + return userCredential.user; + } catch (err: any) { + error.value = err.message; + throw err; + } finally { + isLoading.value = false; + } + }; + + // Sign in with Google + const signInWithGoogle = async () => { + try { + error.value = null; + isLoading.value = true; + + const auth = getAuthInstance(); + if (!auth) { + throw new Error("Firebase not initialized"); + } + + const provider = new GoogleAuthProvider(); + const userCredential = await signInWithPopup(auth, provider); + const idToken = await userCredential.user.getIdToken(); + + try { + await authenticateWithBackend(idToken); + } catch (backendErr) { + console.warn( + "[useAuth] Backend authentication failed after Google login:", + backendErr + ); + } + + user.value = userCredential.user; + registerListener(); + + return userCredential.user; + } catch (err: any) { + error.value = err.message; + throw err; + } finally { + isLoading.value = false; + } + }; + + const registerWithEmail = async (email: string, password: string) => { + try { + error.value = null; + isLoading.value = true; + + const auth = getAuthInstance(); + if (!auth) { + throw new Error("Firebase not initialized"); + } + + const userCredential = await createUserWithEmailAndPassword( + auth, + email, + password + ); + const idToken = await userCredential.user.getIdToken(); + + try { + await authenticateWithBackend(idToken); + } catch (backendErr) { + console.warn( + "[useAuth] Backend authentication failed after registration:", + backendErr + ); + } + + user.value = userCredential.user; + registerListener(); + + return userCredential.user; + } catch (err: any) { + error.value = err.message; + throw err; + } finally { + isLoading.value = false; + } + }; + + // Sign out + const signOut = async () => { + try { + const auth = getAuthInstance(); + if (!auth) { + throw new Error("Firebase not initialized"); + } + + await firebaseSignOut(auth); + user.value = null; + backendUser.value = null; + } catch (err: any) { + error.value = err.message; + throw err; + } + }; + + // Get current user's ID token + const getIdToken = async () => { + if (!user.value) return null; + return await user.value.getIdToken(); + }; + + return { + user: readonly(user), + isLoading: readonly(isLoading), + error: readonly(error), + backendUser: readonly(backendUser), + initAuth, + signInWithEmail, + signInWithGoogle, + registerWithEmail, + signOut, + getIdToken, + }; +}; diff --git a/app/composables/useFirebaseStorage.ts b/app/composables/useFirebaseStorage.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/app/composables/useFirebaseStorage.ts @@ -0,0 +1 @@ +export {}; diff --git a/app/composables/useLoginModal.ts b/app/composables/useLoginModal.ts new file mode 100644 index 0000000..20f5db9 --- /dev/null +++ b/app/composables/useLoginModal.ts @@ -0,0 +1 @@ +export const useLoginModal = () => useState('login-modal-open', () => false); diff --git a/app/pages/checkout/success.vue b/app/pages/checkout/success.vue new file mode 100644 index 0000000..c406d4f --- /dev/null +++ b/app/pages/checkout/success.vue @@ -0,0 +1,235 @@ + + + diff --git a/app/pages/designer.vue b/app/pages/designer.vue new file mode 100644 index 0000000..ace1139 --- /dev/null +++ b/app/pages/designer.vue @@ -0,0 +1,431 @@ + + + diff --git a/app/pages/index.vue b/app/pages/index.vue index 55316b5..ffd5091 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,124 +1,130 @@ + + diff --git a/app/pages/orders.vue b/app/pages/orders.vue new file mode 100644 index 0000000..3493e2d --- /dev/null +++ b/app/pages/orders.vue @@ -0,0 +1,227 @@ + + + diff --git a/app/pages/profile.vue b/app/pages/profile.vue new file mode 100644 index 0000000..8012c3d --- /dev/null +++ b/app/pages/profile.vue @@ -0,0 +1,156 @@ + + + diff --git a/app/pages/register.vue b/app/pages/register.vue new file mode 100644 index 0000000..3dd7484 --- /dev/null +++ b/app/pages/register.vue @@ -0,0 +1,186 @@ + + + diff --git a/composables/useSlipmatDesigner.ts b/composables/useSlipmatDesigner.ts index f978400..a7ac70f 100644 --- a/composables/useSlipmatDesigner.ts +++ b/composables/useSlipmatDesigner.ts @@ -13,15 +13,6 @@ export interface SlipmatTemplate { backgroundColor: string; } -export interface ExportedDesign { - previewUrl: string; - previewBlob: Blob; - productionUrl: string; - productionBlob: Blob; - templateId: string; - createdAt: string; -} - const DISPLAY_SIZE = 720; const PREVIEW_SIZE = 1024; const MIN_ZOOM = 0.5; @@ -59,6 +50,7 @@ const TEMPLATE_PRESETS: SlipmatTemplate[] = [ ]; type FabricCanvas = FabricNamespace.Canvas; +type FabricSerializedCanvas = ReturnType; type FabricCircle = FabricNamespace.Circle; type FabricRect = FabricNamespace.Rect; type FabricTextbox = FabricNamespace.Textbox; @@ -69,6 +61,16 @@ type CanvasReadyPayload = { fabric: FabricModule; }; +export interface ExportedDesign { + previewUrl: string; + previewBlob: Blob; + productionUrl: string; + productionBlob: Blob; + templateId: string; + createdAt: string; + canvasJson: FabricSerializedCanvas; +} + const FALLBACK_TEMPLATE: SlipmatTemplate = TEMPLATE_PRESETS[0] ?? { id: "custom", name: "Custom", @@ -320,8 +322,6 @@ export const useSlipmatDesigner = () => { instance.on(eventName, handleMutation); }); - instance.on("after:render", () => schedulePreviewRefresh()); - const selectionEvents = [ "selection:created", "selection:updated", @@ -605,6 +605,77 @@ export const useSlipmatDesigner = () => { } }; + const waitForCanvasReady = async (): Promise => { + if (canvas.value) { + return canvas.value; + } + + await new Promise((resolve) => { + const stop = watch( + canvas, + (value) => { + if (value) { + stop(); + resolve(); + } + }, + { immediate: false } + ); + }); + + if (!canvas.value) { + throw new Error("Canvas not ready"); + } + + return canvas.value; + }; + + const loadDesign = async (payload: { + templateId?: string | null; + canvasJson: string | FabricSerializedCanvas; + previewUrl?: string | null; + }) => { + if (payload.templateId) { + selectTemplate(payload.templateId); + } + + const currentCanvas = await waitForCanvasReady(); + if (!fabricApi.value) { + throw new Error("Fabric API not ready"); + } + + const parsedJson = + typeof payload.canvasJson === "string" + ? (JSON.parse(payload.canvasJson) as FabricSerializedCanvas) + : payload.canvasJson; + + await new Promise((resolve, reject) => { + currentCanvas.loadFromJSON(parsedJson, () => { + maintainStaticLayerOrder(); + updateSelectedStyleState(); + currentCanvas.renderAll(); + schedulePreviewRefresh(); + resolve(); + }); + }).catch((error) => { + throw error; + }); + + // Reset cached assets; caller can provide preview if available. + previewBlob.value = null; + productionBlob.value = null; + if (productionObjectUrl.value) { + URL.revokeObjectURL(productionObjectUrl.value); + productionObjectUrl.value = null; + } + + if (payload.previewUrl) { + previewUrl.value = payload.previewUrl; + } else { + await refreshPreview(); + } + }; + const exportDesign = async (): Promise => { const currentCanvas = canvas.value; if (!currentCanvas) { @@ -629,6 +700,8 @@ export const useSlipmatDesigner = () => { }); const productionDataBlob = await dataUrlToBlob(productionDataUrl); + const canvasJson = currentCanvas.toJSON(); + previewUrl.value = previewDataUrl; previewBlob.value = previewDataBlob; productionBlob.value = productionDataBlob; @@ -645,6 +718,7 @@ export const useSlipmatDesigner = () => { productionBlob: productionDataBlob, templateId: selectedTemplate.value.id, createdAt: new Date().toISOString(), + canvasJson, }; } finally { isExporting.value = false; @@ -677,6 +751,20 @@ export const useSlipmatDesigner = () => { ); }; + const setBackgroundColor = (color: string) => { + const bgCircle = backgroundCircle.value; + if (!bgCircle || !canvas.value) { + return; + } + bgCircle.set({ fill: color }); + selectedTemplate.value = { + ...selectedTemplate.value, + backgroundColor: color, + }; + canvas.value.requestRenderAll(); + schedulePreviewRefresh(); + }; + watch(selectedTemplate, () => { resetZoom(); applyTemplateToCanvas(); @@ -689,6 +777,7 @@ export const useSlipmatDesigner = () => { templates, selectedTemplate, selectTemplate, + loadDesign, displaySize, productionPixelSize, templateLabel, @@ -697,9 +786,9 @@ export const useSlipmatDesigner = () => { productionBlob, productionObjectUrl, isExporting, - activeFillColor, - activeStrokeColor, - canStyleSelection, + activeFillColor, + activeStrokeColor, + canStyleSelection, zoomLevel, zoomPercent, minZoom: MIN_ZOOM, @@ -709,8 +798,9 @@ export const useSlipmatDesigner = () => { addTextbox, addShape, addImageFromFile, - setActiveFillColor, - setActiveStrokeColor, + setActiveFillColor, + setActiveStrokeColor, + setBackgroundColor, setZoom, zoomIn, zoomOut, diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0508e3f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + slipmatz-web: + build: + context: . + dockerfile: Dockerfile + container_name: slipmatz-web + ports: + - "3000:3000" + environment: + - NODE_ENV=production + # Add your environment variables here + - NUXT_PUBLIC_FIREBASE_API_KEY=${NUXT_PUBLIC_FIREBASE_API_KEY} + - NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN} + - NUXT_PUBLIC_FIREBASE_PROJECT_ID=${NUXT_PUBLIC_FIREBASE_PROJECT_ID} + - NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET} + - NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID} + - NUXT_PUBLIC_FIREBASE_APP_ID=${NUXT_PUBLIC_FIREBASE_APP_ID} + - NUXT_PUBLIC_STORAGE_URL=${NUXT_PUBLIC_STORAGE_URL} + - NUXT_PUBLIC_BACKEND_URL=${NUXT_PUBLIC_BACKEND_URL} + - NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} + restart: unless-stopped + networks: + - slipmatz-network + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + slipmatz-network: + driver: bridge diff --git a/nuxt.config.ts b/nuxt.config.ts index 900c32b..72fe6ae 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -5,7 +5,29 @@ export default defineNuxtConfig({ compatibilityDate: "2025-07-15", devtools: { enabled: true }, css: ["./app/assets/css/main.css"], + modules: ["@nuxtjs/color-mode"], vite: { plugins: [tailwindcss()], }, + runtimeConfig: { + stripeSecretKey: process.env.STRIPE_SECRET_KEY, + stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET, + public: { + firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY, + firebaseAuthDomain: process.env.NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + firebaseProjectId: process.env.NUXT_PUBLIC_FIREBASE_PROJECT_ID, + firebaseStorageBucket: process.env.NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + firebaseMessagingSenderId: process.env.NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + firebaseAppId: process.env.NUXT_PUBLIC_FIREBASE_APP_ID, + firebaseMeasurementId: process.env.NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID, + stripePublishableKey: process.env.NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, + backendUrl: process.env.NUXT_PUBLIC_BACKEND_URL || 'http://localhost:3000', + storageUrl: process.env.NUXT_PUBLIC_STORAGE_URL || 'http://localhost:9000', + } + }, + colorMode: { + preference: 'light', + fallback: 'light', + classSuffix: '' + } }); diff --git a/package-lock.json b/package-lock.json index d3dcb70..3349706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,12 @@ "name": "slipmatz-web", "hasInstallScript": true, "dependencies": { + "@nuxtjs/color-mode": "^3.5.2", "@tailwindcss/vite": "^4.1.16", "fabric": "^6.0.2", + "firebase": "^12.5.0", "nuxt": "^4.2.0", + "stripe": "^19.3.0", "tailwindcss": "^4.1.16", "vue": "^3.5.22", "vue-router": "^4.6.3" @@ -902,6 +905,645 @@ "node": ">=18" } }, + "node_modules/@firebase/ai": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.5.0.tgz", + "integrity": "sha512-OXv/jZLRjV9jTejWA4KOvW8gM1hNsLvQSCPwKhi2CEfe0Nap3rM6z+Ial0PGqXga0WgzhpypEvJOFvaAUFX3kg==", + "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.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.5.tgz", + "integrity": "sha512-zyNY77xJOGwcuB+xCxF8z8lSiHvD4ox7BCsqLEHEvgqQoRjxFZ0fkROR6NV5QyXmCqRLodMM8J5d2EStOocWIw==", + "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.5", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.5.tgz", + "integrity": "sha512-lVG/nRnXaot0rQSZazmTNqy83ti9O3+kdwoaE0d5wahRIWNoDirbIMcGVjDDgdmf4IE6FYreWOMh0L3DV1475w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.5", + "@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.11.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", + "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", + "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": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.1.tgz", + "integrity": "sha512-I0o2ZiZMnMTOQfqT22ur+zcGDVSAfdNZBHo26/Tfi8EllfR1BO7aTVo2rt/ts8o/FWsK8pOALLeVBGhZt8w/vg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.1", + "@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.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "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.9.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.2.tgz", + "integrity": "sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==", + "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.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.2.tgz", + "integrity": "sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.2", + "@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.7.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", + "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "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.20", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", + "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.7.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/@ioredis/commands": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", @@ -1474,6 +2116,96 @@ } } }, + "node_modules/@nuxtjs/color-mode": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/color-mode/-/color-mode-3.5.2.tgz", + "integrity": "sha512-cC6RfgZh3guHBMLLjrBB2Uti5eUoGM9KyauOaYS9ETmxNWBMTvpgjvSiSJp1OFljIXPIqVTJ3xtJpSNZiO3ZaA==", + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.13.2", + "pathe": "^1.1.2", + "pkg-types": "^1.2.1", + "semver": "^7.6.3" + } + }, + "node_modules/@nuxtjs/color-mode/node_modules/@nuxt/kit": { + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.20.1.tgz", + "integrity": "sha512-TIslaylfI5kd3AxX5qts0qyrIQ9Uq3HAA1bgIIJ+c+zpDfK338YS+YrCWxBBzDMECRCbAS58mqAd2MtJfG1ENA==", + "license": "MIT", + "dependencies": { + "c12": "^3.3.1", + "consola": "^3.4.2", + "defu": "^6.1.4", + "destr": "^2.0.5", + "errx": "^0.1.0", + "exsolve": "^1.0.7", + "ignore": "^7.0.5", + "jiti": "^2.6.1", + "klona": "^2.0.6", + "knitwork": "^1.2.0", + "mlly": "^1.8.0", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "rc9": "^2.1.2", + "scule": "^1.3.0", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ufo": "^1.6.1", + "unctx": "^2.4.1", + "untyped": "^2.0.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/@nuxtjs/color-mode/node_modules/@nuxt/kit/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/@nuxtjs/color-mode/node_modules/@nuxt/kit/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/@nuxtjs/color-mode/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "license": "MIT" + }, + "node_modules/@nuxtjs/color-mode/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/@nuxtjs/color-mode/node_modules/pkg-types/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/@nuxtjs/color-mode/node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/@oxc-minify/binding-android-arm64": { "version": "0.96.0", "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm64/-/binding-android-arm64-0.96.0.tgz", @@ -2587,6 +3319,70 @@ "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", "license": "MIT" }, + "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/@rolldown/pluginutils": { "version": "1.0.0-beta.29", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", @@ -3366,6 +4162,15 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/parse-path": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", @@ -4444,7 +5249,6 @@ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", - "optional": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -4453,6 +5257,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -5394,7 +6214,6 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -5488,7 +6307,6 @@ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -5498,7 +6316,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -5514,7 +6331,6 @@ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", - "optional": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -5798,6 +6614,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", @@ -5833,6 +6661,42 @@ "node": ">=8" } }, + "node_modules/firebase": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.5.0.tgz", + "integrity": "sha512-Ak8JcpH7FL6kiv0STwkv5+3CYEROO9iFWSx7OCZVvc4kIIABAIyAGs1mPGaHRxGUIApFZdMCXA7baq17uS6Mow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.5.0", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/app": "0.14.5", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.5", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@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.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -6017,7 +6881,6 @@ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -6048,7 +6911,6 @@ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", - "optional": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -6177,7 +7039,6 @@ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6234,7 +7095,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6321,6 +7181,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/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -6387,6 +7253,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/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7206,6 +8078,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "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.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -7230,6 +8108,12 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "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/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7331,7 +8215,6 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -8031,6 +8914,18 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ofetch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", @@ -8934,6 +9829,30 @@ "node": ">= 6" } }, + "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/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -8963,6 +9882,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -9552,6 +10486,78 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9819,6 +10825,26 @@ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "license": "MIT" }, + "node_modules/stripe": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.3.0.tgz", + "integrity": "sha512-3MbqRkw5LXb4LWP1LgIEYxUAYhYDDU5pcHZj4Xha6VWPnN1wrUmQ7Htsgm8wR584s0hn1aQg1lYD0Hi+F37E5g==", + "license": "MIT", + "dependencies": { + "qs": "^6.11.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@types/node": ">=16" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/structured-clone-es": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/structured-clone-es/-/structured-clone-es-1.0.0.tgz", @@ -10125,8 +11151,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-fest": { "version": "5.2.0", @@ -10959,6 +11984,12 @@ "node": ">=14" } }, + "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/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10975,6 +12006,29 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "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/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", diff --git a/package.json b/package.json index d15f903..6214e47 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,12 @@ "postinstall": "nuxt prepare" }, "dependencies": { + "@nuxtjs/color-mode": "^3.5.2", "@tailwindcss/vite": "^4.1.16", "fabric": "^6.0.2", + "firebase": "^12.5.0", "nuxt": "^4.2.0", + "stripe": "^19.3.0", "tailwindcss": "^4.1.16", "vue": "^3.5.22", "vue-router": "^4.6.3" diff --git a/server/api/checkout.session.post.ts b/server/api/checkout.session.post.ts new file mode 100644 index 0000000..29328ff --- /dev/null +++ b/server/api/checkout.session.post.ts @@ -0,0 +1,74 @@ +import Stripe from "stripe"; + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const stripeSecretKey = config.stripeSecretKey; + + if (!stripeSecretKey) { + throw createError({ + statusCode: 500, + statusMessage: "Stripe secret key not configured", + }); + } + + const stripe = new Stripe(stripeSecretKey, { + apiVersion: "2025-10-29.clover", + }); + + const body = await readBody<{ + designId: string; + templateId?: string; + designName?: string; + amount: number; + currency?: string; + successUrl: string; + cancelUrl: string; + customerEmail?: string; + }>(event); + + if ( + !body?.designId || + !body?.amount || + !body?.successUrl || + !body?.cancelUrl + ) { + throw createError({ + statusCode: 400, + statusMessage: "Missing required fields", + }); + } + + const { currency = "usd" } = body; + + const session = await stripe.checkout.sessions.create({ + mode: "payment", + payment_method_types: ["card"], + billing_address_collection: "auto", + customer_email: body.customerEmail, + shipping_address_collection: { allowed_countries: ["US", "CA"] }, + line_items: [ + { + quantity: 1, + price_data: { + currency, + unit_amount: Math.round(body.amount * 100), + product_data: { + name: body.designName ?? `Slipmat design ${body.designId}`, + metadata: { + designId: body.designId, + ...(body.templateId ? { templateId: body.templateId } : {}), + }, + }, + }, + }, + ], + metadata: { + designId: body.designId, + ...(body.templateId ? { templateId: body.templateId } : {}), + }, + success_url: body.successUrl, + cancel_url: body.cancelUrl, + }); + + return { id: session.id, url: session.url }; +}); diff --git a/server/api/checkout/[sessionId].get.ts b/server/api/checkout/[sessionId].get.ts new file mode 100644 index 0000000..21f6029 --- /dev/null +++ b/server/api/checkout/[sessionId].get.ts @@ -0,0 +1,62 @@ +import Stripe from 'stripe' + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const stripeSecretKey = config.stripeSecretKey + + if (!stripeSecretKey) { + throw createError({ statusCode: 500, statusMessage: 'Stripe secret key not configured' }) + } + + const params = event.context.params as { sessionId?: string } + const sessionId = params?.sessionId + + if (!sessionId) { + throw createError({ statusCode: 400, statusMessage: 'Missing session id' }) + } + + const stripe = new Stripe(stripeSecretKey, { + apiVersion: '2025-10-29.clover', + }) + + try { + const session = await stripe.checkout.sessions.retrieve(sessionId, { + expand: ['payment_intent', 'customer'], + }) + + const customerDetails = session.customer_details ?? null + + return { + id: session.id, + paymentStatus: session.payment_status, + amountTotal: session.amount_total, + currency: session.currency, + customerEmail: customerDetails?.email ?? session.customer_email ?? null, + customerName: customerDetails?.name ?? null, + createdAt: session.created ? new Date(session.created * 1000).toISOString() : null, + metadata: session.metadata ?? {}, + customerDetails: customerDetails + ? { + name: customerDetails.name ?? null, + email: customerDetails.email ?? null, + phone: customerDetails.phone ?? null, + address: customerDetails.address + ? { + line1: customerDetails.address.line1 ?? null, + line2: customerDetails.address.line2 ?? null, + city: customerDetails.address.city ?? null, + state: customerDetails.address.state ?? null, + postalCode: customerDetails.address.postal_code ?? null, + country: customerDetails.address.country ?? null, + } + : null, + } + : null, + } + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode ?? 500, + statusMessage: error?.message ?? 'Unable to retrieve checkout session', + }) + } +}) diff --git a/server/api/designs.post.ts b/server/api/designs.post.ts new file mode 100644 index 0000000..5b688f3 --- /dev/null +++ b/server/api/designs.post.ts @@ -0,0 +1,60 @@ +export default defineEventHandler(async (event) => { + const body = await readBody<{ + designId: string; + templateId: string; + ownerEmail?: string | null; + ownerId?: string | null; + previewUrl?: string | null; + productionUrl?: string | null; + canvasJson: unknown; + metadata?: Record; + }>(event); + + if (!body?.designId || !body?.templateId || body.canvasJson === undefined) { + throw createError({ + statusCode: 400, + statusMessage: "Missing required design fields", + }); + } + + const config = useRuntimeConfig(); + const backendUrl = config.public?.backendUrl; + + if (!backendUrl) { + throw createError({ + statusCode: 500, + statusMessage: "Backend URL not configured", + }); + } + + const payload = { + designId: body.designId, + templateId: body.templateId, + ownerEmail: body.ownerEmail ?? null, + ownerId: body.ownerId ?? null, + previewUrl: body.previewUrl ?? null, + productionUrl: body.productionUrl ?? null, + canvasJson: body.canvasJson, + metadata: body.metadata ?? {}, + updatedAt: new Date().toISOString(), + }; + + try { + const result = await $fetch("/designs", { + baseURL: backendUrl, + method: "POST", + body: payload, + }); + + return { + ok: true, + result, + }; + } catch (err) { + console.error("[designs] Failed to forward design payload", err); + throw createError({ + statusCode: 502, + statusMessage: (err as Error)?.message ?? "Failed to persist design", + }); + } +}); diff --git a/server/api/designs/[designId].get.ts b/server/api/designs/[designId].get.ts new file mode 100644 index 0000000..99e8713 --- /dev/null +++ b/server/api/designs/[designId].get.ts @@ -0,0 +1,30 @@ +export default defineEventHandler(async (event) => { + const params = event.context.params as { designId?: string } + const designId = params?.designId + + if (!designId) { + throw createError({ statusCode: 400, statusMessage: "Missing design id" }) + } + + const config = useRuntimeConfig() + const backendUrl = config.public?.backendUrl + + if (!backendUrl) { + throw createError({ statusCode: 500, statusMessage: "Backend URL not configured" }) + } + + try { + const design = await $fetch(`/designs/${encodeURIComponent(designId)}`, { + baseURL: backendUrl, + method: "GET", + }) + + return design + } catch (err) { + console.error(`[designs] Failed to fetch design ${designId}`, err) + throw createError({ + statusCode: 502, + statusMessage: (err as Error)?.message ?? "Failed to load design", + }) + } +}) diff --git a/server/api/orders.get.ts b/server/api/orders.get.ts new file mode 100644 index 0000000..8ab97b6 --- /dev/null +++ b/server/api/orders.get.ts @@ -0,0 +1,43 @@ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const backendUrl = config.public?.backendUrl; + + if (!backendUrl) { + throw createError({ statusCode: 500, statusMessage: "Backend URL not configured" }); + } + + const query = getQuery(event); + const customerEmail = typeof query.customerEmail === "string" ? query.customerEmail : null; + + if (!customerEmail) { + throw createError({ statusCode: 400, statusMessage: "Missing customerEmail" }); + } + + try { + const result = await $fetch("/transactions", { + baseURL: backendUrl, + method: "GET", + query: { customerEmail }, + }); + + if (Array.isArray(result)) { + return result; + } + + if (result && Array.isArray((result as any).data)) { + return (result as any).data; + } + + if (result && Array.isArray((result as any).orders)) { + return (result as any).orders; + } + + return result; + } catch (err) { + console.error("[orders] Failed to fetch order history", err); + throw createError({ + statusCode: 502, + statusMessage: (err as Error)?.message ?? "Failed to load order history", + }); + } +}); diff --git a/server/api/transactions.post.ts b/server/api/transactions.post.ts new file mode 100644 index 0000000..b771b5e --- /dev/null +++ b/server/api/transactions.post.ts @@ -0,0 +1,73 @@ +export default defineEventHandler(async (event) => { + const body = await readBody<{ + stripeSessionId: string; + designId: string; + templateId?: string; + amount: number | string; + currency: string; + customerEmail?: string; + customerDetails?: { + name?: string | null; + email?: string | null; + phone?: string | null; + address?: { + line1?: string | null; + line2?: string | null; + city?: string | null; + state?: string | null; + postalCode?: string | null; + country?: string | null; + } | null; + }; + }>(event); + + if (!body?.stripeSessionId || !body?.designId) { + throw createError({ + statusCode: 400, + statusMessage: "Missing required fields", + }); + } + + const config = useRuntimeConfig(); + const backendUrl = config.public?.backendUrl; + + if (!backendUrl) { + throw createError({ + statusCode: 500, + statusMessage: "Backend URL not configured", + }); + } + + const record = { + stripeSessionId: body.stripeSessionId, + designId: body.designId, + templateId: body.templateId ?? null, + amount: body.amount.toString(), + currency: body.currency, + customerEmail: body.customerEmail ?? null, + customerDetails: body.customerDetails ?? null, + }; + + console.log("[transactions] Forwarding record to backend:", record); + + try { + const backendResult = await $fetch("/transactions", { + baseURL: backendUrl, + method: "POST", + body: record, + }); + + return { + ok: true, + receivedAt: new Date().toISOString(), + backendResult, + }; + } catch (err) { + console.error("[transactions] Failed to forward to backend", err); + throw createError({ + statusCode: 502, + statusMessage: + (err as Error)?.message ?? "Failed to save transaction to backend", + }); + } +});