diff --git a/.env.example b/.env.example
index f7fa990..4dfd12b 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,23 @@
-COMPANY_NAME="Vercel Inc."
-SITE_NAME="Next.js Commerce"
+# Note: Shopify is the brand here, but you can use any other ecommerce provider
+COMPANY_NAME="Tarazoo"
+SITE_NAME="Tarazoo Commerce"
SHOPIFY_REVALIDATION_SECRET=""
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
-SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
+SHOPIFY_STORE_DOMAIN=""
+
+# Supabase Configuration
+NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
+NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"
+SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
+
+# Cohere (Vision) API Key
+COHERE_API_KEY=""
+NEXT_PUBLIC_MERCHANT_ID_DEFAULT="a1b2c3d4-e5f6-7890-abcd-ef1234567890"
+
+# MINLP Service Configuration
+MINLP_BASE_URL="https://your-minlp-service.vercel.app"
+
+# Feature Flags
+DEMO_MODE="true"
+ENABLE_PWA="true"
+ENABLE_EXPLAIN="true"
diff --git a/README_DEMO.md b/README_DEMO.md
new file mode 100644
index 0000000..fa662d8
--- /dev/null
+++ b/README_DEMO.md
@@ -0,0 +1,176 @@
+# Tarazoo - Unified Commerce Platform Demo
+
+## π Quick Start (36-Hour Hackathon Build)
+
+### Overview
+Tarazoo is a unified commerce platform combining:
+- **Mobile Shopper App**: Barcode scanning, cart with 13% tax, checkout
+- **Merchant Dashboard**: Live orders, sales metrics, MINLP optimization
+- **Tech Stack**: Next.js, Supabase, FastAPI, Vercel
+
+## π± Demo Features
+
+### Shopper Experience (Mobile-First)
+- **Camera Scanner**: Tap camera icon β scan product barcodes β auto-add to cart
+- **Smart Cart**: Shows subtotal + 13% tax calculation
+- **Quick Checkout**: Creates orders in Supabase with status tracking
+- **PWA Support**: Install as mobile app for native-like experience
+
+### Merchant Dashboard
+- **Live Orders Feed**: Real-time order updates via Supabase
+- **Sales Metrics**: Total revenue, average order value, today's orders
+- **MINLP Optimization**: One-click supply chain optimization
+- **Explanation Engine**: 3 bullets + TL;DR for optimization results
+
+## π Setup Instructions
+
+### 1. Supabase Setup
+```bash
+# Create a new Supabase project at https://supabase.com
+# Run migrations in Supabase SQL editor:
+# - /supabase/migrations/001_initial_schema.sql
+# - /supabase/migrations/002_seed_data.sql
+```
+
+### 2. Environment Variables
+```bash
+cp .env.example .env.local
+# Add your Supabase credentials:
+# NEXT_PUBLIC_SUPABASE_URL=your-project-url
+# NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
+```
+
+### 3. Install & Run
+```bash
+# Install dependencies
+pnpm install
+
+# Run the Next.js app
+pnpm dev
+
+# In another terminal, run the MINLP service
+cd services/minlp
+pip install -r requirements.txt
+python main.py
+```
+
+## π Demo Script (3-4 minutes)
+
+### Act 1: Shopper Journey
+1. Open app on mobile (http://localhost:3000)
+2. Tap camera icon in navbar
+3. Scan barcode: `1234567890123` (Coffee)
+4. Product auto-adds to cart with notification
+5. Scan another: `2345678901234` (Chocolate)
+6. Open cart β see subtotal + 13% tax
+7. Checkout β order confirmed
+
+### Act 2: Merchant Dashboard
+1. Navigate to /dashboard
+2. See new order appear instantly (realtime)
+3. Review sales metrics cards
+4. Click "Run MINLP Optimization"
+5. View optimization results (cost, suppliers, lead time)
+6. Click "Get Explanation" β see 3 bullets + TL;DR
+
+### Act 3: Wrap Up
+- "Unified UI for shoppers and merchants"
+- "Realtime Supabase for instant updates"
+- "FastAPI MINLP for supply chain optimization"
+- "Deployed on Vercel with edge functions"
+
+## π§ͺ Test Barcodes
+
+| Product | Barcode | Price |
+|---------|---------|-------|
+| Organic Coffee Beans | 1234567890123 | $24.99 |
+| Premium Dark Chocolate | 2345678901234 | $8.99 |
+| Artisan Sourdough Bread | 3456789012345 | $5.99 |
+| Organic Almond Butter | 4567890123456 | $12.99 |
+| Free Range Eggs | 5678901234567 | $7.99 |
+| Greek Yogurt | 6789012345678 | $6.99 |
+| Raw Honey | 7890123456789 | $14.99 |
+| Extra Virgin Olive Oil | 8901234567890 | $18.99 |
+
+## π’ Deployment
+
+### Vercel Deployment
+```bash
+# Deploy Next.js app
+vercel
+
+# Deploy MINLP as Vercel Function
+# Create /api/minlp endpoint that proxies to FastAPI
+```
+
+### Environment Variables (Production)
+- Add all env vars from .env.local to Vercel dashboard
+- Enable Supabase Realtime in project settings
+- Set MINLP_BASE_URL to deployed function URL
+
+## π SLOs & Performance
+
+- **ScanβCart**: β€ 2s P50
+- **CheckoutβOrder**: β€ 5s P95
+- **MINLP Run**: β€ 10s
+- **Explain**: β€ 3s
+
+## π― Key Differentiators
+
+1. **One Codebase**: Shopper + Merchant in same Next.js app
+2. **Mobile-First**: Camera scanner with torch + fallback
+3. **Realtime**: Orders appear instantly via Supabase
+4. **Smart MINLP**: Optimizes supply chain with explanations
+5. **PWA Ready**: Installable with offline support
+
+## π Troubleshooting
+
+### Camera Not Working?
+- Check browser permissions for camera access
+- Use manual barcode input as fallback
+- Ensure HTTPS in production (required for getUserMedia)
+
+### Orders Not Appearing?
+- Verify Supabase Realtime is enabled
+- Check RLS policies allow INSERT/SELECT
+- Confirm merchant_id matches in dashboard
+
+### MINLP Service Issues?
+- Ensure FastAPI is running on port 8000
+- Check CORS is enabled in main.py
+- Verify MINLP_BASE_URL in .env.local
+
+## π Architecture Notes
+
+```
+/apps/web (Next.js)
+ βββ app/
+ β βββ cart/ (with tax calculation)
+ β βββ checkout/ (Supabase order creation)
+ β βββ dashboard/ (merchant portal)
+ βββ components/
+ β βββ camera-scanner.tsx (@zxing/library)
+ β βββ cart/supabase-cart-context.tsx
+ βββ lib/
+ βββ supabase.ts (client + operations)
+
+/services/minlp (FastAPI)
+ βββ main.py (optimization + explanation)
+
+/supabase/migrations/
+ βββ 001_initial_schema.sql
+ βββ 002_seed_data.sql
+```
+
+## π Hackathon Impact
+
+**Why This Wins:**
+- **Visual Demo**: Barcode scanning is impressive live
+- **Business Value**: Real supply chain optimization
+- **Technical Depth**: Realtime, PWA, MINLP integration
+- **Polish**: Unified UI, instant feedback, explanations
+- **Scalable**: Ready for production with Vercel + Supabase
+
+---
+
+Built with β€οΈ for the 36-hour hackathon challenge
diff --git a/app/api/minlp/explain/route.ts b/app/api/minlp/explain/route.ts
new file mode 100644
index 0000000..12de7d8
--- /dev/null
+++ b/app/api/minlp/explain/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000';
+
+export async function POST(req: NextRequest) {
+
+ try {
+ const body = await req.json();
+ const { solution } = body;
+
+ // Call MINLP service for explanation
+ const explainResponse = await fetch(`${MINLP_BASE_URL}/explain`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ solution })
+ });
+
+ if (!explainResponse.ok) {
+ throw new Error('MINLP explain service failed');
+ }
+
+ const explanation = await explainResponse.json();
+
+ return NextResponse.json(explanation);
+ } catch (error) {
+ console.error('MINLP explain error:', error);
+ return NextResponse.json(
+ { error: 'Failed to generate explanation' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/minlp/solve/route.ts b/app/api/minlp/solve/route.ts
new file mode 100644
index 0000000..a749b6c
--- /dev/null
+++ b/app/api/minlp/solve/route.ts
@@ -0,0 +1,55 @@
+import { createMinlpRun } from 'lib/supabase';
+import { NextRequest, NextResponse } from 'next/server';
+
+const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000';
+
+export async function POST(request: NextRequest) {
+
+ try {
+ const body = await request.json();
+ const { merchantId, orders } = body;
+
+ // Prepare items from orders for MINLP
+ const items = orders.flatMap((order: any) =>
+ order.items || [{
+ sku: 'SKU001',
+ qty: 2,
+ price_cents: 2499
+ }]
+ );
+
+ // Call MINLP service
+ const minlpResponse = await fetch(`${MINLP_BASE_URL}/solve`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ merchant_id: merchantId,
+ order_id: orders[0]?.order_id,
+ items
+ })
+ });
+
+ if (!minlpResponse.ok) {
+ throw new Error('MINLP service failed');
+ }
+
+ const solution = await minlpResponse.json();
+
+ // Save to Supabase
+ const minlpRun = await createMinlpRun(
+ merchantId,
+ orders[0]?.order_id || null,
+ { items },
+ solution,
+ undefined
+ );
+
+ return NextResponse.json({ success: true, solution, runId: minlpRun?.run_id });
+ } catch (error) {
+ console.error('MINLP solve error:', error);
+ return NextResponse.json(
+ { error: 'Failed to run optimization' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/products/[handle]/route.ts b/app/api/products/[handle]/route.ts
new file mode 100644
index 0000000..2b9c58e
--- /dev/null
+++ b/app/api/products/[handle]/route.ts
@@ -0,0 +1,24 @@
+import { getProduct } from 'lib/shopify';
+import { NextRequest, NextResponse } from 'next/server';
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url);
+ const handle = searchParams.get('handle');
+
+ if (!handle) {
+ return NextResponse.json({ error: 'Handle parameter is required' }, { status: 400 });
+ }
+
+ try {
+ const product = await getProduct(handle, { fresh: true });
+
+ if (!product) {
+ return NextResponse.json({ error: 'Product not found' }, { status: 404 });
+ }
+
+ return NextResponse.json(product);
+ } catch (error) {
+ console.error('Error fetching product:', error);
+ return NextResponse.json({ error: 'Failed to fetch product' }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/sync-products/route.ts b/app/api/sync-products/route.ts
new file mode 100644
index 0000000..eb78c16
--- /dev/null
+++ b/app/api/sync-products/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { syncShopifyToSupabase } from 'lib/shopify-supabase-sync';
+
+export async function POST(request: NextRequest) {
+ try {
+ const result = await syncShopifyToSupabase();
+
+ if (result.success) {
+ return NextResponse.json({
+ success: true,
+ message: `Synced ${result.count} products from Shopify`,
+ products: result.products
+ });
+ } else {
+ return NextResponse.json(
+ { success: false, error: result.error },
+ { status: 500 }
+ );
+ }
+ } catch (error) {
+ console.error('Sync API error:', error);
+ return NextResponse.json(
+ { success: false, error: 'Sync failed' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function GET(request: NextRequest) {
+ // Trigger sync on GET for easy testing
+ return POST(request);
+}
diff --git a/app/api/vision-detect/route.ts b/app/api/vision-detect/route.ts
new file mode 100644
index 0000000..c2a58c8
--- /dev/null
+++ b/app/api/vision-detect/route.ts
@@ -0,0 +1,88 @@
+'use server';
+
+import { NextRequest, NextResponse } from 'next/server';
+
+// Allowed catalog labels. Must match exactly for downstream cart lookup.
+const ALLOWED_ITEMS = [
+ 'pen',
+ 'head cap',
+ 'redbull',
+ 'google cloud water bottle',
+ 'chips'
+];
+
+export async function POST(req: NextRequest) {
+ try {
+ const apiKey = process.env.COHERE_API_KEY;
+ if (!apiKey) {
+ return NextResponse.json({ error: 'Cohere API key not configured' }, { status: 500 });
+ }
+
+ const { image } = await req.json();
+ if (!image || typeof image !== 'string') {
+ return NextResponse.json({ error: 'Missing image' }, { status: 400 });
+ }
+
+ // Accept either a data URL (data:image/jpeg;base64,...) or raw base64
+ const match = image.match(/^data:(.*?);base64,(.*)$/);
+ const mime = match ? match[1] : 'image/jpeg';
+ const b64 = match ? match[2] : image;
+
+ // System prompt ensures exact label selection
+ const systemPrompt = `You are a retail vision assistant. Given an image of a product, choose exactly one label from this list and reply ONLY with that label in lowercase with no punctuation or extra words:\n\n- pen\n- head cap\n- redbull\n- google cloud water bottle\n- chips\n\nIf uncertain, pick the closest match.`;
+
+ // Call Cohere Chat API with vision model
+ const cohereRes = await fetch('https://api.cohere.ai/v1/chat', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: 'c4ai-aya-vision-32b',
+ message: 'Identify the product from the image and output only the exact label.',
+ preamble: systemPrompt,
+ temperature: 0,
+ // Attach the image as base64. Different versions of Cohere accept either
+ // `attachments` or `images`; we include attachments which is widely supported.
+ attachments: [
+ {
+ type: 'image',
+ data: b64,
+ mime_type: mime
+ }
+ ]
+ })
+ });
+
+ if (!cohereRes.ok) {
+ const errText = await cohereRes.text();
+ return NextResponse.json({ error: 'Cohere API error', details: errText }, { status: 502 });
+ }
+
+ const data = await cohereRes.json();
+ // Cohere chat responses typically include a `text` field or `message`/`content`.
+ let raw = (data.text || data.reply || data.response || '').toString().trim().toLowerCase();
+ if (!raw && data?.message?.content) {
+ raw = String(data.message.content).trim().toLowerCase();
+ }
+
+ // Normalize to allowed set
+ // Also allow minor variations (e.g., 'red bull' => 'redbull')
+ const normalized = raw
+ .replace(/\s+/g, ' ') // collapse spaces
+ .replace(/red bull/g, 'redbull')
+ .replace(/google cloud bottle/g, 'google cloud water bottle')
+ .trim();
+
+ const matchLabel = ALLOWED_ITEMS.find((x) => x === normalized);
+ if (!matchLabel) {
+ return NextResponse.json({ error: 'Unrecognized label', raw }, { status: 422 });
+ }
+
+ return NextResponse.json({ item: matchLabel });
+ } catch (err: any) {
+ return NextResponse.json({ error: 'Unexpected error', details: err?.message || String(err) }, { status: 500 });
+ }
+}
+
diff --git a/app/cart/page.tsx b/app/cart/page.tsx
new file mode 100644
index 0000000..f14c774
--- /dev/null
+++ b/app/cart/page.tsx
@@ -0,0 +1,152 @@
+'use client';
+
+import { useState } from 'react';
+import Link from 'next/link';
+import Image from 'next/image';
+import { useSupabaseCart } from 'components/cart/supabase-cart-context';
+import { formatPrice } from 'lib/utils';
+
+export default function CartPage() {
+ const { items, removeItem, updateQuantity, subtotal, tax, total, clearCart } = useSupabaseCart();
+
+ if (items.length === 0) {
+ return (
+
+
Shopping Cart
+
+
Your cart is empty
+
+ Continue Shopping
+
+
+
+ );
+ }
+
+ return (
+
+
Shopping Cart
+
+
+
+
+ Items in your shopping cart
+
+
+
+ {items.map((item) => (
+
+
+
+
+ {item.product.name.substring(0, 20)}
+
+
+
+
+
+
+
+
+
+
+ {item.product.name}
+
+
+
+
SKU: {item.product.sku}
+
+ {formatPrice(item.product.price_cents / 100)}
+
+
+
+
+
+ Quantity, {item.product.name}
+
+
updateQuantity(item.product.product_id, parseInt(e.target.value))}
+ className="max-w-full rounded-md border border-gray-300 py-1.5 text-left text-base font-medium leading-5 text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm"
+ >
+ {[...Array(10)].map((_, i) => (
+
+ {i + 1}
+
+ ))}
+
+
+
+
removeItem(item.product.product_id)}
+ className="-m-2 inline-flex p-2 text-gray-400 hover:text-gray-500"
+ >
+ Remove
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ {/* Order summary */}
+
+
+ Order summary
+
+
+
+
+
Subtotal
+ {formatPrice(subtotal / 100)}
+
+
+
+ Tax (13%)
+
+ {formatPrice(tax / 100)}
+
+
+
Order total
+ {formatPrice(total / 100)}
+
+
+
+
+
+ Checkout
+
+
+
+
+
+ or{' '}
+
+ Continue Shopping
+ →
+
+
+
+
+
+
+ );
+}
diff --git a/app/checkout/page.tsx b/app/checkout/page.tsx
new file mode 100644
index 0000000..57a10e7
--- /dev/null
+++ b/app/checkout/page.tsx
@@ -0,0 +1,256 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useSupabaseCart } from 'components/cart/supabase-cart-context';
+import { createOrder } from 'lib/supabase';
+import { formatPrice } from 'lib/utils';
+
+const MERCHANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; // Demo merchant
+
+export default function CheckoutPage() {
+ const router = useRouter();
+ const { items, subtotal, tax, total, clearCart } = useSupabaseCart();
+ const DISCOUNT_LABEL = 'Hack The North Developer Discount';
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleCheckout = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsProcessing(true);
+ setError(null);
+
+ try {
+ // Prepare order items
+ const orderItems = items.map(item => ({
+ sku: item.product.sku,
+ qty: item.quantity,
+ price_cents: item.product.price_cents
+ }));
+
+ // Create order in Supabase with full discount to zero out invoice
+ const order = await createOrder(
+ MERCHANT_ID,
+ orderItems,
+ subtotal,
+ tax,
+ 0, // total is 0 after full discount
+ DISCOUNT_LABEL,
+ total // discount equals the cart total (subtotal + tax)
+ );
+
+ if (order) {
+ // Clear the cart
+ clearCart();
+
+ // Redirect to success page
+ router.push(`/checkout/success?orderId=${order.order_id}`);
+ } else {
+ throw new Error('Failed to create order');
+ }
+ } catch (err) {
+ console.error('Checkout error:', err);
+ setError('Failed to process order. Please try again.');
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ if (items.length === 0) {
+ router.push('/cart');
+ return null;
+ }
+
+ return (
+
+
Checkout
+
+
+
+ Items in your cart
+
+
+
+
+ {/* Order summary */}
+
+
+ Order summary
+
+
+
+
+
Subtotal
+ {formatPrice(subtotal / 100)}
+
+
+
+ Tax (13%)
+
+ {formatPrice(tax / 100)}
+
+
+
{DISCOUNT_LABEL}
+ - {formatPrice(total / 100)}
+
+
+
Order total
+ {formatPrice(0)}
+
+
+
+
+
Items
+
+ {items.map((item) => (
+
+
+ {item.product.name} x {item.quantity}
+
+
+ {formatPrice((item.product.price_cents * item.quantity) / 100)}
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/app/checkout/success/page.tsx b/app/checkout/success/page.tsx
new file mode 100644
index 0000000..649ff26
--- /dev/null
+++ b/app/checkout/success/page.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+
+export default function CheckoutSuccessPage() {
+ const searchParams = useSearchParams();
+ const orderId = searchParams?.get('orderId');
+
+ return (
+
+
+
+
+
+ Order Confirmed!
+
+
+
+ Thank you for your order. Your order has been successfully placed and will be processed shortly.
+
+
+ {orderId && (
+
+
Order ID:
+
{orderId}
+
+ )}
+
+
+
+ Continue Shopping
+
+
+ View Dashboard
+
+
+
+
+ );
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
new file mode 100644
index 0000000..d1a9ad6
--- /dev/null
+++ b/app/dashboard/page.tsx
@@ -0,0 +1,315 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { getOrders, subscribeToOrders, subscribeToMinlpRuns, getLatestMinlpRun, createMinlpRun } from 'lib/supabase';
+import type { Order, MinlpRun } from 'packages/shared/types';
+import { formatPrice } from 'lib/utils';
+
+const MERCHANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+
+export default function DashboardPage() {
+ const [orders, setOrders] = useState([]);
+ const [latestMinlpRun, setLatestMinlpRun] = useState(null);
+ const [isRunningMinlp, setIsRunningMinlp] = useState(false);
+ const [showExplanation, setShowExplanation] = useState(false);
+
+ useEffect(() => {
+ // Load initial orders
+ loadOrders();
+
+ // Load latest MINLP run
+ loadLatestMinlpRun();
+
+ // Subscribe to realtime updates
+ const ordersChannel = subscribeToOrders(MERCHANT_ID, (payload) => {
+ if (payload.eventType === 'INSERT') {
+ setOrders(prev => [payload.new as Order, ...prev]);
+ }
+ });
+
+ const minlpChannel = subscribeToMinlpRuns(MERCHANT_ID, (payload) => {
+ if (payload.eventType === 'INSERT') {
+ setLatestMinlpRun(payload.new as MinlpRun);
+ }
+ });
+
+ return () => {
+ ordersChannel.unsubscribe();
+ minlpChannel.unsubscribe();
+ };
+ }, []);
+
+ const loadOrders = async () => {
+ const data = await getOrders(MERCHANT_ID);
+ setOrders(data);
+ };
+
+ const loadLatestMinlpRun = async () => {
+ const data = await getLatestMinlpRun(MERCHANT_ID);
+ setLatestMinlpRun(data);
+ };
+
+ const runMinlpOptimization = async () => {
+ setIsRunningMinlp(true);
+ try {
+ const response = await fetch('/api/minlp/solve', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ merchantId: MERCHANT_ID,
+ orders: orders.slice(0, 5) // Use latest 5 orders for optimization
+ })
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+ await loadLatestMinlpRun();
+ }
+ } catch (error) {
+ console.error('MINLP optimization failed:', error);
+ } finally {
+ setIsRunningMinlp(false);
+ }
+ };
+
+ const getExplanation = async () => {
+ if (!latestMinlpRun) return;
+
+ try {
+ const response = await fetch('/api/minlp/explain', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ solution: latestMinlpRun.solution_json
+ })
+ });
+
+ if (response.ok) {
+ const explanation = await response.json();
+ setLatestMinlpRun({
+ ...latestMinlpRun,
+ rationale_text: `${explanation.bullets.join(' β’ ')} | TL;DR: ${explanation.tldr}`
+ });
+ setShowExplanation(true);
+ }
+ } catch (error) {
+ console.error('Failed to get explanation:', error);
+ }
+ };
+
+ // Calculate metrics
+ const totalRevenue = orders.reduce((sum, order) => sum + order.total_cents, 0);
+ const avgOrderValue = orders.length > 0 ? totalRevenue / orders.length : 0;
+ const todaysOrders = orders.filter(order => {
+ const orderDate = new Date(order.created_at);
+ const today = new Date();
+ return orderDate.toDateString() === today.toDateString();
+ });
+
+ return (
+
+
+
Merchant Dashboard
+
Monitor sales, orders, and run optimizations
+
+
+ {/* Key Metrics */}
+
+
+
+
+
+
+
+ Total Revenue
+ {formatPrice(totalRevenue / 100)}
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Orders
+ {orders.length}
+
+
+
+
+
+
+
+
+
+
+
+
+ Avg Order Value
+ {formatPrice(avgOrderValue / 100)}
+
+
+
+
+
+
+
+
+
+
+
+
+ Today's Orders
+ {todaysOrders.length}
+
+
+
+
+
+
+
+ {/* MINLP Optimization Section */}
+
+
+
Supply Chain Optimization
+
+ {isRunningMinlp ? (
+ <>
+
+
+
+
+ Running MINLP...
+ >
+ ) : (
+ 'Run MINLP Optimization'
+ )}
+
+
+
+ {latestMinlpRun && (
+
+
Latest Optimization Result
+ {latestMinlpRun.solution_json && (
+
+
+
+
Optimal Cost
+
+ {formatPrice(latestMinlpRun.solution_json.kpis?.total_cost / 100 || 0)}
+
+
+
+
Suppliers Used
+
+ {latestMinlpRun.solution_json.kpis?.supplier_count || 0}
+
+
+
+
Avg Lead Time
+
+ {latestMinlpRun.solution_json.kpis?.avg_lead_time || 0} days
+
+
+
+
+
+ Get Explanation β
+
+
+ {showExplanation && latestMinlpRun.rationale_text && (
+
+
{latestMinlpRun.rationale_text}
+
+ )}
+
+ )}
+
+ Run ID: {latestMinlpRun.run_id} β’ {new Date(latestMinlpRun.created_at).toLocaleString()}
+
+
+ )}
+
+
+ {/* Recent Orders */}
+
+
+
Recent Orders (Live Feed)
+
+
+
+
+
+
+ Order ID
+
+
+ Status
+
+
+ Total
+
+
+ Date
+
+
+
+
+ {orders.slice(0, 10).map((order) => (
+
+
+ {order.order_id.substring(0, 8)}...
+
+
+
+ {order.status}
+
+
+
+ {formatPrice(order.total_cents / 100)}
+
+
+ {new Date(order.created_at).toLocaleString()}
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 5e3355c..ec06a99 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,20 +1,20 @@
-import { CartProvider } from 'components/cart/cart-context';
-import { Navbar } from 'components/layout/navbar';
+import { TarazooNavbar } from 'components/layout/navbar/tarazoo-navbar';
+import { Providers } from 'components/providers';
import { WelcomeToast } from 'components/welcome-toast';
import { GeistSans } from 'geist/font/sans';
import { getCart } from 'lib/shopify';
+import { baseUrl } from 'lib/utils';
import { ReactNode } from 'react';
import { Toaster } from 'sonner';
import './globals.css';
-import { baseUrl } from 'lib/utils';
const { SITE_NAME } = process.env;
export const metadata = {
metadataBase: new URL(baseUrl),
title: {
- default: SITE_NAME!,
- template: `%s | ${SITE_NAME}`
+ default: 'Tarazoo - Unified Commerce Platform',
+ template: `%s | Tarazoo`
},
robots: {
follow: true,
@@ -27,20 +27,19 @@ export default async function RootLayout({
}: {
children: ReactNode;
}) {
- // Don't await the fetch, pass the Promise to the context provider
- const cart = getCart();
-
+ // Provide Shopify cart context for components relying on useCart
+ const shopifyCartPromise = getCart();
return (
-
+
-
-
+
+
{children}
-
+
);
diff --git a/app/merchant/[merchantId]/page.tsx b/app/merchant/[merchantId]/page.tsx
new file mode 100644
index 0000000..933c675
--- /dev/null
+++ b/app/merchant/[merchantId]/page.tsx
@@ -0,0 +1,7 @@
+import { DashboardClient } from 'components/dashboard/DashboardClient';
+
+export default async function MerchantDashboardPage({ params }: { params: { merchantId: string } }) {
+ const { merchantId } = params;
+ return ;
+}
+
diff --git a/app/page.tsx b/app/page.tsx
index 7c4a7d7..55d73af 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,6 +1,9 @@
import { Carousel } from 'components/carousel';
import { ThreeItemGrid } from 'components/grid/three-items';
import Footer from 'components/layout/footer';
+import ShopifySetup from 'components/setup/shopify-setup';
+import { isShopifyConfigured } from 'lib/shopify/config';
+import { syncShopifyToSupabase } from 'lib/shopify-supabase-sync';
export const metadata = {
description:
@@ -10,7 +13,18 @@ export const metadata = {
}
};
-export default function HomePage() {
+export const dynamic = 'force-dynamic';
+
+export default async function HomePage() {
+ const shopifyConfigured = isShopifyConfigured();
+
+ if (!shopifyConfigured) {
+ return ;
+ }
+
+ // Ensure Supabase has the latest Shopify data on each homepage load
+ await syncShopifyToSupabase();
+
return (
<>
diff --git a/app/product/[handle]/page.tsx b/app/product/[handle]/page.tsx
index 33de3c0..b2d17d0 100644
--- a/app/product/[handle]/page.tsx
+++ b/app/product/[handle]/page.tsx
@@ -8,6 +8,7 @@ import { ProductProvider } from 'components/product/product-context';
import { ProductDescription } from 'components/product/product-description';
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
import { getProduct, getProductRecommendations } from 'lib/shopify';
+import { syncShopifyProductByHandle } from 'lib/shopify-supabase-sync';
import { Image } from 'lib/shopify/types';
import Link from 'next/link';
import { Suspense } from 'react';
@@ -16,7 +17,7 @@ export async function generateMetadata(props: {
params: Promise<{ handle: string }>;
}): Promise {
const params = await props.params;
- const product = await getProduct(params.handle);
+ const product = await getProduct(params.handle, { fresh: true });
if (!product) return notFound();
@@ -49,9 +50,13 @@ export async function generateMetadata(props: {
};
}
+export const dynamic = 'force-dynamic';
+
export default async function ProductPage(props: { params: Promise<{ handle: string }> }) {
const params = await props.params;
- const product = await getProduct(params.handle);
+ // Always sync the clicked product into Supabase and fetch fresh Shopify data
+ await syncShopifyProductByHandle(params.handle);
+ const product = await getProduct(params.handle, { fresh: true });
if (!product) return notFound();
diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx
index dfb07e3..10bb855 100644
--- a/app/search/[collection]/page.tsx
+++ b/app/search/[collection]/page.tsx
@@ -21,6 +21,8 @@ export async function generateMetadata(props: {
};
}
+export const dynamic = 'force-dynamic';
+
export default async function CategoryPage(props: {
params: Promise<{ collection: string }>;
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -29,7 +31,7 @@ export default async function CategoryPage(props: {
const params = await props.params;
const { sort } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
- const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
+ const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse, fresh: true });
return (
diff --git a/app/search/children-wrapper.tsx b/app/search/children-wrapper.tsx
index 8d345b5..298d3d7 100644
--- a/app/search/children-wrapper.tsx
+++ b/app/search/children-wrapper.tsx
@@ -6,5 +6,5 @@ import { Fragment } from 'react';
// Ensure children are re-rendered when the search query changes
export default function ChildrenWrapper({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams();
- return {children} ;
+ return {children} ;
}
diff --git a/app/search/page.tsx b/app/search/page.tsx
index dce5f05..16f4f24 100644
--- a/app/search/page.tsx
+++ b/app/search/page.tsx
@@ -8,6 +8,8 @@ export const metadata = {
description: 'Search for products in the store.'
};
+export const dynamic = 'force-dynamic';
+
export default async function SearchPage(props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
@@ -15,7 +17,7 @@ export default async function SearchPage(props: {
const { sort, q: searchValue } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
- const products = await getProducts({ sortKey, reverse, query: searchValue });
+ const products = await getProducts({ sortKey, reverse, query: searchValue, fresh: true });
const resultsText = products.length > 1 ? 'results' : 'result';
return (
diff --git a/components/camera-scanner.tsx b/components/camera-scanner.tsx
new file mode 100644
index 0000000..9c0da3e
--- /dev/null
+++ b/components/camera-scanner.tsx
@@ -0,0 +1,303 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import type { Product } from '../packages/shared/types';
+import { getProductByName } from 'lib/supabase';
+
+interface CameraScannerProps {
+ onDetected: (product: Product) => void;
+ onClose: () => void;
+ autoCloseOnScan?: boolean;
+}
+
+export default function CameraScanner({ onDetected, onClose, autoCloseOnScan = true }: CameraScannerProps) {
+ const videoRef = useRef(null);
+ const [isScanning, setIsScanning] = useState(false);
+ const [error, setError] = useState(null);
+ const [manualName, setManualName] = useState('');
+ const [torchEnabled, setTorchEnabled] = useState(false);
+ const streamRef = useRef(null);
+ const timerRef = useRef(null);
+ const [autoMode, setAutoMode] = useState(false);
+
+ useEffect(() => {
+ startScanning();
+
+ return () => {
+ stopScanning();
+ };
+ }, []);
+
+ const startScanning = async () => {
+ try {
+ setIsScanning(true);
+ setError(null);
+
+ const constraints: MediaStreamConstraints = {
+ video: {
+ facingMode: 'environment',
+ width: { ideal: 1280 },
+ height: { ideal: 720 }
+ }
+ };
+
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ streamRef.current = stream;
+
+ if (videoRef.current) {
+ videoRef.current.srcObject = stream;
+ await new Promise((resolve) => {
+ videoRef.current!.onloadedmetadata = resolve;
+ });
+
+ // Check for torch support
+ const track = stream.getVideoTracks()[0];
+ if (track && 'getCapabilities' in track) {
+ const capabilities = (track as any).getCapabilities();
+ if (capabilities && 'torch' in capabilities) {
+ setTorchEnabled(true);
+ }
+ }
+
+ // Start periodic vision detection only if auto mode is enabled
+ if (autoMode) startPeriodicDetection();
+ }
+ } catch (err) {
+ console.error('Camera error:', err);
+ const errorMessage = err instanceof Error && err.name === 'NotAllowedError'
+ ? 'Camera access denied. Please allow camera permissions and try again.'
+ : err instanceof Error && err.name === 'NotFoundError'
+ ? 'No camera found. Please connect a camera and try again.'
+ : 'Unable to access camera. Please check permissions and try again.';
+ setError(errorMessage);
+ setIsScanning(false);
+ }
+ };
+
+ const stopScanning = () => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current as any);
+ timerRef.current = null;
+ }
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ streamRef.current = null;
+ }
+ setIsScanning(false);
+ };
+
+ const toggleTorch = async () => {
+ if (streamRef.current && torchEnabled) {
+ const track = streamRef.current.getVideoTracks()[0];
+ if (!track) return;
+ const capabilities = (track as any).getCapabilities && (track as any).getCapabilities();
+
+ if (capabilities && 'torch' in capabilities) {
+ const settings = (track as any).getSettings ? (track as any).getSettings() : {};
+ const currentTorch = (settings as any).torch || false;
+ await (track as any).applyConstraints({
+ advanced: [{ torch: !currentTorch } as any]
+ });
+ }
+ }
+ };
+
+ const startPeriodicDetection = () => {
+ if (timerRef.current) return;
+ timerRef.current = setInterval(async () => {
+ try {
+ const label = await detectCurrentFrame();
+ if (!label) return;
+ const product = await getProductByName(label);
+ if (product) {
+ if ('vibrate' in navigator) {
+ navigator.vibrate(200);
+ }
+ stopScanning();
+ onDetected(product);
+ if (autoCloseOnScan) onClose();
+ } else {
+ setError(`Detected "${label}" but could not find a matching product.`);
+ setTimeout(() => setError(null), 1500);
+ }
+ } catch (e) {
+ // Swallow intermittent errors from detection
+ }
+ }, 1500);
+ };
+
+ const captureAndDetect = async () => {
+ try {
+ const label = await detectCurrentFrame();
+ if (!label) {
+ setError('Could not identify item. Try again.');
+ setTimeout(() => setError(null), 1500);
+ return;
+ }
+ const product = await getProductByName(label);
+ if (product) {
+ if ('vibrate' in navigator) navigator.vibrate(150);
+ stopScanning();
+ onDetected(product);
+ if (autoCloseOnScan) onClose();
+ } else {
+ setError(`Detected "${label}" but no matching product found.`);
+ setTimeout(() => setError(null), 1500);
+ }
+ } catch (e) {
+ setError('Capture failed. Please try again.');
+ setTimeout(() => setError(null), 1500);
+ }
+ };
+
+ const detectCurrentFrame = async (): Promise => {
+ if (!videoRef.current) return null;
+ const video = videoRef.current;
+ const canvas = document.createElement('canvas');
+ // Square crop center for robustness
+ const size = Math.min(video.videoWidth || 640, video.videoHeight || 640) || 640;
+ canvas.width = size;
+ canvas.height = size;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return null;
+ ctx.drawImage(
+ video,
+ (video.videoWidth - size) / 2,
+ (video.videoHeight - size) / 2,
+ size,
+ size,
+ 0,
+ 0,
+ size,
+ size
+ );
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
+ const res = await fetch('/api/vision-detect', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ image: dataUrl })
+ });
+ if (!res.ok) return null;
+ const json = await res.json();
+ return json.item || null;
+ };
+
+ const handleManualSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!manualName) return;
+ const product = await getProductByName(manualName.toLowerCase());
+ if (product) {
+ onDetected(product);
+ if (autoCloseOnScan) onClose();
+ } else {
+ setError('No matching product found by name');
+ setTimeout(() => setError(null), 1500);
+ }
+ setManualName('');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {torchEnabled && (
+
+
+
+
+
+ )}
+
+
+
+ {/* Scanning overlay */}
+
+
+
+ {isScanning && autoMode && (
+
+ )}
+
+
+
+ {/* Controls: torch (left), auto toggle (next), shutter (bottom center in manual mode) */}
+
{
+ const next = !autoMode;
+ setAutoMode(next);
+ if (next) {
+ startPeriodicDetection();
+ setIsScanning(true);
+ } else if (timerRef.current) {
+ clearInterval(timerRef.current as any);
+ timerRef.current = null;
+ setIsScanning(false);
+ }
+ }}
+ className="absolute top-4 left-20 z-10 bg-white/20 backdrop-blur rounded-full px-3 py-2 text-white text-sm"
+ >
+ {autoMode ? 'Auto: ON' : 'Auto: OFF'}
+
+
+ {!autoMode && (
+
+
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* Manual name input fallback */}
+
+
+
+
+ );
+}
diff --git a/components/carousel.tsx b/components/carousel.tsx
index 751cf4b..63c6762 100644
--- a/components/carousel.tsx
+++ b/components/carousel.tsx
@@ -4,7 +4,7 @@ import { GridTileImage } from './grid/tile';
export async function Carousel() {
// Collections that start with `hidden-*` are hidden from the search page.
- const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel' });
+ const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel', fresh: true });
if (!products?.length) return null;
diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx
index 85e1307..25cb1b8 100644
--- a/components/cart/add-to-cart.tsx
+++ b/components/cart/add-to-cart.tsx
@@ -5,7 +5,7 @@ import clsx from 'clsx';
import { addItem } from 'components/cart/actions';
import { useProduct } from 'components/product/product-context';
import { Product, ProductVariant } from 'lib/shopify/types';
-import { useActionState } from 'react';
+import { useFormState } from 'react-dom';
import { useCart } from './cart-context';
function SubmitButton({
@@ -61,7 +61,7 @@ export function AddToCart({ product }: { product: Product }) {
const { variants, availableForSale } = product;
const { addCartItem } = useCart();
const { state } = useProduct();
- const [message, formAction] = useActionState(addItem, null);
+ const [message, formAction] = useFormState(addItem, null);
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every(
@@ -70,20 +70,23 @@ export function AddToCart({ product }: { product: Product }) {
);
const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;
const selectedVariantId = variant?.id || defaultVariantId;
- const addItemAction = formAction.bind(null, selectedVariantId);
- const finalVariant = variants.find(
+ const finalVariant = variants.find(
(variant) => variant.id === selectedVariantId
)!;
+ // Prefer the selected variant's availability if present; otherwise use product availability
+ const selectedVariant = variants.find((v) => v.id === selectedVariantId);
+ const isAvailableForSale = selectedVariant?.availableForSale ?? availableForSale;
+
return (
-
);
}
diff --git a/components/layout/navbar/tarazoo-navbar.tsx b/components/layout/navbar/tarazoo-navbar.tsx
new file mode 100644
index 0000000..2a6ce0f
--- /dev/null
+++ b/components/layout/navbar/tarazoo-navbar.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import CartButton from 'components/layout/navbar/cart-button';
+import Link from 'next/link';
+import ScannerButton from './scanner-button';
+
+export function TarazooNavbar() {
+ const defaultMerchantId = process.env.NEXT_PUBLIC_MERCHANT_ID_DEFAULT || 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+ return (
+
+
+
+
Tarazoo
+
+
+
+
+ Shop
+
+
+ Merchant
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/layout/search/filter/dropdown.tsx b/components/layout/search/filter/dropdown.tsx
index 6896c5e..69c90a8 100644
--- a/components/layout/search/filter/dropdown.tsx
+++ b/components/layout/search/filter/dropdown.tsx
@@ -29,7 +29,7 @@ export default function FilterItemDropdown({ list }: { list: ListItem[] }) {
list.forEach((listItem: ListItem) => {
if (
('path' in listItem && pathname === listItem.path) ||
- ('slug' in listItem && searchParams.get('sort') === listItem.slug)
+ ('slug' in listItem && searchParams?.get('sort') === listItem.slug)
) {
setActive(listItem.title);
}
diff --git a/components/layout/search/filter/item.tsx b/components/layout/search/filter/item.tsx
index 3fce8e8..c62aabd 100644
--- a/components/layout/search/filter/item.tsx
+++ b/components/layout/search/filter/item.tsx
@@ -11,7 +11,7 @@ function PathFilterItem({ item }: { item: PathFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = pathname === item.path;
- const newParams = new URLSearchParams(searchParams.toString());
+ const newParams = new URLSearchParams(searchParams?.toString() || '');
const DynamicTag = active ? 'p' : Link;
newParams.delete('q');
@@ -36,15 +36,19 @@ function PathFilterItem({ item }: { item: PathFilterItem }) {
function SortFilterItem({ item }: { item: SortFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
- const active = searchParams.get('sort') === item.slug;
- const q = searchParams.get('q');
- const href = createUrl(
- pathname,
- new URLSearchParams({
- ...(q && { q }),
- ...(item.slug && item.slug.length && { sort: item.slug })
- })
- );
+ const active = searchParams?.get('sort') === item.slug;
+ const q = searchParams?.get('q');
+ const params = new URLSearchParams(searchParams?.toString() || '');
+
+ if (q && typeof q === 'string') {
+ params.set('q', q);
+ }
+
+ if (item.slug) {
+ params.set('sort', item.slug);
+ }
+
+ const href = createUrl(pathname, params);
const DynamicTag = active ? 'p' : Link;
return (
diff --git a/components/providers.tsx b/components/providers.tsx
new file mode 100644
index 0000000..87affb1
--- /dev/null
+++ b/components/providers.tsx
@@ -0,0 +1,13 @@
+'use client';
+import { CartProvider } from 'components/cart/supabase-cart-context';
+import { CartProvider as ShopifyCartProvider } from 'components/cart/cart-context';
+import type { Cart } from 'lib/shopify/types';
+import React from 'react';
+
+export function Providers({ children, cartPromise }: { children: React.ReactNode; cartPromise: Promise }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/setup/shopify-setup.tsx b/components/setup/shopify-setup.tsx
new file mode 100644
index 0000000..f784818
--- /dev/null
+++ b/components/setup/shopify-setup.tsx
@@ -0,0 +1,98 @@
+'use client';
+
+export default function ShopifySetup() {
+ return (
+
+
+
Welcome to Next.js Commerce
+
+ Your store needs to be configured with Shopify credentials to start selling.
+
+
+
+
+ π οΈ Quick Setup Guide
+
+
+ Create a Shopify store if you don't have one:
+
+ Start free trial β
+
+
+
+ Create a Custom App in your Shopify admin:
+
+ Go to Settings β Apps and sales channels β Develop apps
+ Create an app and configure Storefront API access
+ Enable all Storefront API scopes needed
+
+
+
+ Get your credentials:
+
+ Copy your Storefront API access token
+ Note your store domain (e.g., your-store.myshopify.com)
+
+
+
+ Update your .env file with:
+
+{`SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
+SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-token-here"
+SHOPIFY_REVALIDATION_SECRET="your-secret"`}
+
+
+
+ Restart your development server to apply the changes
+
+
+
+
+
+
+
+
+ β οΈ Current Status
+
+
+ Shopify credentials are not configured. The application is running in setup mode.
+
+
+
+
+
+ );
+}
diff --git a/docs/shopify-setup.md b/docs/shopify-setup.md
new file mode 100644
index 0000000..3b73519
--- /dev/null
+++ b/docs/shopify-setup.md
@@ -0,0 +1,35 @@
+# Setting Up Shopify Storefront API
+
+## Step 1: Get Your Shopify Store Domain
+Your store domain is: `your-store.myshopify.com`
+
+## Step 2: Create a Storefront Access Token
+
+1. Go to your Shopify Admin
+2. Navigate to: **Settings** β **Apps and sales channels** β **Develop apps**
+3. Click **Create an app**
+4. Name it "Tarazoo Integration"
+5. In the app configuration:
+ - Go to **API credentials** tab
+ - Under **Storefront API**, click **Configure**
+ - Check these scopes:
+ - `unauthenticated_read_product_listings`
+ - `unauthenticated_read_product_inventory`
+ - `unauthenticated_read_product_tags`
+ - Click **Save**
+6. Click **Install app**
+7. Under **Storefront API access token**, click **Reveal token once**
+8. Copy the token (it starts with something like `shpat_`)
+
+## Step 3: Add to .env file
+
+```bash
+SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
+SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-token-here"
+```
+
+## Step 4: Test the connection
+
+```bash
+curl -X POST http://localhost:3000/api/sync-products
+```
diff --git a/frontend/.next/cache/webpack/client-development-fallback/0.pack.gz b/frontend/.next/cache/webpack/client-development-fallback/0.pack.gz
new file mode 100644
index 0000000..4fa48a9
Binary files /dev/null and b/frontend/.next/cache/webpack/client-development-fallback/0.pack.gz differ
diff --git a/frontend/.next/cache/webpack/client-development-fallback/index.pack.gz b/frontend/.next/cache/webpack/client-development-fallback/index.pack.gz
new file mode 100644
index 0000000..4c17c3f
Binary files /dev/null and b/frontend/.next/cache/webpack/client-development-fallback/index.pack.gz differ
diff --git a/frontend/.next/cache/webpack/client-development/0.pack.gz b/frontend/.next/cache/webpack/client-development/0.pack.gz
new file mode 100644
index 0000000..7331fdb
Binary files /dev/null and b/frontend/.next/cache/webpack/client-development/0.pack.gz differ
diff --git a/frontend/.next/cache/webpack/client-development/index.pack.gz b/frontend/.next/cache/webpack/client-development/index.pack.gz
new file mode 100644
index 0000000..da29a89
Binary files /dev/null and b/frontend/.next/cache/webpack/client-development/index.pack.gz differ
diff --git a/frontend/.next/cache/webpack/client-development/index.pack.gz.old b/frontend/.next/cache/webpack/client-development/index.pack.gz.old
new file mode 100644
index 0000000..62b0256
Binary files /dev/null and b/frontend/.next/cache/webpack/client-development/index.pack.gz.old differ
diff --git a/frontend/.next/server/app-paths-manifest.json b/frontend/.next/server/app-paths-manifest.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/frontend/.next/server/app-paths-manifest.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/frontend/.next/server/pages-manifest.json b/frontend/.next/server/pages-manifest.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/frontend/.next/server/pages-manifest.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/frontend/.next/server/server-reference-manifest.js b/frontend/.next/server/server-reference-manifest.js
new file mode 100644
index 0000000..cc0af96
--- /dev/null
+++ b/frontend/.next/server/server-reference-manifest.js
@@ -0,0 +1 @@
+self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"
\ No newline at end of file
diff --git a/frontend/.next/server/server-reference-manifest.json b/frontend/.next/server/server-reference-manifest.json
new file mode 100644
index 0000000..e2429c9
--- /dev/null
+++ b/frontend/.next/server/server-reference-manifest.json
@@ -0,0 +1,5 @@
+{
+ "node": {},
+ "edge": {},
+ "encryptionKey": "TEyKbbu+WKJkZFV0ab1zMjavu95OP4JO/kvlloqZkMs="
+}
\ No newline at end of file
diff --git a/frontend/.next/types/cache-life.d.ts b/frontend/.next/types/cache-life.d.ts
new file mode 100644
index 0000000..3353fc7
--- /dev/null
+++ b/frontend/.next/types/cache-life.d.ts
@@ -0,0 +1,141 @@
+// Type definitions for Next.js cacheLife configs
+
+declare module 'next/cache' {
+ export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
+ export {
+ revalidateTag,
+ revalidatePath,
+ unstable_expireTag,
+ unstable_expirePath,
+ } from 'next/dist/server/web/spec-extension/revalidate'
+ export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
+
+
+ /**
+ * Cache this `"use cache"` for a timespan defined by the `"default"` profile.
+ * ```
+ * stale: 300 seconds (5 minutes)
+ * revalidate: 900 seconds (15 minutes)
+ * expire: never
+ * ```
+ *
+ * This cache may be stale on clients for 5 minutes before checking with the server.
+ * If the server receives a new request after 15 minutes, start revalidating new values in the background.
+ * It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
+ */
+ export function unstable_cacheLife(profile: "default"): void
+
+ /**
+ * Cache this `"use cache"` for a timespan defined by the `"seconds"` profile.
+ * ```
+ * stale: 30 seconds
+ * revalidate: 1 seconds
+ * expire: 60 seconds (1 minute)
+ * ```
+ *
+ * This cache may be stale on clients for 30 seconds before checking with the server.
+ * If the server receives a new request after 1 seconds, start revalidating new values in the background.
+ * If this entry has no traffic for 1 minute it will expire. The next request will recompute it.
+ */
+ export function unstable_cacheLife(profile: "seconds"): void
+
+ /**
+ * Cache this `"use cache"` for a timespan defined by the `"minutes"` profile.
+ * ```
+ * stale: 300 seconds (5 minutes)
+ * revalidate: 60 seconds (1 minute)
+ * expire: 3600 seconds (1 hour)
+ * ```
+ *
+ * This cache may be stale on clients for 5 minutes before checking with the server.
+ * If the server receives a new request after 1 minute, start revalidating new values in the background.
+ * If this entry has no traffic for 1 hour it will expire. The next request will recompute it.
+ */
+ export function unstable_cacheLife(profile: "minutes"): void
+
+ /**
+ * Cache this `"use cache"` for a timespan defined by the `"hours"` profile.
+ * ```
+ * stale: 300 seconds (5 minutes)
+ * revalidate: 3600 seconds (1 hour)
+ * expire: 86400 seconds (1 day)
+ * ```
+ *
+ * This cache may be stale on clients for 5 minutes before checking with the server.
+ * If the server receives a new request after 1 hour, start revalidating new values in the background.
+ * If this entry has no traffic for 1 day it will expire. The next request will recompute it.
+ */
+ export function unstable_cacheLife(profile: "hours"): void
+
+ /**
+ * Cache this `"use cache"` for a timespan defined by the `"days"` profile.
+ * ```
+ * stale: 300 seconds (5 minutes)
+ * revalidate: 86400 seconds (1 day)
+ * expire: 604800 seconds (1 week)
+ * ```
+ *
+ * This cache may be stale on clients for 5 minutes before checking with the server.
+ * If the server receives a new request after 1 day, start revalidating new values in the background.
+ * If this entry has no traffic for 1 week it will expire. The next request will recompute it.
+ */
+ export function unstable_cacheLife(profile: "days"): void
+
+ /**
+ * Cache this `"use cache"` for a timespan defined by the `"weeks"` profile.
+ * ```
+ * stale: 300 seconds (5 minutes)
+ * revalidate: 604800 seconds (1 week)
+ * expire: 2592000 seconds (30 days)
+ * ```
+ *
+ * This cache may be stale on clients for 5 minutes before checking with the server.
+ * If the server receives a new request after 1 week, start revalidating new values in the background.
+ * If this entry has no traffic for 30 days it will expire. The next request will recompute it.
+ */
+ export function unstable_cacheLife(profile: "weeks"): void
+
+ /**
+ * Cache this `"use cache"` for a timespan defined by the `"max"` profile.
+ * ```
+ * stale: 300 seconds (5 minutes)
+ * revalidate: 2592000 seconds (30 days)
+ * expire: never
+ * ```
+ *
+ * This cache may be stale on clients for 5 minutes before checking with the server.
+ * If the server receives a new request after 30 days, start revalidating new values in the background.
+ * It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
+ */
+ export function unstable_cacheLife(profile: "max"): void
+
+ /**
+ * Cache this `"use cache"` using a custom timespan.
+ * ```
+ * stale: ... // seconds
+ * revalidate: ... // seconds
+ * expire: ... // seconds
+ * ```
+ *
+ * This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate`
+ *
+ * If a value is left out, the lowest of other cacheLife() calls or the default, is used instead.
+ */
+ export function unstable_cacheLife(profile: {
+ /**
+ * This cache may be stale on clients for ... seconds before checking with the server.
+ */
+ stale?: number,
+ /**
+ * If the server receives a new request after ... seconds, start revalidating new values in the background.
+ */
+ revalidate?: number,
+ /**
+ * If this entry has no traffic for ... seconds it will expire. The next request will recompute it.
+ */
+ expire?: number
+ }): void
+
+
+ export { cacheTag as unstable_cacheTag } from 'next/dist/server/use-cache/cache-tag'
+}
diff --git a/frontend/.next/types/package.json b/frontend/.next/types/package.json
new file mode 100644
index 0000000..1632c2c
--- /dev/null
+++ b/frontend/.next/types/package.json
@@ -0,0 +1 @@
+{"type": "module"}
\ No newline at end of file
diff --git a/frontend/.next/types/routes.d.ts b/frontend/.next/types/routes.d.ts
new file mode 100644
index 0000000..15617e1
--- /dev/null
+++ b/frontend/.next/types/routes.d.ts
@@ -0,0 +1,55 @@
+// This file is generated automatically by Next.js
+// Do not edit this file manually
+
+type AppRoutes = never
+type PageRoutes = never
+type LayoutRoutes = never
+type RedirectRoutes = never
+type RewriteRoutes = never
+type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
+
+
+interface ParamMap {
+}
+
+
+export type ParamsOf = ParamMap[Route]
+
+interface LayoutSlotMap {
+}
+
+
+export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap }
+
+declare global {
+ /**
+ * Props for Next.js App Router page components
+ * @example
+ * ```tsx
+ * export default function Page(props: PageProps<'/blog/[slug]'>) {
+ * const { slug } = await props.params
+ * return Blog post: {slug}
+ * }
+ * ```
+ */
+ interface PageProps {
+ params: Promise
+ searchParams: Promise>
+ }
+
+ /**
+ * Props for Next.js App Router layout components
+ * @example
+ * ```tsx
+ * export default function Layout(props: LayoutProps<'/dashboard'>) {
+ * return {props.children}
+ * }
+ * ```
+ */
+ type LayoutProps = {
+ params: Promise
+ children: React.ReactNode
+ } & {
+ [K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
+ }
+}
diff --git a/frontend/.next/types/validator.ts b/frontend/.next/types/validator.ts
new file mode 100644
index 0000000..000dc8e
--- /dev/null
+++ b/frontend/.next/types/validator.ts
@@ -0,0 +1,16 @@
+// This file is generated automatically by Next.js
+// Do not edit this file manually
+// This file validates that all pages and layouts export the correct types
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/shopify-supabase-sync.ts b/lib/shopify-supabase-sync.ts
new file mode 100644
index 0000000..400d1d8
--- /dev/null
+++ b/lib/shopify-supabase-sync.ts
@@ -0,0 +1,190 @@
+import { getProduct, getProducts } from './shopify';
+import { supabase, supabaseAdmin } from './supabase';
+import type { Product as ShopifyProduct } from './shopify/types';
+
+const MERCHANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+
+export interface SupabaseProduct {
+ product_id: string;
+ merchant_id: string;
+ sku: string;
+ name: string;
+ price_cents: number;
+ barcode: string;
+ shopify_id: string;
+ shopify_handle: string;
+ image_url?: string;
+ created_at?: string;
+}
+
+// Sync a single product by Shopify handle
+export async function syncShopifyProductByHandle(handle: string) {
+ try {
+ const product = await getProduct(handle, { fresh: true });
+ if (!product) {
+ return { success: false, message: 'Product not found in Shopify' };
+ }
+
+ const supabaseProducts: SupabaseProduct[] = [];
+ product.variants.forEach((variant, index) => {
+ const shopifyBarcode = (variant as any).barcode;
+ const supabaseProduct: SupabaseProduct = {
+ product_id: `${product.id}-${variant.id}`,
+ merchant_id: MERCHANT_ID,
+ sku: (variant as any).sku || `SKU-${product.id}-${index}`,
+ name: variant.title === 'Default Title' ? product.title : `${product.title} - ${variant.title}`,
+ price_cents: Math.round(parseFloat(variant.price.amount) * 100),
+ barcode: shopifyBarcode || generateBarcode(product, index),
+ shopify_id: product.id,
+ shopify_handle: product.handle,
+ image_url: product.featuredImage?.url
+ };
+ supabaseProducts.push(supabaseProduct);
+ });
+
+ const client = supabaseAdmin || supabase;
+ const { data, error } = await client
+ .from('products')
+ .upsert(supabaseProducts, { onConflict: 'product_id', ignoreDuplicates: false })
+ .select();
+
+ if (error) {
+ console.error('Supabase sync (single) error:', error);
+ return { success: false, error };
+ }
+
+ return { success: true, count: data?.length || 0, products: data };
+ } catch (error) {
+ console.error('Single product sync failed:', error);
+ return { success: false, error };
+ }
+}
+
+// Generate barcode from Shopify product ID or variant barcode
+function generateBarcode(product: ShopifyProduct, variantIndex: number = 0): string {
+ // Check if variant exists
+ const variant = product.variants[variantIndex];
+
+ // Generate deterministic barcode from product/variant ID
+ const baseId = product.id.replace(/[^0-9]/g, '');
+ const variantId = variant?.id.replace(/[^0-9]/g, '') || '0';
+
+ // Create 13-digit barcode (EAN-13 format)
+ const combined = (baseId + variantId).padStart(12, '0').slice(-12);
+ return combined + calculateCheckDigit(combined);
+}
+
+// Calculate EAN-13 check digit
+function calculateCheckDigit(code: string | undefined): string {
+ if (!code) return '0';
+ let sum = 0;
+ for (let i = 0; i < 12; i++) {
+ const digit = Number(code.charAt(i) || '0');
+ sum += digit * (i % 2 === 0 ? 1 : 3);
+ }
+ return ((10 - (sum % 10)) % 10).toString();
+}
+
+export async function syncShopifyToSupabase() {
+ try {
+ console.log('π Syncing Shopify products to Supabase...');
+
+ // Fetch products from Shopify (fresh)
+ const shopifyProducts = await getProducts({ fresh: true });
+
+ if (!shopifyProducts || shopifyProducts.length === 0) {
+ console.log('No products found in Shopify');
+ return { success: false, message: 'No products in Shopify' };
+ }
+
+ // Transform Shopify products to Supabase format
+ const supabaseProducts: SupabaseProduct[] = [];
+
+ for (const product of shopifyProducts) {
+ // Create a product entry for each variant
+ product.variants.forEach((variant, index) => {
+ // Check if variant has a barcode field (from Shopify inventory)
+ const shopifyBarcode = (variant as any).barcode;
+
+ const supabaseProduct: SupabaseProduct = {
+ product_id: `${product.id}-${variant.id}`,
+ merchant_id: MERCHANT_ID,
+ sku: (variant as any).sku || `SKU-${product.id}-${index}`,
+ name: variant.title === 'Default Title'
+ ? product.title
+ : `${product.title} - ${variant.title}`,
+ price_cents: Math.round(parseFloat(variant.price.amount) * 100),
+ barcode: shopifyBarcode || generateBarcode(product, index),
+ shopify_id: product.id,
+ shopify_handle: product.handle,
+ image_url: product.featuredImage?.url
+ };
+
+ supabaseProducts.push(supabaseProduct);
+ });
+ }
+
+ // Schema update will be done manually in Supabase dashboard
+
+ // Use admin client to bypass RLS
+ const client = supabaseAdmin || supabase;
+
+ // Upsert products to Supabase
+ const { data, error } = await client
+ .from('products')
+ .upsert(supabaseProducts, {
+ onConflict: 'product_id',
+ ignoreDuplicates: false
+ })
+ .select();
+
+ if (error) {
+ console.error('Supabase sync error:', error);
+ return { success: false, error };
+ }
+
+ console.log(`β
Synced ${data?.length || 0} products from Shopify to Supabase`);
+
+ // Log sample products with barcodes
+ if (data && data.length > 0) {
+ console.log('\nπ¦ Sample synced products:');
+ data.slice(0, 3).forEach(p => {
+ console.log(` - ${p.name}`);
+ console.log(` Barcode: ${p.barcode}`);
+ console.log(` Price: $${(p.price_cents / 100).toFixed(2)}`);
+ });
+ }
+
+ return {
+ success: true,
+ count: data?.length || 0,
+ products: data
+ };
+
+ } catch (error) {
+ console.error('Sync failed:', error);
+ return { success: false, error };
+ }
+}
+
+export async function getProductByBarcodeOrShopify(barcode: string) {
+ // First try to find by barcode
+ const { data: barcodeMatch } = await supabase
+ .from('products')
+ .select('*')
+ .eq('barcode', barcode)
+ .single();
+
+ if (barcodeMatch) {
+ return barcodeMatch;
+ }
+
+ // If no barcode match, check if it's a Shopify product ID
+ const { data: shopifyMatch } = await supabase
+ .from('products')
+ .select('*')
+ .or(`shopify_id.eq.${barcode},product_id.eq.${barcode}`)
+ .single();
+
+ return shopifyMatch;
+}
diff --git a/lib/shopify/config.ts b/lib/shopify/config.ts
new file mode 100644
index 0000000..504fff3
--- /dev/null
+++ b/lib/shopify/config.ts
@@ -0,0 +1,9 @@
+// Check if Shopify credentials are configured
+export function isShopifyConfigured(): boolean {
+ return Boolean(
+ process.env.SHOPIFY_STORE_DOMAIN &&
+ process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN &&
+ !process.env.SHOPIFY_STORE_DOMAIN.includes('[YOUR-STORE]') &&
+ !process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN.includes('[PASTE')
+ );
+}
diff --git a/lib/shopify/fragments/product.ts b/lib/shopify/fragments/product.ts
index be14ded..d922326 100644
--- a/lib/shopify/fragments/product.ts
+++ b/lib/shopify/fragments/product.ts
@@ -38,6 +38,8 @@ const productFragment = /* GraphQL */ `
amount
currencyCode
}
+ sku
+ barcode
}
}
}
diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts
index b908931..2dce971 100644
--- a/lib/shopify/index.ts
+++ b/lib/shopify/index.ts
@@ -5,10 +5,11 @@ import {
} from 'lib/constants';
import { isShopifyError } from 'lib/type-guards';
import { ensureStartsWith } from 'lib/utils';
+import { isShopifyConfigured } from './config';
+import { mockProducts, mockCollections, mockMenu, mockCart } from './mock-data';
import {
revalidateTag,
- unstable_cacheTag as cacheTag,
- unstable_cacheLife as cacheLife
+ unstable_noStore as noStore
} from 'next/cache';
import { cookies, headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
@@ -62,7 +63,7 @@ const domain = process.env.SHOPIFY_STORE_DOMAIN
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
: '';
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
-const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
+const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN || '';
type ExtractVariables = T extends { variables: object }
? T['variables']
@@ -71,12 +72,25 @@ type ExtractVariables = T extends { variables: object }
export async function shopifyFetch({
headers,
query,
+ tags,
variables
}: {
headers?: HeadersInit;
query: string;
+ tags?: string[];
variables?: ExtractVariables;
}): Promise<{ status: number; body: T } | never> {
+ // Check if Shopify is properly configured
+ if (!isShopifyConfigured()) {
+ console.warn('β οΈ Shopify credentials not configured. Please set SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_ACCESS_TOKEN in your .env file');
+ throw {
+ cause: 'Shopify not configured',
+ status: 500,
+ message: 'Store configuration is missing. Please configure your Shopify credentials.',
+ query
+ };
+ }
+
try {
const result = await fetch(endpoint, {
method: 'POST',
@@ -88,7 +102,8 @@ export async function shopifyFetch({
body: JSON.stringify({
...(query && { query }),
...(variables && { variables })
- })
+ }),
+ next: { tags }
});
const body = await result.json();
@@ -264,6 +279,10 @@ export async function updateCart(
}
export async function getCart(): Promise {
+ if (!isShopifyConfigured()) {
+ return mockCart;
+ }
+
const cartId = (await cookies()).get('cartId')?.value;
if (!cartId) {
@@ -286,15 +305,16 @@ export async function getCart(): Promise {
export async function getCollection(
handle: string
): Promise {
- 'use cache';
- cacheTag(TAGS.collections);
- cacheLife('days');
+ if (!isShopifyConfigured()) {
+ return mockCollections.find((c) => c.handle === handle);
+ }
const res = await shopifyFetch({
query: getCollectionQuery,
variables: {
handle
- }
+ },
+ tags: [TAGS.collections]
});
return reshapeCollection(res.body.data.collection);
@@ -303,15 +323,22 @@ export async function getCollection(
export async function getCollectionProducts({
collection,
reverse,
- sortKey
+ sortKey,
+ fresh
}: {
collection: string;
reverse?: boolean;
sortKey?: string;
+ fresh?: boolean;
}): Promise {
- 'use cache';
- cacheTag(TAGS.collections, TAGS.products);
- cacheLife('days');
+ if (fresh) {
+ noStore();
+ }
+
+ if (!isShopifyConfigured()) {
+ // Return mock products for demo mode
+ return mockProducts;
+ }
const res = await shopifyFetch({
query: getCollectionProductsQuery,
@@ -319,7 +346,8 @@ export async function getCollectionProducts({
handle: collection,
reverse,
sortKey: sortKey === 'CREATED_AT' ? 'CREATED' : sortKey
- }
+ },
+ tags: [TAGS.collections, TAGS.products]
});
if (!res.body.data.collection) {
@@ -327,18 +355,17 @@ export async function getCollectionProducts({
return [];
}
- return reshapeProducts(
- removeEdgesAndNodes(res.body.data.collection.products)
- );
+ return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products));
}
export async function getCollections(): Promise {
- 'use cache';
- cacheTag(TAGS.collections);
- cacheLife('days');
+ if (!isShopifyConfigured()) {
+ return mockCollections;
+ }
const res = await shopifyFetch({
- query: getCollectionsQuery
+ query: getCollectionsQuery,
+ tags: [TAGS.collections]
});
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
const collections = [
@@ -364,15 +391,16 @@ export async function getCollections(): Promise {
}
export async function getMenu(handle: string): Promise {
- 'use cache';
- cacheTag(TAGS.collections);
- cacheLife('days');
+ if (!isShopifyConfigured()) {
+ return mockMenu;
+ }
const res = await shopifyFetch({
query: getMenuQuery,
variables: {
handle
- }
+ },
+ tags: [TAGS.collections]
});
return (
@@ -403,33 +431,36 @@ export async function getPages(): Promise {
return removeEdgesAndNodes(res.body.data.pages);
}
-export async function getProduct(handle: string): Promise {
- 'use cache';
- cacheTag(TAGS.products);
- cacheLife('days');
+export async function getProduct(
+ handle: string,
+ opts?: { fresh?: boolean }
+): Promise {
+ if (opts?.fresh) {
+ noStore();
+ }
+
+ if (!isShopifyConfigured()) {
+ return mockProducts.find((p) => p.handle === handle);
+ }
const res = await shopifyFetch({
query: getProductQuery,
variables: {
handle
- }
+ },
+ tags: [TAGS.products]
});
return reshapeProduct(res.body.data.product, false);
}
-export async function getProductRecommendations(
- productId: string
-): Promise {
- 'use cache';
- cacheTag(TAGS.products);
- cacheLife('days');
-
+export async function getProductRecommendations(productId: string): Promise {
const res = await shopifyFetch({
query: getProductRecommendationsQuery,
variables: {
productId
- }
+ },
+ tags: [TAGS.products]
});
return reshapeProducts(res.body.data.productRecommendations);
@@ -438,15 +469,21 @@ export async function getProductRecommendations(
export async function getProducts({
query,
reverse,
- sortKey
+ sortKey,
+ fresh
}: {
query?: string;
reverse?: boolean;
sortKey?: string;
+ fresh?: boolean;
}): Promise {
- 'use cache';
- cacheTag(TAGS.products);
- cacheLife('days');
+ if (fresh) {
+ noStore();
+ }
+
+ if (!isShopifyConfigured()) {
+ return mockProducts;
+ }
const res = await shopifyFetch({
query: getProductsQuery,
@@ -454,7 +491,8 @@ export async function getProducts({
query,
reverse,
sortKey
- }
+ },
+ tags: [TAGS.products]
});
return reshapeProducts(removeEdgesAndNodes(res.body.data.products));
diff --git a/lib/shopify/mock-data.ts b/lib/shopify/mock-data.ts
new file mode 100644
index 0000000..769d61e
--- /dev/null
+++ b/lib/shopify/mock-data.ts
@@ -0,0 +1,248 @@
+import { Product, Collection } from './types';
+
+// Mock products for demo mode when Shopify is not configured
+export const mockProducts: Product[] = [
+ {
+ id: 'mock-product-1',
+ handle: 'sample-product-1',
+ availableForSale: true,
+ title: 'Sample Product 1',
+ description: 'This is a sample product for demonstration purposes.',
+ descriptionHtml: 'This is a sample product for demonstration purposes.
',
+ options: [
+ {
+ id: 'option-1',
+ name: 'Size',
+ values: ['Small', 'Medium', 'Large']
+ }
+ ],
+ priceRange: {
+ maxVariantPrice: {
+ amount: '99.00',
+ currencyCode: 'USD'
+ },
+ minVariantPrice: {
+ amount: '99.00',
+ currencyCode: 'USD'
+ }
+ },
+ variants: [
+ {
+ id: 'variant-1',
+ title: 'Small',
+ availableForSale: true,
+ selectedOptions: [
+ {
+ name: 'Size',
+ value: 'Small'
+ }
+ ],
+ price: {
+ amount: '99.00',
+ currencyCode: 'USD'
+ }
+ }
+ ],
+ featuredImage: {
+ url: 'https://via.placeholder.com/600x600/4F46E5/ffffff?text=Sample+Product+1',
+ altText: 'Sample Product 1',
+ width: 600,
+ height: 600
+ },
+ images: [
+ {
+ url: 'https://via.placeholder.com/600x600/4F46E5/ffffff?text=Sample+Product+1',
+ altText: 'Sample Product 1',
+ width: 600,
+ height: 600
+ }
+ ],
+ seo: {
+ title: 'Sample Product 1',
+ description: 'This is a sample product for demonstration purposes.'
+ },
+ tags: [],
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'mock-product-2',
+ handle: 'sample-product-2',
+ availableForSale: true,
+ title: 'Sample Product 2',
+ description: 'Another sample product for demonstration.',
+ descriptionHtml: 'Another sample product for demonstration.
',
+ options: [
+ {
+ id: 'option-2',
+ name: 'Color',
+ values: ['Red', 'Blue', 'Green']
+ }
+ ],
+ priceRange: {
+ maxVariantPrice: {
+ amount: '149.00',
+ currencyCode: 'USD'
+ },
+ minVariantPrice: {
+ amount: '149.00',
+ currencyCode: 'USD'
+ }
+ },
+ variants: [
+ {
+ id: 'variant-2',
+ title: 'Red',
+ availableForSale: true,
+ selectedOptions: [
+ {
+ name: 'Color',
+ value: 'Red'
+ }
+ ],
+ price: {
+ amount: '149.00',
+ currencyCode: 'USD'
+ }
+ }
+ ],
+ featuredImage: {
+ url: 'https://via.placeholder.com/600x600/10B981/ffffff?text=Sample+Product+2',
+ altText: 'Sample Product 2',
+ width: 600,
+ height: 600
+ },
+ images: [
+ {
+ url: 'https://via.placeholder.com/600x600/10B981/ffffff?text=Sample+Product+2',
+ altText: 'Sample Product 2',
+ width: 600,
+ height: 600
+ }
+ ],
+ seo: {
+ title: 'Sample Product 2',
+ description: 'Another sample product for demonstration.'
+ },
+ tags: [],
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'mock-product-3',
+ handle: 'sample-product-3',
+ availableForSale: true,
+ title: 'Sample Product 3',
+ description: 'A third sample product for the demo.',
+ descriptionHtml: 'A third sample product for the demo.
',
+ options: [
+ {
+ id: 'option-3',
+ name: 'Style',
+ values: ['Classic', 'Modern']
+ }
+ ],
+ priceRange: {
+ maxVariantPrice: {
+ amount: '199.00',
+ currencyCode: 'USD'
+ },
+ minVariantPrice: {
+ amount: '199.00',
+ currencyCode: 'USD'
+ }
+ },
+ variants: [
+ {
+ id: 'variant-3',
+ title: 'Classic',
+ availableForSale: true,
+ selectedOptions: [
+ {
+ name: 'Style',
+ value: 'Classic'
+ }
+ ],
+ price: {
+ amount: '199.00',
+ currencyCode: 'USD'
+ }
+ }
+ ],
+ featuredImage: {
+ url: 'https://via.placeholder.com/600x600/F59E0B/ffffff?text=Sample+Product+3',
+ altText: 'Sample Product 3',
+ width: 600,
+ height: 600
+ },
+ images: [
+ {
+ url: 'https://via.placeholder.com/600x600/F59E0B/ffffff?text=Sample+Product+3',
+ altText: 'Sample Product 3',
+ width: 600,
+ height: 600
+ }
+ ],
+ seo: {
+ title: 'Sample Product 3',
+ description: 'A third sample product for the demo.'
+ },
+ tags: [],
+ updatedAt: new Date().toISOString()
+ }
+];
+
+export const mockCollections: Collection[] = [
+ {
+ handle: '',
+ title: 'All',
+ description: 'All products',
+ seo: {
+ title: 'All',
+ description: 'All products'
+ },
+ path: '/search',
+ updatedAt: new Date().toISOString()
+ },
+ {
+ handle: 'featured',
+ title: 'Featured',
+ description: 'Featured products',
+ seo: {
+ title: 'Featured',
+ description: 'Featured products'
+ },
+ path: '/search/featured',
+ updatedAt: new Date().toISOString()
+ }
+];
+
+export const mockMenu = [
+ {
+ title: 'All',
+ path: '/search'
+ },
+ {
+ title: 'Featured',
+ path: '/search/featured'
+ }
+];
+
+export const mockCart = {
+ id: 'mock-cart',
+ checkoutUrl: '#',
+ cost: {
+ subtotalAmount: {
+ amount: '0',
+ currencyCode: 'USD'
+ },
+ totalAmount: {
+ amount: '0',
+ currencyCode: 'USD'
+ },
+ totalTaxAmount: {
+ amount: '0',
+ currencyCode: 'USD'
+ }
+ },
+ lines: [],
+ totalQuantity: 0
+};
diff --git a/lib/supabase.ts b/lib/supabase.ts
new file mode 100644
index 0000000..96c8857
--- /dev/null
+++ b/lib/supabase.ts
@@ -0,0 +1,247 @@
+import { createClient } from '@supabase/supabase-js';
+import type { Product, Order, OrderItem, MinlpRun } from '../packages/shared/types';
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
+const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
+const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
+
+// Client for browser/public operations
+export const supabase = createClient(supabaseUrl, supabaseAnonKey);
+
+// Service client for server operations (if needed)
+export const supabaseAdmin = supabaseServiceKey
+ ? createClient(supabaseUrl, supabaseServiceKey)
+ : null;
+
+// Product operations
+export async function getProductByBarcode(barcode: string): Promise {
+ // First try local Supabase lookup
+ const { data, error } = await supabase
+ .from('products')
+ .select('*')
+ .eq('barcode', barcode)
+ .single();
+
+ if (data) {
+ return data;
+ }
+
+ // If not found, trigger sync and try again
+ if (error?.code === 'PGRST116') {
+ console.log('Product not found locally, syncing from Shopify...');
+
+ // Trigger sync
+ await fetch('/api/sync-products', { method: 'POST' });
+
+ // Try again after sync
+ const { data: retryData } = await supabase
+ .from('products')
+ .select('*')
+ .eq('barcode', barcode)
+ .single();
+
+ return retryData || null;
+ }
+
+ return null;
+}
+
+export async function getAllProducts(): Promise {
+ const { data, error } = await supabase
+ .from('products')
+ .select('*')
+ .order('name');
+
+ if (error) {
+ console.error('Error fetching products:', error);
+ return [];
+ }
+
+ return data || [];
+}
+
+// Find a product by exact name (case-insensitive)
+export async function getProductByName(name: string): Promise {
+ const { data, error } = await supabase
+ .from('products')
+ .select('*')
+ .ilike('name', name)
+ .maybeSingle();
+
+ if (error) {
+ console.error('Error fetching product by name:', error);
+ return null;
+ }
+
+ return data ?? null;
+}
+
+// Order operations
+export async function createOrder(
+ merchantId: string,
+ items: Array<{ sku: string; qty: number; price_cents: number }>,
+ subtotal: number,
+ tax: number,
+ total: number,
+ discountLabel?: string,
+ discountCents?: number
+): Promise {
+ // Start a Supabase transaction
+ const { data: order, error: orderError } = await supabase
+ .from('orders')
+ .insert({
+ merchant_id: merchantId,
+ subtotal_cents: subtotal,
+ tax_cents: tax,
+ total_cents: total,
+ status: 'confirmed_demo',
+ // New discount fields (requires DB migration)
+ discount_cents: typeof discountCents === 'number' ? discountCents : 0,
+ discount_label: discountLabel ?? null
+ })
+ .select()
+ .single();
+
+ if (orderError) {
+ console.error('Error creating order:', orderError);
+ return null;
+ }
+
+ // Create order items
+ const orderItems = items.map(item => ({
+ order_id: order.order_id,
+ sku: item.sku,
+ qty: item.qty,
+ price_cents: item.price_cents
+ }));
+
+ const { error: itemsError } = await supabase
+ .from('order_items')
+ .insert(orderItems);
+
+ if (itemsError) {
+ console.error('Error creating order items:', itemsError);
+ return null;
+ }
+
+ return order;
+}
+
+export async function getOrders(merchantId?: string): Promise {
+ let query = supabase
+ .from('orders')
+ .select('*')
+ .order('created_at', { ascending: false });
+
+ if (merchantId) {
+ query = query.eq('merchant_id', merchantId);
+ }
+
+ const { data, error } = await query;
+
+ if (error) {
+ console.error('Error fetching orders:', error);
+ return [];
+ }
+
+ return data || [];
+}
+
+export async function getOrderItems(orderId: string): Promise {
+ const { data, error } = await supabase
+ .from('order_items')
+ .select('*')
+ .eq('order_id', orderId);
+
+ if (error) {
+ console.error('Error fetching order items:', error);
+ return [];
+ }
+
+ return data || [];
+}
+
+// MINLP operations
+export async function createMinlpRun(
+ merchantId: string,
+ orderId: string | null,
+ inputJson: any,
+ solutionJson?: any,
+ rationaleText?: string
+): Promise {
+ const { data, error } = await supabase
+ .from('minlp_runs')
+ .insert({
+ merchant_id: merchantId,
+ order_id: orderId,
+ input_json: inputJson,
+ solution_json: solutionJson,
+ rationale_text: rationaleText
+ })
+ .select()
+ .single();
+
+ if (error) {
+ console.error('Error creating MINLP run:', error);
+ return null;
+ }
+
+ return data;
+}
+
+export async function getLatestMinlpRun(merchantId: string): Promise {
+ const { data, error } = await supabase
+ .from('minlp_runs')
+ .select('*')
+ .eq('merchant_id', merchantId)
+ .order('created_at', { ascending: false })
+ .limit(1)
+ .maybeSingle();
+
+ // If there are no rows yet, data will be null and error will be undefined with maybeSingle()
+ if (error) {
+ console.error('Error fetching latest MINLP run:', error);
+ return null;
+ }
+
+ return data;
+}
+
+// Realtime subscriptions
+export function subscribeToOrders(
+ merchantId: string,
+ callback: (payload: any) => void
+) {
+ return supabase
+ .channel(`orders:${merchantId}`)
+ .on(
+ 'postgres_changes',
+ {
+ event: '*',
+ schema: 'public',
+ table: 'orders',
+ filter: `merchant_id=eq.${merchantId}`
+ },
+ callback
+ )
+ .subscribe();
+}
+
+export function subscribeToMinlpRuns(
+ merchantId: string,
+ callback: (payload: any) => void
+) {
+ return supabase
+ .channel(`minlp_runs:${merchantId}`)
+ .on(
+ 'postgres_changes',
+ {
+ event: '*',
+ schema: 'public',
+ table: 'minlp_runs',
+ filter: `merchant_id=eq.${merchantId}`
+ },
+ callback
+ )
+ .subscribe();
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index 2550716..a6f0dac 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,9 +1,22 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
import { ReadonlyURLSearchParams } from 'next/navigation';
-export const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
- ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+export const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
+ ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: 'http://localhost:3000';
+export function formatPrice(amount: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount);
+}
+
export const createUrl = (
pathname: string,
params: URLSearchParams | ReadonlyURLSearchParams
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 0000000..012ec01
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,38 @@
+import type { NextRequest } from 'next/server';
+import { NextResponse } from 'next/server';
+
+// Middleware for protecting certain routes
+export function middleware(req: NextRequest) {
+ // If in demo mode, bypass auth for protected routes
+ if (process.env.DEMO_MODE === 'true') {
+ return NextResponse.next();
+ }
+
+ const { pathname, search } = req.nextUrl;
+
+ const isProtected = (
+ pathname.startsWith('/merchant') ||
+ pathname.startsWith('/api/minlp')
+ );
+
+ if (!isProtected) {
+ return NextResponse.next();
+ }
+
+ // Default cookie set by auth provider
+ const hasSession = Boolean(req.cookies.get('appSession'));
+ if (!hasSession) {
+ const loginUrl = new URL('/api/auth/login', req.url);
+ // Always send users to the dashboard after login
+ loginUrl.searchParams.set('returnTo', '/dashboard');
+ // Force Google connection for the gimmick flow
+ loginUrl.searchParams.set('connection', 'google-oauth2');
+ return NextResponse.redirect(loginUrl);
+ }
+
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: ['/merchant/:path*', '/api/minlp/:path*']
+};
diff --git a/next.config.ts b/next.config.mjs
similarity index 73%
rename from next.config.ts
rename to next.config.mjs
index 4af2a87..08fafaa 100644
--- a/next.config.ts
+++ b/next.config.mjs
@@ -1,9 +1,4 @@
export default {
- experimental: {
- ppr: true,
- inlineCss: true,
- useCache: true
- },
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
diff --git a/package.json b/package.json
index d0ca7fa..300f7e4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"prettier": "prettier --write --ignore-unknown .",
@@ -11,20 +11,25 @@
"dependencies": {
"@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.2.0",
+ "@supabase/supabase-js": "^2.57.4",
+ "@zxing/browser": "^0.1.5",
+ "@zxing/library": "^0.21.3",
"clsx": "^2.1.1",
+ "dotenv": "^17.2.2",
"geist": "^1.3.1",
- "next": "15.3.0-canary.13",
- "react": "19.0.0",
- "react-dom": "19.0.0",
- "sonner": "^2.0.1"
+ "next": "^14.2.3",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "sonner": "^2.0.1",
+ "tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/postcss": "^4.0.14",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "22.13.10",
- "@types/react": "19.0.12",
- "@types/react-dom": "19.0.4",
+ "@types/react": "^18.3.0",
+ "@types/react-dom": "^18.3.0",
"postcss": "^8.5.3",
"prettier": "3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
diff --git a/packages/shared/types.ts b/packages/shared/types.ts
new file mode 100644
index 0000000..24386dc
--- /dev/null
+++ b/packages/shared/types.ts
@@ -0,0 +1,66 @@
+export interface Product {
+ product_id: string;
+ merchant_id: string;
+ sku: string;
+ name: string;
+ price_cents: number;
+ barcode?: string;
+ shopify_id?: string;
+ shopify_handle?: string;
+ image_url?: string;
+ created_at: string;
+}
+
+export interface Order {
+ order_id: string;
+ merchant_id: string;
+ subtotal_cents: number;
+ tax_cents: number;
+ total_cents: number;
+ created_at: string;
+ status: 'pending' | 'paid' | 'confirmed_demo' | 'shipped' | 'delivered';
+}
+
+export interface OrderItem {
+ id: string;
+ order_id: string;
+ sku: string;
+ qty: number;
+ price_cents: number;
+ created_at: string;
+}
+
+export interface MinlpRun {
+ run_id: string;
+ merchant_id: string;
+ order_id?: string;
+ input_json: any;
+ solution_json?: any;
+ rationale_text?: string;
+ created_at: string;
+}
+
+export interface CartItem {
+ product: Product;
+ quantity: number;
+}
+
+export interface MinlpSolution {
+ optimal_cost: number;
+ assignments: Array<{
+ sku: string;
+ supplier: string;
+ quantity: number;
+ cost: number;
+ }>;
+ kpis: {
+ total_cost: number;
+ supplier_count: number;
+ avg_lead_time: number;
+ };
+}
+
+export interface MinlpExplanation {
+ bullets: string[];
+ tldr: string;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8b6ef33..4f6e726 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -10,59 +10,74 @@ importers:
dependencies:
'@headlessui/react':
specifier: ^2.2.0
- version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ version: 2.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@heroicons/react':
specifier: ^2.2.0
- version: 2.2.0(react@19.0.0)
+ version: 2.2.0(react@18.3.1)
+ '@supabase/supabase-js':
+ specifier: ^2.57.4
+ version: 2.57.4
+ '@zxing/browser':
+ specifier: ^0.1.5
+ version: 0.1.5(@zxing/library@0.21.3)
+ '@zxing/library':
+ specifier: ^0.21.3
+ version: 0.21.3
clsx:
specifier: ^2.1.1
version: 2.1.1
+ dotenv:
+ specifier: ^17.2.2
+ version: 17.2.2
geist:
specifier: ^1.3.1
- version: 1.3.1(next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
+ version: 1.5.1(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
next:
- specifier: 15.3.0-canary.13
- version: 15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ specifier: ^14.2.3
+ version: 14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react:
- specifier: 19.0.0
- version: 19.0.0
+ specifier: ^18.3.1
+ version: 18.3.1
react-dom:
- specifier: 19.0.0
- version: 19.0.0(react@19.0.0)
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
sonner:
specifier: ^2.0.1
- version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ tailwind-merge:
+ specifier: ^3.3.1
+ version: 3.3.1
devDependencies:
'@tailwindcss/container-queries':
specifier: ^0.1.1
- version: 0.1.1(tailwindcss@4.0.14)
+ version: 0.1.1(tailwindcss@4.1.13)
'@tailwindcss/postcss':
specifier: ^4.0.14
- version: 4.0.14
+ version: 4.1.13
'@tailwindcss/typography':
specifier: ^0.5.16
- version: 0.5.16(tailwindcss@4.0.14)
+ version: 0.5.16(tailwindcss@4.1.13)
'@types/node':
specifier: 22.13.10
version: 22.13.10
'@types/react':
- specifier: 19.0.12
- version: 19.0.12
+ specifier: ^18.3.0
+ version: 18.3.24
'@types/react-dom':
- specifier: 19.0.4
- version: 19.0.4(@types/react@19.0.12)
+ specifier: ^18.3.0
+ version: 18.3.7(@types/react@18.3.24)
postcss:
specifier: ^8.5.3
- version: 8.5.3
+ version: 8.5.6
prettier:
specifier: 3.5.3
version: 3.5.3
prettier-plugin-tailwindcss:
specifier: ^0.6.11
- version: 0.6.11(prettier@3.5.3)
+ version: 0.6.14(prettier@3.5.3)
tailwindcss:
specifier: ^4.0.14
- version: 4.0.14
+ version: 4.1.13
typescript:
specifier: 5.8.2
version: 5.8.2
@@ -73,17 +88,14 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
- '@emnapi/runtime@1.3.1':
- resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
+ '@floating-ui/core@1.7.3':
+ resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
- '@floating-ui/core@1.6.9':
- resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
+ '@floating-ui/dom@1.7.4':
+ resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
- '@floating-ui/dom@1.6.13':
- resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
-
- '@floating-ui/react-dom@2.1.2':
- resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
+ '@floating-ui/react-dom@2.1.6':
+ resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
@@ -94,11 +106,11 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
- '@floating-ui/utils@0.2.9':
- resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
- '@headlessui/react@2.2.0':
- resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==}
+ '@headlessui/react@2.2.8':
+ resolution: {integrity: sha512-vkiZulDC0lFeTrZTbA4tHvhZHvkUb2PFh5xJ1BvWAZdRK0fayMKO1QEO4inWkXxK1i0I1rcwwu1d6mo0K7Pcbw==}
engines: {node: '>=10'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@@ -109,317 +121,300 @@ packages:
peerDependencies:
react: '>= 16 || ^19.0.0-rc'
- '@img/sharp-darwin-arm64@0.33.5':
- resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [darwin]
-
- '@img/sharp-darwin-x64@0.33.5':
- resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [darwin]
-
- '@img/sharp-libvips-darwin-arm64@1.0.4':
- resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
- cpu: [arm64]
- os: [darwin]
-
- '@img/sharp-libvips-darwin-x64@1.0.4':
- resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
- cpu: [x64]
- os: [darwin]
-
- '@img/sharp-libvips-linux-arm64@1.0.4':
- resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
- cpu: [arm64]
- os: [linux]
-
- '@img/sharp-libvips-linux-arm@1.0.5':
- resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
- cpu: [arm]
- os: [linux]
-
- '@img/sharp-libvips-linux-s390x@1.0.4':
- resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
- cpu: [s390x]
- os: [linux]
-
- '@img/sharp-libvips-linux-x64@1.0.4':
- resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
- cpu: [x64]
- os: [linux]
-
- '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
- resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
- cpu: [arm64]
- os: [linux]
-
- '@img/sharp-libvips-linuxmusl-x64@1.0.4':
- resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
- cpu: [x64]
- os: [linux]
-
- '@img/sharp-linux-arm64@0.33.5':
- resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [linux]
-
- '@img/sharp-linux-arm@0.33.5':
- resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm]
- os: [linux]
-
- '@img/sharp-linux-s390x@0.33.5':
- resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [s390x]
- os: [linux]
+ '@isaacs/fs-minipass@4.0.1':
+ resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
+ engines: {node: '>=18.0.0'}
- '@img/sharp-linux-x64@0.33.5':
- resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [linux]
-
- '@img/sharp-linuxmusl-arm64@0.33.5':
- resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [linux]
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
- '@img/sharp-linuxmusl-x64@0.33.5':
- resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [linux]
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
- '@img/sharp-wasm32@0.33.5':
- resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [wasm32]
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
- '@img/sharp-win32-ia32@0.33.5':
- resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [ia32]
- os: [win32]
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
- '@img/sharp-win32-x64@0.33.5':
- resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [win32]
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
- '@next/env@15.3.0-canary.13':
- resolution: {integrity: sha512-JSc7jRSVdstjZ0bfxKMFeYM+gVRgUbPpGSWq9JLDQDH/mYHMN+LMNR8CafQCKjoSL7tzkBpH9Ug6r9WaIescCw==}
+ '@next/env@14.2.32':
+ resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==}
- '@next/swc-darwin-arm64@15.3.0-canary.13':
- resolution: {integrity: sha512-A1EiOZHBTFF3Asyb+h4R0/IuOFEx+HN/0ek9BwR7g4neqZunAMU0LaGeExhxX7eUDJR4NWV16HEQq6nBcJB/UA==}
+ '@next/swc-darwin-arm64@14.2.32':
+ resolution: {integrity: sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@15.3.0-canary.13':
- resolution: {integrity: sha512-ojmJVrcv571Q893G0EZGgnYJOGjxYTYSvrNiXMaY2gz9W8p1G+wY/Fc6f2Vm5c2GQcjUdmJOb57x3Ujdxi3szw==}
+ '@next/swc-darwin-x64@14.2.32':
+ resolution: {integrity: sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@15.3.0-canary.13':
- resolution: {integrity: sha512-k4dEOZZ9x8PtHH8HtD/3h/epDBRqWOf13UOE3JY/NH60pY5t4uXG3JEj9tcKnezhv0/Q5eT9c6WiydXdjs2YvQ==}
+ '@next/swc-linux-arm64-gnu@14.2.32':
+ resolution: {integrity: sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-musl@15.3.0-canary.13':
- resolution: {integrity: sha512-Ms7b0OF05Q2qpo90ih/cVhviNrEatVZtsobBVyoXGfWxv/gOrhXoxuzROFGNdGXRZNJ7EgUaWmO4pZGjfUhEWw==}
+ '@next/swc-linux-arm64-musl@14.2.32':
+ resolution: {integrity: sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-x64-gnu@15.3.0-canary.13':
- resolution: {integrity: sha512-id/4NWejJpglZiY/PLpV0H675bITfo0QrUNjZtRuKfphJNkPoRGsMXdaZ3mSpFscTqofyaINQ3fis0D4sSmJUw==}
+ '@next/swc-linux-x64-gnu@14.2.32':
+ resolution: {integrity: sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-musl@15.3.0-canary.13':
- resolution: {integrity: sha512-9eE2E6KN01yxwE9H2fWaQA6PRvfjuY+lvadGBpub/pf710kdWFe9VYb8zECT492Vw90axHmktFZDTXuf2WaVTA==}
+ '@next/swc-linux-x64-musl@14.2.32':
+ resolution: {integrity: sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-win32-arm64-msvc@15.3.0-canary.13':
- resolution: {integrity: sha512-PbJ/yFCUBxhLr6wKoaC+CQebzeaiqrYOJXEMb9O1XFWp2te8okLjF2BihSziFVLtoA4m2one56pG5jU7W9GUzg==}
+ '@next/swc-win32-arm64-msvc@14.2.32':
+ resolution: {integrity: sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@15.3.0-canary.13':
- resolution: {integrity: sha512-6dUpH6huWVS0uBObUWBTolu/lZIP99oD1TdgjGt3S2te+OjXAlza8ERgR8mGTV04hpRZFv7tUivISaGlkYE+Bw==}
+ '@next/swc-win32-ia32-msvc@14.2.32':
+ resolution: {integrity: sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@next/swc-win32-x64-msvc@14.2.32':
+ resolution: {integrity: sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
- '@react-aria/focus@3.20.1':
- resolution: {integrity: sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==}
+ '@react-aria/focus@3.21.1':
+ resolution: {integrity: sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- '@react-aria/interactions@3.24.1':
- resolution: {integrity: sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==}
+ '@react-aria/interactions@3.25.5':
+ resolution: {integrity: sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- '@react-aria/ssr@3.9.7':
- resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==}
+ '@react-aria/ssr@3.9.10':
+ resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==}
engines: {node: '>= 12'}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- '@react-aria/utils@3.28.1':
- resolution: {integrity: sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==}
+ '@react-aria/utils@3.30.1':
+ resolution: {integrity: sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- '@react-stately/flags@3.1.0':
- resolution: {integrity: sha512-KSHOCxTFpBtxhIRcKwsD1YDTaNxFtCYuAUb0KEihc16QwqZViq4hasgPBs2gYm7fHRbw7WYzWKf6ZSo/+YsFlg==}
+ '@react-stately/flags@3.1.2':
+ resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==}
- '@react-stately/utils@3.10.5':
- resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==}
+ '@react-stately/utils@3.10.8':
+ resolution: {integrity: sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- '@react-types/shared@3.28.0':
- resolution: {integrity: sha512-9oMEYIDc3sk0G5rysnYvdNrkSg7B04yTKl50HHSZVbokeHpnU0yRmsDaWb9B/5RprcKj8XszEk5guBO8Sa/Q+Q==}
+ '@react-types/shared@3.32.0':
+ resolution: {integrity: sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+ '@supabase/auth-js@2.71.1':
+ resolution: {integrity: sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==}
+
+ '@supabase/functions-js@2.4.6':
+ resolution: {integrity: sha512-bhjZ7rmxAibjgmzTmQBxJU6ZIBCCJTc3Uwgvdi4FewueUTAGO5hxZT1Sj6tiD+0dSXf9XI87BDdJrg12z8Uaew==}
+
+ '@supabase/node-fetch@2.6.15':
+ resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
+ engines: {node: 4.x || >=6.0.0}
+
+ '@supabase/postgrest-js@1.21.4':
+ resolution: {integrity: sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==}
+
+ '@supabase/realtime-js@2.15.5':
+ resolution: {integrity: sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==}
+
+ '@supabase/storage-js@2.12.1':
+ resolution: {integrity: sha512-QWg3HV6Db2J81VQx0PqLq0JDBn4Q8B1FYn1kYcbla8+d5WDmTdwwMr+EJAxNOSs9W4mhKMv+EYCpCrTFlTj4VQ==}
+
+ '@supabase/supabase-js@2.57.4':
+ resolution: {integrity: sha512-LcbTzFhHYdwfQ7TRPfol0z04rLEyHabpGYANME6wkQ/kLtKNmI+Vy+WEM8HxeOZAtByUFxoUTTLwhXmrh+CcVw==}
+
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
- '@swc/helpers@0.5.15':
- resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@swc/helpers@0.5.17':
+ resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
+
+ '@swc/helpers@0.5.5':
+ resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
'@tailwindcss/container-queries@0.1.1':
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
peerDependencies:
tailwindcss: '>=3.2.0'
- '@tailwindcss/node@4.0.14':
- resolution: {integrity: sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==}
+ '@tailwindcss/node@4.1.13':
+ resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==}
- '@tailwindcss/oxide-android-arm64@4.0.14':
- resolution: {integrity: sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==}
+ '@tailwindcss/oxide-android-arm64@4.1.13':
+ resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
- '@tailwindcss/oxide-darwin-arm64@4.0.14':
- resolution: {integrity: sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==}
+ '@tailwindcss/oxide-darwin-arm64@4.1.13':
+ resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@tailwindcss/oxide-darwin-x64@4.0.14':
- resolution: {integrity: sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==}
+ '@tailwindcss/oxide-darwin-x64@4.1.13':
+ resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@tailwindcss/oxide-freebsd-x64@4.0.14':
- resolution: {integrity: sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==}
+ '@tailwindcss/oxide-freebsd-x64@4.1.13':
+ resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.14':
- resolution: {integrity: sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==}
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13':
+ resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
- '@tailwindcss/oxide-linux-arm64-gnu@4.0.14':
- resolution: {integrity: sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==}
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.13':
+ resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@tailwindcss/oxide-linux-arm64-musl@4.0.14':
- resolution: {integrity: sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==}
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.13':
+ resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@tailwindcss/oxide-linux-x64-gnu@4.0.14':
- resolution: {integrity: sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==}
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.13':
+ resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@tailwindcss/oxide-linux-x64-musl@4.0.14':
- resolution: {integrity: sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==}
+ '@tailwindcss/oxide-linux-x64-musl@4.1.13':
+ resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@tailwindcss/oxide-win32-arm64-msvc@4.0.14':
- resolution: {integrity: sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==}
+ '@tailwindcss/oxide-wasm32-wasi@4.1.13':
+ resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.13':
+ resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@tailwindcss/oxide-win32-x64-msvc@4.0.14':
- resolution: {integrity: sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==}
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.13':
+ resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
- '@tailwindcss/oxide@4.0.14':
- resolution: {integrity: sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==}
+ '@tailwindcss/oxide@4.1.13':
+ resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==}
engines: {node: '>= 10'}
- '@tailwindcss/postcss@4.0.14':
- resolution: {integrity: sha512-+uIR6KtKhla1XeIanF27KtrfYy+PX+R679v5LxbkmEZlhQe3g8rk+wKj7Xgt++rWGRuFLGMXY80Ek8JNn+kN/g==}
+ '@tailwindcss/postcss@4.1.13':
+ resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==}
'@tailwindcss/typography@0.5.16':
resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
- '@tanstack/react-virtual@3.13.4':
- resolution: {integrity: sha512-jPWC3BXvVLHsMX67NEHpJaZ+/FySoNxFfBEiF4GBc1+/nVwdRm+UcSCYnKP3pXQr0eEsDpXi/PQZhNfJNopH0g==}
+ '@tanstack/react-virtual@3.13.12':
+ resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- '@tanstack/virtual-core@3.13.4':
- resolution: {integrity: sha512-fNGO9fjjSLns87tlcto106enQQLycCKR4DPNpgq3djP5IdcPFdPAmaKjsgzIeRhH7hWrELgW12hYnRthS5kLUw==}
+ '@tanstack/virtual-core@3.13.12':
+ resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
'@types/node@22.13.10':
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
- '@types/react-dom@19.0.4':
- resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
+ '@types/phoenix@1.6.6':
+ resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
+
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
+ '@types/react-dom@18.3.7':
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
peerDependencies:
- '@types/react': ^19.0.0
+ '@types/react': ^18.0.0
+
+ '@types/react@18.3.24':
+ resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==}
- '@types/react@19.0.12':
- resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==}
+ '@types/ws@8.18.1':
+ resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+
+ '@zxing/browser@0.1.5':
+ resolution: {integrity: sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==}
+ peerDependencies:
+ '@zxing/library': ^0.21.0
+
+ '@zxing/library@0.21.3':
+ resolution: {integrity: sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==}
+ engines: {node: '>= 10.4.0'}
+
+ '@zxing/text-encoding@0.9.0':
+ resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
- caniuse-lite@1.0.30001706:
- resolution: {integrity: sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==}
+ caniuse-lite@1.0.30001741:
+ resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==}
+
+ chownr@3.0.0:
+ resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
+ engines: {node: '>=18'}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
@@ -428,20 +423,6 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
- color-convert@2.0.1:
- resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
- engines: {node: '>=7.0.0'}
-
- color-name@1.1.4:
- resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
-
- color-string@1.9.1:
- resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
-
- color@4.2.3:
- resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
- engines: {node: '>=12.5.0'}
-
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -450,91 +431,95 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
- detect-libc@2.0.3:
- resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
+ detect-libc@2.0.4:
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
- enhanced-resolve@5.18.1:
- resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
+ dotenv@17.2.2:
+ resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==}
+ engines: {node: '>=12'}
+
+ enhanced-resolve@5.18.3:
+ resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
- geist@1.3.1:
- resolution: {integrity: sha512-Q4gC1pBVPN+D579pBaz0TRRnGA4p9UK6elDY/xizXdFk/g4EKR5g0I+4p/Kj6gM0SajDBZ/0FvDV9ey9ud7BWw==}
+ geist@1.5.1:
+ resolution: {integrity: sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==}
peerDependencies:
next: '>=13.2.0'
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- is-arrayish@0.3.2:
- resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
-
- jiti@2.4.2:
- resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+ jiti@2.5.1:
+ resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
hasBin: true
- lightningcss-darwin-arm64@1.29.2:
- resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ lightningcss-darwin-arm64@1.30.1:
+ resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
- lightningcss-darwin-x64@1.29.2:
- resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
+ lightningcss-darwin-x64@1.30.1:
+ resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
- lightningcss-freebsd-x64@1.29.2:
- resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
+ lightningcss-freebsd-x64@1.30.1:
+ resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
- lightningcss-linux-arm-gnueabihf@1.29.2:
- resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
+ lightningcss-linux-arm-gnueabihf@1.30.1:
+ resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
- lightningcss-linux-arm64-gnu@1.29.2:
- resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
+ lightningcss-linux-arm64-gnu@1.30.1:
+ resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- lightningcss-linux-arm64-musl@1.29.2:
- resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
+ lightningcss-linux-arm64-musl@1.30.1:
+ resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- lightningcss-linux-x64-gnu@1.29.2:
- resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
+ lightningcss-linux-x64-gnu@1.30.1:
+ resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- lightningcss-linux-x64-musl@1.29.2:
- resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
+ lightningcss-linux-x64-musl@1.30.1:
+ resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- lightningcss-win32-arm64-msvc@1.29.2:
- resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
+ lightningcss-win32-arm64-msvc@1.30.1:
+ resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
- lightningcss-win32-x64-msvc@1.29.2:
- resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
+ lightningcss-win32-x64-msvc@1.30.1:
+ resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
- lightningcss@1.29.2:
- resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
+ lightningcss@1.30.1:
+ resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
lodash.castarray@4.4.0:
@@ -546,29 +531,46 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ magic-string@0.30.19:
+ resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
+
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minizlib@3.0.2:
+ resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
+ engines: {node: '>= 18'}
+
+ mkdirp@3.0.1:
+ resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
+ engines: {node: '>=10'}
+ hasBin: true
+
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
- next@15.3.0-canary.13:
- resolution: {integrity: sha512-c8BO/c1FjV/jY4OmlBTKaeI0YYDIsakkmJQFgpjq9RzoBetoi/VLAloZMDpsrfSFIhHDHhraLMxzSvS6mFKeuA==}
- engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+ next@14.2.32:
+ resolution: {integrity: sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==}
+ engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.41.2
- babel-plugin-react-compiler: '*'
- react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
- react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react: ^18.2.0
+ react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@playwright/test':
optional: true
- babel-plugin-react-compiler:
- optional: true
sass:
optional: true
@@ -583,15 +585,17 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.3:
- resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
- prettier-plugin-tailwindcss@0.6.11:
- resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==}
+ prettier-plugin-tailwindcss@0.6.14:
+ resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==}
engines: {node: '>=14.21.3'}
peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*'
+ '@prettier/plugin-hermes': '*'
+ '@prettier/plugin-oxc': '*'
'@prettier/plugin-pug': '*'
'@shopify/prettier-plugin-liquid': '*'
'@trivago/prettier-plugin-sort-imports': '*'
@@ -611,6 +615,10 @@ packages:
peerDependenciesMeta:
'@ianvs/prettier-plugin-sort-imports':
optional: true
+ '@prettier/plugin-hermes':
+ optional: true
+ '@prettier/plugin-oxc':
+ optional: true
'@prettier/plugin-pug':
optional: true
'@shopify/prettier-plugin-liquid':
@@ -647,32 +655,20 @@ packages:
engines: {node: '>=14'}
hasBin: true
- react-dom@19.0.0:
- resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==}
+ react-dom@18.3.1:
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
- react: ^19.0.0
+ react: ^18.3.1
- react@19.0.0:
- resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
+ react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
- scheduler@0.25.0:
- resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
-
- semver@7.7.1:
- resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
- engines: {node: '>=10'}
- hasBin: true
-
- sharp@0.33.5:
- resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-
- simple-swizzle@0.2.2:
- resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
+ scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
- sonner@2.0.1:
- resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
@@ -685,13 +681,13 @@ packages:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
- styled-jsx@5.1.6:
- resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
+ styled-jsx@5.1.1:
+ resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
peerDependencies:
'@babel/core': '*'
babel-plugin-macros: '*'
- react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
peerDependenciesMeta:
'@babel/core':
optional: true
@@ -701,13 +697,27 @@ packages:
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
- tailwindcss@4.0.14:
- resolution: {integrity: sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==}
+ tailwind-merge@3.3.1:
+ resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
- tapable@2.2.1:
- resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+ tailwindcss@4.1.13:
+ resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==}
+
+ tapable@2.2.3:
+ resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
engines: {node: '>=6'}
+ tar@7.4.3:
+ resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
+ engines: {node: '>=18'}
+
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+ ts-custom-error@3.3.1:
+ resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==}
+ engines: {node: '>=14.0.0'}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -719,402 +729,440 @@ packages:
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
+ use-sync-external-store@1.5.0:
+ resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+ ws@8.18.3:
+ resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ yallist@5.0.0:
+ resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
+ engines: {node: '>=18'}
+
snapshots:
'@alloc/quick-lru@5.2.0': {}
- '@emnapi/runtime@1.3.1':
+ '@floating-ui/core@1.7.3':
dependencies:
- tslib: 2.8.1
- optional: true
+ '@floating-ui/utils': 0.2.10
- '@floating-ui/core@1.6.9':
+ '@floating-ui/dom@1.7.4':
dependencies:
- '@floating-ui/utils': 0.2.9
+ '@floating-ui/core': 1.7.3
+ '@floating-ui/utils': 0.2.10
- '@floating-ui/dom@1.6.13':
+ '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@floating-ui/core': 1.6.9
- '@floating-ui/utils': 0.2.9
+ '@floating-ui/dom': 1.7.4
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
- '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@floating-ui/dom': 1.6.13
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
-
- '@floating-ui/react@0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
- dependencies:
- '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- '@floating-ui/utils': 0.2.9
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
+ '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@floating-ui/utils': 0.2.10
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
tabbable: 6.2.0
- '@floating-ui/utils@0.2.9': {}
+ '@floating-ui/utils@0.2.10': {}
- '@headlessui/react@2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@headlessui/react@2.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@floating-ui/react': 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- '@react-aria/focus': 3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- '@tanstack/react-virtual': 3.13.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
-
- '@heroicons/react@2.2.0(react@19.0.0)':
+ '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@react-aria/focus': 3.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@react-aria/interactions': 3.25.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ use-sync-external-store: 1.5.0(react@18.3.1)
+
+ '@heroicons/react@2.2.0(react@18.3.1)':
dependencies:
- react: 19.0.0
+ react: 18.3.1
- '@img/sharp-darwin-arm64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.0.4
- optional: true
+ '@isaacs/fs-minipass@4.0.1':
+ dependencies:
+ minipass: 7.1.2
- '@img/sharp-darwin-x64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.0.4
- optional: true
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
- '@img/sharp-libvips-darwin-arm64@1.0.4':
- optional: true
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
- '@img/sharp-libvips-darwin-x64@1.0.4':
- optional: true
+ '@jridgewell/resolve-uri@3.1.2': {}
- '@img/sharp-libvips-linux-arm64@1.0.4':
- optional: true
+ '@jridgewell/sourcemap-codec@1.5.5': {}
- '@img/sharp-libvips-linux-arm@1.0.5':
- optional: true
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
- '@img/sharp-libvips-linux-s390x@1.0.4':
- optional: true
+ '@next/env@14.2.32': {}
- '@img/sharp-libvips-linux-x64@1.0.4':
+ '@next/swc-darwin-arm64@14.2.32':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
+ '@next/swc-darwin-x64@14.2.32':
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.0.4':
+ '@next/swc-linux-arm64-gnu@14.2.32':
optional: true
- '@img/sharp-linux-arm64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.0.4
+ '@next/swc-linux-arm64-musl@14.2.32':
optional: true
- '@img/sharp-linux-arm@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.0.5
+ '@next/swc-linux-x64-gnu@14.2.32':
optional: true
- '@img/sharp-linux-s390x@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.0.4
+ '@next/swc-linux-x64-musl@14.2.32':
optional: true
- '@img/sharp-linux-x64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.0.4
+ '@next/swc-win32-arm64-msvc@14.2.32':
optional: true
- '@img/sharp-linuxmusl-arm64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
+ '@next/swc-win32-ia32-msvc@14.2.32':
optional: true
- '@img/sharp-linuxmusl-x64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.0.4
+ '@next/swc-win32-x64-msvc@14.2.32':
optional: true
- '@img/sharp-wasm32@0.33.5':
+ '@react-aria/focus@3.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@emnapi/runtime': 1.3.1
- optional: true
-
- '@img/sharp-win32-ia32@0.33.5':
- optional: true
-
- '@img/sharp-win32-x64@0.33.5':
- optional: true
-
- '@next/env@15.3.0-canary.13': {}
-
- '@next/swc-darwin-arm64@15.3.0-canary.13':
- optional: true
-
- '@next/swc-darwin-x64@15.3.0-canary.13':
- optional: true
-
- '@next/swc-linux-arm64-gnu@15.3.0-canary.13':
- optional: true
+ '@react-aria/interactions': 3.25.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@react-aria/utils': 3.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@react-types/shared': 3.32.0(react@18.3.1)
+ '@swc/helpers': 0.5.17
+ clsx: 2.1.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
- '@next/swc-linux-arm64-musl@15.3.0-canary.13':
- optional: true
+ '@react-aria/interactions@3.25.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@react-aria/ssr': 3.9.10(react@18.3.1)
+ '@react-aria/utils': 3.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@react-stately/flags': 3.1.2
+ '@react-types/shared': 3.32.0(react@18.3.1)
+ '@swc/helpers': 0.5.17
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@react-aria/ssr@3.9.10(react@18.3.1)':
+ dependencies:
+ '@swc/helpers': 0.5.17
+ react: 18.3.1
- '@next/swc-linux-x64-gnu@15.3.0-canary.13':
- optional: true
+ '@react-aria/utils@3.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@react-aria/ssr': 3.9.10(react@18.3.1)
+ '@react-stately/flags': 3.1.2
+ '@react-stately/utils': 3.10.8(react@18.3.1)
+ '@react-types/shared': 3.32.0(react@18.3.1)
+ '@swc/helpers': 0.5.17
+ clsx: 2.1.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
- '@next/swc-linux-x64-musl@15.3.0-canary.13':
- optional: true
+ '@react-stately/flags@3.1.2':
+ dependencies:
+ '@swc/helpers': 0.5.17
- '@next/swc-win32-arm64-msvc@15.3.0-canary.13':
- optional: true
+ '@react-stately/utils@3.10.8(react@18.3.1)':
+ dependencies:
+ '@swc/helpers': 0.5.17
+ react: 18.3.1
- '@next/swc-win32-x64-msvc@15.3.0-canary.13':
- optional: true
+ '@react-types/shared@3.32.0(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
- '@react-aria/focus@3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@supabase/auth-js@2.71.1':
dependencies:
- '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- '@react-types/shared': 3.28.0(react@19.0.0)
- '@swc/helpers': 0.5.15
- clsx: 2.1.1
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
+ '@supabase/node-fetch': 2.6.15
- '@react-aria/interactions@3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@supabase/functions-js@2.4.6':
dependencies:
- '@react-aria/ssr': 3.9.7(react@19.0.0)
- '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- '@react-stately/flags': 3.1.0
- '@react-types/shared': 3.28.0(react@19.0.0)
- '@swc/helpers': 0.5.15
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
-
- '@react-aria/ssr@3.9.7(react@19.0.0)':
+ '@supabase/node-fetch': 2.6.15
+
+ '@supabase/node-fetch@2.6.15':
dependencies:
- '@swc/helpers': 0.5.15
- react: 19.0.0
+ whatwg-url: 5.0.0
- '@react-aria/utils@3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@supabase/postgrest-js@1.21.4':
dependencies:
- '@react-aria/ssr': 3.9.7(react@19.0.0)
- '@react-stately/flags': 3.1.0
- '@react-stately/utils': 3.10.5(react@19.0.0)
- '@react-types/shared': 3.28.0(react@19.0.0)
- '@swc/helpers': 0.5.15
- clsx: 2.1.1
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
+ '@supabase/node-fetch': 2.6.15
- '@react-stately/flags@3.1.0':
+ '@supabase/realtime-js@2.15.5':
dependencies:
- '@swc/helpers': 0.5.15
+ '@supabase/node-fetch': 2.6.15
+ '@types/phoenix': 1.6.6
+ '@types/ws': 8.18.1
+ ws: 8.18.3
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
- '@react-stately/utils@3.10.5(react@19.0.0)':
+ '@supabase/storage-js@2.12.1':
dependencies:
- '@swc/helpers': 0.5.15
- react: 19.0.0
+ '@supabase/node-fetch': 2.6.15
- '@react-types/shared@3.28.0(react@19.0.0)':
+ '@supabase/supabase-js@2.57.4':
dependencies:
- react: 19.0.0
+ '@supabase/auth-js': 2.71.1
+ '@supabase/functions-js': 2.4.6
+ '@supabase/node-fetch': 2.6.15
+ '@supabase/postgrest-js': 1.21.4
+ '@supabase/realtime-js': 2.15.5
+ '@supabase/storage-js': 2.12.1
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
'@swc/counter@0.1.3': {}
- '@swc/helpers@0.5.15':
+ '@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
- '@tailwindcss/container-queries@0.1.1(tailwindcss@4.0.14)':
+ '@swc/helpers@0.5.5':
dependencies:
- tailwindcss: 4.0.14
+ '@swc/counter': 0.1.3
+ tslib: 2.8.1
- '@tailwindcss/node@4.0.14':
+ '@tailwindcss/container-queries@0.1.1(tailwindcss@4.1.13)':
dependencies:
- enhanced-resolve: 5.18.1
- jiti: 2.4.2
- tailwindcss: 4.0.14
+ tailwindcss: 4.1.13
- '@tailwindcss/oxide-android-arm64@4.0.14':
+ '@tailwindcss/node@4.1.13':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.18.3
+ jiti: 2.5.1
+ lightningcss: 1.30.1
+ magic-string: 0.30.19
+ source-map-js: 1.2.1
+ tailwindcss: 4.1.13
+
+ '@tailwindcss/oxide-android-arm64@4.1.13':
optional: true
- '@tailwindcss/oxide-darwin-arm64@4.0.14':
+ '@tailwindcss/oxide-darwin-arm64@4.1.13':
optional: true
- '@tailwindcss/oxide-darwin-x64@4.0.14':
+ '@tailwindcss/oxide-darwin-x64@4.1.13':
optional: true
- '@tailwindcss/oxide-freebsd-x64@4.0.14':
+ '@tailwindcss/oxide-freebsd-x64@4.1.13':
optional: true
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.14':
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13':
optional: true
- '@tailwindcss/oxide-linux-arm64-gnu@4.0.14':
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.13':
optional: true
- '@tailwindcss/oxide-linux-arm64-musl@4.0.14':
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.13':
optional: true
- '@tailwindcss/oxide-linux-x64-gnu@4.0.14':
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.13':
optional: true
- '@tailwindcss/oxide-linux-x64-musl@4.0.14':
+ '@tailwindcss/oxide-linux-x64-musl@4.1.13':
optional: true
- '@tailwindcss/oxide-win32-arm64-msvc@4.0.14':
+ '@tailwindcss/oxide-wasm32-wasi@4.1.13':
optional: true
- '@tailwindcss/oxide-win32-x64-msvc@4.0.14':
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.13':
optional: true
- '@tailwindcss/oxide@4.0.14':
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.13':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.13':
+ dependencies:
+ detect-libc: 2.0.4
+ tar: 7.4.3
optionalDependencies:
- '@tailwindcss/oxide-android-arm64': 4.0.14
- '@tailwindcss/oxide-darwin-arm64': 4.0.14
- '@tailwindcss/oxide-darwin-x64': 4.0.14
- '@tailwindcss/oxide-freebsd-x64': 4.0.14
- '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.14
- '@tailwindcss/oxide-linux-arm64-gnu': 4.0.14
- '@tailwindcss/oxide-linux-arm64-musl': 4.0.14
- '@tailwindcss/oxide-linux-x64-gnu': 4.0.14
- '@tailwindcss/oxide-linux-x64-musl': 4.0.14
- '@tailwindcss/oxide-win32-arm64-msvc': 4.0.14
- '@tailwindcss/oxide-win32-x64-msvc': 4.0.14
-
- '@tailwindcss/postcss@4.0.14':
+ '@tailwindcss/oxide-android-arm64': 4.1.13
+ '@tailwindcss/oxide-darwin-arm64': 4.1.13
+ '@tailwindcss/oxide-darwin-x64': 4.1.13
+ '@tailwindcss/oxide-freebsd-x64': 4.1.13
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.13
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.13
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.13
+ '@tailwindcss/oxide-wasm32-wasi': 4.1.13
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.13
+
+ '@tailwindcss/postcss@4.1.13':
dependencies:
'@alloc/quick-lru': 5.2.0
- '@tailwindcss/node': 4.0.14
- '@tailwindcss/oxide': 4.0.14
- lightningcss: 1.29.2
- postcss: 8.5.3
- tailwindcss: 4.0.14
+ '@tailwindcss/node': 4.1.13
+ '@tailwindcss/oxide': 4.1.13
+ postcss: 8.5.6
+ tailwindcss: 4.1.13
- '@tailwindcss/typography@0.5.16(tailwindcss@4.0.14)':
+ '@tailwindcss/typography@0.5.16(tailwindcss@4.1.13)':
dependencies:
lodash.castarray: 4.4.0
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
- tailwindcss: 4.0.14
+ tailwindcss: 4.1.13
- '@tanstack/react-virtual@3.13.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@tanstack/virtual-core': 3.13.4
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
+ '@tanstack/virtual-core': 3.13.12
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
- '@tanstack/virtual-core@3.13.4': {}
+ '@tanstack/virtual-core@3.13.12': {}
'@types/node@22.13.10':
dependencies:
undici-types: 6.20.0
- '@types/react-dom@19.0.4(@types/react@19.0.12)':
+ '@types/phoenix@1.6.6': {}
+
+ '@types/prop-types@15.7.15': {}
+
+ '@types/react-dom@18.3.7(@types/react@18.3.24)':
dependencies:
- '@types/react': 19.0.12
+ '@types/react': 18.3.24
- '@types/react@19.0.12':
+ '@types/react@18.3.24':
dependencies:
+ '@types/prop-types': 15.7.15
csstype: 3.1.3
- busboy@1.6.0:
+ '@types/ws@8.18.1':
dependencies:
- streamsearch: 1.1.0
-
- caniuse-lite@1.0.30001706: {}
+ '@types/node': 22.13.10
- client-only@0.0.1: {}
-
- clsx@2.1.1: {}
+ '@zxing/browser@0.1.5(@zxing/library@0.21.3)':
+ dependencies:
+ '@zxing/library': 0.21.3
+ optionalDependencies:
+ '@zxing/text-encoding': 0.9.0
- color-convert@2.0.1:
+ '@zxing/library@0.21.3':
dependencies:
- color-name: 1.1.4
- optional: true
+ ts-custom-error: 3.3.1
+ optionalDependencies:
+ '@zxing/text-encoding': 0.9.0
- color-name@1.1.4:
+ '@zxing/text-encoding@0.9.0':
optional: true
- color-string@1.9.1:
+ busboy@1.6.0:
dependencies:
- color-name: 1.1.4
- simple-swizzle: 0.2.2
- optional: true
+ streamsearch: 1.1.0
- color@4.2.3:
- dependencies:
- color-convert: 2.0.1
- color-string: 1.9.1
- optional: true
+ caniuse-lite@1.0.30001741: {}
+
+ chownr@3.0.0: {}
+
+ client-only@0.0.1: {}
+
+ clsx@2.1.1: {}
cssesc@3.0.0: {}
csstype@3.1.3: {}
- detect-libc@2.0.3: {}
+ detect-libc@2.0.4: {}
+
+ dotenv@17.2.2: {}
- enhanced-resolve@5.18.1:
+ enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
- tapable: 2.2.1
+ tapable: 2.2.3
- geist@1.3.1(next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)):
+ geist@1.5.1(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
dependencies:
- next: 15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ next: 14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
graceful-fs@4.2.11: {}
- is-arrayish@0.3.2:
- optional: true
+ jiti@2.5.1: {}
- jiti@2.4.2: {}
+ js-tokens@4.0.0: {}
- lightningcss-darwin-arm64@1.29.2:
+ lightningcss-darwin-arm64@1.30.1:
optional: true
- lightningcss-darwin-x64@1.29.2:
+ lightningcss-darwin-x64@1.30.1:
optional: true
- lightningcss-freebsd-x64@1.29.2:
+ lightningcss-freebsd-x64@1.30.1:
optional: true
- lightningcss-linux-arm-gnueabihf@1.29.2:
+ lightningcss-linux-arm-gnueabihf@1.30.1:
optional: true
- lightningcss-linux-arm64-gnu@1.29.2:
+ lightningcss-linux-arm64-gnu@1.30.1:
optional: true
- lightningcss-linux-arm64-musl@1.29.2:
+ lightningcss-linux-arm64-musl@1.30.1:
optional: true
- lightningcss-linux-x64-gnu@1.29.2:
+ lightningcss-linux-x64-gnu@1.30.1:
optional: true
- lightningcss-linux-x64-musl@1.29.2:
+ lightningcss-linux-x64-musl@1.30.1:
optional: true
- lightningcss-win32-arm64-msvc@1.29.2:
+ lightningcss-win32-arm64-msvc@1.30.1:
optional: true
- lightningcss-win32-x64-msvc@1.29.2:
+ lightningcss-win32-x64-msvc@1.30.1:
optional: true
- lightningcss@1.29.2:
+ lightningcss@1.30.1:
dependencies:
- detect-libc: 2.0.3
+ detect-libc: 2.0.4
optionalDependencies:
- lightningcss-darwin-arm64: 1.29.2
- lightningcss-darwin-x64: 1.29.2
- lightningcss-freebsd-x64: 1.29.2
- lightningcss-linux-arm-gnueabihf: 1.29.2
- lightningcss-linux-arm64-gnu: 1.29.2
- lightningcss-linux-arm64-musl: 1.29.2
- lightningcss-linux-x64-gnu: 1.29.2
- lightningcss-linux-x64-musl: 1.29.2
- lightningcss-win32-arm64-msvc: 1.29.2
- lightningcss-win32-x64-msvc: 1.29.2
+ lightningcss-darwin-arm64: 1.30.1
+ lightningcss-darwin-x64: 1.30.1
+ lightningcss-freebsd-x64: 1.30.1
+ lightningcss-linux-arm-gnueabihf: 1.30.1
+ lightningcss-linux-arm64-gnu: 1.30.1
+ lightningcss-linux-arm64-musl: 1.30.1
+ lightningcss-linux-x64-gnu: 1.30.1
+ lightningcss-linux-x64-musl: 1.30.1
+ lightningcss-win32-arm64-msvc: 1.30.1
+ lightningcss-win32-x64-msvc: 1.30.1
lodash.castarray@4.4.0: {}
@@ -1122,29 +1170,45 @@ snapshots:
lodash.merge@4.6.2: {}
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ magic-string@0.30.19:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ minipass@7.1.2: {}
+
+ minizlib@3.0.2:
+ dependencies:
+ minipass: 7.1.2
+
+ mkdirp@3.0.1: {}
+
nanoid@3.3.11: {}
- next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- '@next/env': 15.3.0-canary.13
- '@swc/counter': 0.1.3
- '@swc/helpers': 0.5.15
+ '@next/env': 14.2.32
+ '@swc/helpers': 0.5.5
busboy: 1.6.0
- caniuse-lite: 1.0.30001706
+ caniuse-lite: 1.0.30001741
+ graceful-fs: 4.2.11
postcss: 8.4.31
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
- styled-jsx: 5.1.6(react@19.0.0)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ styled-jsx: 5.1.1(react@18.3.1)
optionalDependencies:
- '@next/swc-darwin-arm64': 15.3.0-canary.13
- '@next/swc-darwin-x64': 15.3.0-canary.13
- '@next/swc-linux-arm64-gnu': 15.3.0-canary.13
- '@next/swc-linux-arm64-musl': 15.3.0-canary.13
- '@next/swc-linux-x64-gnu': 15.3.0-canary.13
- '@next/swc-linux-x64-musl': 15.3.0-canary.13
- '@next/swc-win32-arm64-msvc': 15.3.0-canary.13
- '@next/swc-win32-x64-msvc': 15.3.0-canary.13
- sharp: 0.33.5
+ '@next/swc-darwin-arm64': 14.2.32
+ '@next/swc-darwin-x64': 14.2.32
+ '@next/swc-linux-arm64-gnu': 14.2.32
+ '@next/swc-linux-arm64-musl': 14.2.32
+ '@next/swc-linux-x64-gnu': 14.2.32
+ '@next/swc-linux-x64-musl': 14.2.32
+ '@next/swc-win32-arm64-msvc': 14.2.32
+ '@next/swc-win32-ia32-msvc': 14.2.32
+ '@next/swc-win32-x64-msvc': 14.2.32
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -1162,81 +1226,66 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- postcss@8.5.3:
+ postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
- prettier-plugin-tailwindcss@0.6.11(prettier@3.5.3):
+ prettier-plugin-tailwindcss@0.6.14(prettier@3.5.3):
dependencies:
prettier: 3.5.3
prettier@3.5.3: {}
- react-dom@19.0.0(react@19.0.0):
+ react-dom@18.3.1(react@18.3.1):
dependencies:
- react: 19.0.0
- scheduler: 0.25.0
-
- react@19.0.0: {}
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
- scheduler@0.25.0: {}
-
- semver@7.7.1:
- optional: true
-
- sharp@0.33.5:
+ react@18.3.1:
dependencies:
- color: 4.2.3
- detect-libc: 2.0.3
- semver: 7.7.1
- optionalDependencies:
- '@img/sharp-darwin-arm64': 0.33.5
- '@img/sharp-darwin-x64': 0.33.5
- '@img/sharp-libvips-darwin-arm64': 1.0.4
- '@img/sharp-libvips-darwin-x64': 1.0.4
- '@img/sharp-libvips-linux-arm': 1.0.5
- '@img/sharp-libvips-linux-arm64': 1.0.4
- '@img/sharp-libvips-linux-s390x': 1.0.4
- '@img/sharp-libvips-linux-x64': 1.0.4
- '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
- '@img/sharp-libvips-linuxmusl-x64': 1.0.4
- '@img/sharp-linux-arm': 0.33.5
- '@img/sharp-linux-arm64': 0.33.5
- '@img/sharp-linux-s390x': 0.33.5
- '@img/sharp-linux-x64': 0.33.5
- '@img/sharp-linuxmusl-arm64': 0.33.5
- '@img/sharp-linuxmusl-x64': 0.33.5
- '@img/sharp-wasm32': 0.33.5
- '@img/sharp-win32-ia32': 0.33.5
- '@img/sharp-win32-x64': 0.33.5
- optional: true
+ loose-envify: 1.4.0
- simple-swizzle@0.2.2:
+ scheduler@0.23.2:
dependencies:
- is-arrayish: 0.3.2
- optional: true
+ loose-envify: 1.4.0
- sonner@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
source-map-js@1.2.1: {}
streamsearch@1.1.0: {}
- styled-jsx@5.1.6(react@19.0.0):
+ styled-jsx@5.1.1(react@18.3.1):
dependencies:
client-only: 0.0.1
- react: 19.0.0
+ react: 18.3.1
tabbable@6.2.0: {}
- tailwindcss@4.0.14: {}
+ tailwind-merge@3.3.1: {}
+
+ tailwindcss@4.1.13: {}
- tapable@2.2.1: {}
+ tapable@2.2.3: {}
+
+ tar@7.4.3:
+ dependencies:
+ '@isaacs/fs-minipass': 4.0.1
+ chownr: 3.0.0
+ minipass: 7.1.2
+ minizlib: 3.0.2
+ mkdirp: 3.0.1
+ yallist: 5.0.0
+
+ tr46@0.0.3: {}
+
+ ts-custom-error@3.3.1: {}
tslib@2.8.1: {}
@@ -1244,4 +1293,19 @@ snapshots:
undici-types@6.20.0: {}
+ use-sync-external-store@1.5.0(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
util-deprecate@1.0.2: {}
+
+ webidl-conversions@3.0.1: {}
+
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
+ ws@8.18.3: {}
+
+ yallist@5.0.0: {}
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000..377efe7
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,43 @@
+{
+ "name": "Tarazoo - Unified Commerce Platform",
+ "short_name": "Tarazoo",
+ "description": "Mobile-first commerce platform with barcode scanning and merchant dashboard",
+ "theme_color": "#2563eb",
+ "background_color": "#ffffff",
+ "display": "standalone",
+ "scope": "/",
+ "start_url": "/",
+ "orientation": "portrait",
+ "icons": [
+ {
+ "src": "/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ],
+ "categories": ["shopping", "business"],
+ "shortcuts": [
+ {
+ "name": "Scan Product",
+ "url": "/?scan=true",
+ "description": "Open barcode scanner"
+ },
+ {
+ "name": "View Cart",
+ "url": "/cart",
+ "description": "View shopping cart"
+ },
+ {
+ "name": "Dashboard",
+ "url": "/dashboard",
+ "description": "Merchant dashboard"
+ }
+ ]
+}
diff --git a/scripts/add-google-bottle.js b/scripts/add-google-bottle.js
new file mode 100644
index 0000000..bc3dafd
--- /dev/null
+++ b/scripts/add-google-bottle.js
@@ -0,0 +1,38 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '.env' });
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
+
+const supabase = createClient(supabaseUrl, supabaseServiceKey);
+
+async function addGoogleCloudBottle() {
+ const product = {
+ product_id: 'google-cloud-bottle',
+ merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
+ sku: 'SKU-GCLOUD',
+ name: 'Google Cloud Bottle',
+ price_cents: 2499,
+ barcode: '0000000000558',
+ shopify_id: 'google-cloud-bottle',
+ shopify_handle: 'google-cloud-bottle',
+ image_url: 'https://via.placeholder.com/600x600/4285F4/ffffff?text=Google+Cloud+Bottle'
+ };
+
+ const { data, error } = await supabase
+ .from('products')
+ .upsert(product, {
+ onConflict: 'product_id'
+ })
+ .select();
+
+ if (error) {
+ console.error('Error adding product:', error);
+ } else {
+ console.log('β
Added Google Cloud Bottle');
+ console.log(' Barcode: 0000000000558');
+ console.log(' Price: $24.99');
+ }
+}
+
+addGoogleCloudBottle();
diff --git a/scripts/setup-supabase.js b/scripts/setup-supabase.js
new file mode 100644
index 0000000..3dc434c
--- /dev/null
+++ b/scripts/setup-supabase.js
@@ -0,0 +1,109 @@
+const { createClient } = require('@supabase/supabase-js');
+const fs = require('fs');
+const path = require('path');
+
+// Load environment variables
+require('dotenv').config({ path: '.env' });
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
+
+if (!supabaseUrl || !supabaseServiceKey) {
+ console.error('Missing Supabase credentials in .env file');
+ process.exit(1);
+}
+
+const supabase = createClient(supabaseUrl, supabaseServiceKey);
+
+async function runMigrations() {
+ console.log('π Setting up Supabase database...\n');
+
+ try {
+ // Read migration files
+ const schemaSQL = fs.readFileSync(
+ path.join(__dirname, '../supabase/migrations/001_initial_schema.sql'),
+ 'utf8'
+ );
+ const seedSQL = fs.readFileSync(
+ path.join(__dirname, '../supabase/migrations/002_seed_data.sql'),
+ 'utf8'
+ );
+
+ // Run schema migration
+ console.log('π Creating database schema...');
+ const { error: schemaError } = await supabase.rpc('exec_sql', {
+ sql: schemaSQL
+ });
+
+ if (schemaError) {
+ // Try direct approach for schema
+ console.log('Using alternative method for schema creation...');
+ // Tables will be created via Supabase dashboard SQL editor
+ }
+
+ // Check if products table exists and insert seed data
+ console.log('π± Seeding products with barcodes...');
+
+ const products = [
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU001', name: 'Organic Coffee Beans 1kg', price_cents: 2499, barcode: '1234567890123' },
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU002', name: 'Premium Dark Chocolate 200g', price_cents: 899, barcode: '2345678901234' },
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU003', name: 'Artisan Sourdough Bread', price_cents: 599, barcode: '3456789012345' },
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU004', name: 'Organic Almond Butter 500g', price_cents: 1299, barcode: '4567890123456' },
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU005', name: 'Free Range Eggs (Dozen)', price_cents: 799, barcode: '5678901234567' },
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU006', name: 'Greek Yogurt 1L', price_cents: 699, barcode: '6789012345678' },
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU007', name: 'Honey Raw 500ml', price_cents: 1499, barcode: '7890123456789' },
+ { merchant_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', sku: 'SKU008', name: 'Olive Oil Extra Virgin 1L', price_cents: 1899, barcode: '8901234567890' }
+ ];
+
+ const { data, error: seedError } = await supabase
+ .from('products')
+ .upsert(products, {
+ onConflict: 'sku',
+ ignoreDuplicates: true
+ })
+ .select();
+
+ if (seedError) {
+ console.error('Seed error:', seedError);
+ console.log('Please run the SQL migrations directly in Supabase dashboard');
+ } else {
+ console.log(`β
Successfully seeded ${data?.length || 0} products with barcodes`);
+ }
+
+ // Test the connection
+ console.log('\nπ Testing database connection...');
+ const { data: testData, error: testError } = await supabase
+ .from('products')
+ .select('*')
+ .limit(3);
+
+ if (testError) {
+ console.error('Test query error:', testError);
+ console.log('\nβ οΈ Please ensure tables are created by running the SQL in Supabase dashboard:');
+ console.log('1. Go to https://supabase.com/dashboard/project/moxhnbwoqidwoeijsudc/sql/new');
+ console.log('2. Copy and paste the contents of /supabase/migrations/001_initial_schema.sql');
+ console.log('3. Run the query');
+ console.log('4. Then paste and run /supabase/migrations/002_seed_data.sql');
+ } else {
+ console.log('β
Connection successful!');
+ console.log(`π¦ Found ${testData?.length || 0} products in database`);
+ if (testData && testData.length > 0) {
+ console.log('\nSample products with barcodes:');
+ testData.forEach(p => {
+ console.log(` - ${p.name}: Barcode ${p.barcode}`);
+ });
+ }
+ }
+
+ console.log('\nπ Supabase setup complete!');
+ console.log('Your app is now connected to Supabase at:', supabaseUrl);
+
+ } catch (error) {
+ console.error('Setup error:', error);
+ console.log('\nπ Manual setup required:');
+ console.log('1. Go to your Supabase SQL editor');
+ console.log('2. Run the migrations from /supabase/migrations/');
+ }
+}
+
+runMigrations();
diff --git a/scripts/test-checkout.js b/scripts/test-checkout.js
new file mode 100644
index 0000000..edef66d
--- /dev/null
+++ b/scripts/test-checkout.js
@@ -0,0 +1,85 @@
+/*
+ Simulate a checkout and verify Supabase is updated.
+ Uses the same schema as the appβs createOrder.
+*/
+
+require('dotenv').config();
+const { createClient } = require('@supabase/supabase-js');
+
+async function main() {
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
+ const merchantId = process.env.MERCHANT_ID || process.env.NEXT_PUBLIC_MERCHANT_ID_DEFAULT || 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+
+ if (!supabaseUrl || !supabaseAnonKey) {
+ throw new Error('Missing Supabase env vars. Ensure NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are set.');
+ }
+
+ const supabase = createClient(supabaseUrl, supabaseAnonKey);
+
+ // Pick a product to order
+ const { data: product, error: prodErr } = await supabase
+ .from('products')
+ .select('*')
+ .limit(1)
+ .single();
+
+ if (prodErr || !product) {
+ throw new Error(`No product found to test checkout. Error: ${prodErr ? prodErr.message : 'none'}`);
+ }
+
+ const quantity = 1;
+ const subtotal = product.price_cents * quantity;
+ const tax = Math.round(subtotal * 0.13);
+ const totalBeforeDiscount = subtotal + tax;
+
+ // Insert order (zero total, 100% discount)
+ const { data: order, error: orderErr } = await supabase
+ .from('orders')
+ .insert({
+ merchant_id: merchantId,
+ subtotal_cents: subtotal,
+ tax_cents: tax,
+ total_cents: 0,
+ status: 'confirmed_demo',
+ discount_cents: totalBeforeDiscount,
+ discount_label: 'Hack The North Developer Discount'
+ })
+ .select()
+ .single();
+
+ if (orderErr || !order) {
+ throw new Error(`Failed to create order: ${orderErr ? orderErr.message : 'unknown error'}`);
+ }
+
+ // Insert order_items
+ const { error: itemsErr } = await supabase
+ .from('order_items')
+ .insert([
+ {
+ order_id: order.order_id,
+ sku: product.sku,
+ qty: quantity,
+ price_cents: product.price_cents
+ }
+ ]);
+
+ if (itemsErr) {
+ throw new Error(`Failed to create order items: ${itemsErr.message}`);
+ }
+
+ // Verify order values
+ if (order.total_cents !== 0 || order.discount_cents !== totalBeforeDiscount) {
+ throw new Error('Order amounts did not match expected zero-total with full discount.');
+ }
+
+ console.log('OK: Created discounted order');
+ console.log(`Order ID: ${order.order_id}`);
+ console.log(`Subtotal: ${subtotal} | Tax: ${tax} | Discount: ${totalBeforeDiscount} | Total: 0`);
+}
+
+main().catch((e) => {
+ console.error('Test checkout failed:', e.message || e);
+ process.exit(1);
+});
+
diff --git a/scripts/test-shopify-connection.js b/scripts/test-shopify-connection.js
new file mode 100644
index 0000000..4cb6b2e
--- /dev/null
+++ b/scripts/test-shopify-connection.js
@@ -0,0 +1,93 @@
+// Test Shopify connection
+require('dotenv').config({ path: '.env' });
+
+const SHOPIFY_STORE_DOMAIN = process.env.SHOPIFY_STORE_DOMAIN;
+const SHOPIFY_STOREFRONT_ACCESS_TOKEN = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN;
+
+async function testShopifyConnection() {
+ if (!SHOPIFY_STORE_DOMAIN || !SHOPIFY_STOREFRONT_ACCESS_TOKEN) {
+ console.log('β Shopify credentials not configured');
+ console.log('\nTo connect to Shopify:');
+ console.log('1. Go to your Shopify Admin');
+ console.log('2. Settings β Apps and sales channels β Develop apps');
+ console.log('3. Create an app called "Tarazoo Integration"');
+ console.log('4. Configure Storefront API access with product read permissions');
+ console.log('5. Get your Storefront Access Token');
+ console.log('6. Add to .env:');
+ console.log(' SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"');
+ console.log(' SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-token"');
+ return;
+ }
+
+ console.log('π Testing Shopify connection...');
+ console.log(`Store: ${SHOPIFY_STORE_DOMAIN}`);
+
+ const query = `
+ query {
+ products(first: 3) {
+ edges {
+ node {
+ id
+ title
+ handle
+ variants(first: 1) {
+ edges {
+ node {
+ id
+ title
+ price {
+ amount
+ currencyCode
+ }
+ barcode
+ sku
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ try {
+ const response = await fetch(`https://${SHOPIFY_STORE_DOMAIN}/api/2024-01/graphql.json`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Shopify-Storefront-Access-Token': SHOPIFY_STOREFRONT_ACCESS_TOKEN
+ },
+ body: JSON.stringify({ query })
+ });
+
+ const data = await response.json();
+
+ if (data.errors) {
+ console.log('β Shopify API error:', data.errors);
+ return;
+ }
+
+ if (data.data && data.data.products) {
+ console.log('β
Connected to Shopify successfully!');
+ console.log(`\nFound ${data.data.products.edges.length} products:`);
+
+ data.data.products.edges.forEach(({ node }) => {
+ console.log(`\nπ¦ ${node.title}`);
+ console.log(` Handle: ${node.handle}`);
+ if (node.variants.edges[0]) {
+ const variant = node.variants.edges[0].node;
+ console.log(` Price: ${variant.price.currencyCode} ${variant.price.amount}`);
+ console.log(` Barcode: ${variant.barcode || 'No barcode set'}`);
+ console.log(` SKU: ${variant.sku || 'No SKU set'}`);
+ }
+ });
+
+ console.log('\nπ‘ To add barcodes to products:');
+ console.log(' Go to Shopify Admin β Products β Edit Product β Inventory β Barcode field');
+ }
+ } catch (error) {
+ console.error('β Connection failed:', error.message);
+ }
+}
+
+testShopifyConnection();
diff --git a/services/minlp/main.py b/services/minlp/main.py
new file mode 100644
index 0000000..9679d97
--- /dev/null
+++ b/services/minlp/main.py
@@ -0,0 +1,142 @@
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from typing import List, Dict, Any, Optional
+import random
+from datetime import datetime
+
+app = FastAPI(title="MINLP Optimization Service")
+
+# Enable CORS
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+class OptimizationInput(BaseModel):
+ merchant_id: str
+ order_id: Optional[str] = None
+ items: List[Dict[str, Any]]
+
+class OptimizationSolution(BaseModel):
+ optimal_cost: float
+ assignments: List[Dict[str, Any]]
+ kpis: Dict[str, Any]
+
+class ExplanationRequest(BaseModel):
+ solution: Dict[str, Any]
+
+class ExplanationResponse(BaseModel):
+ bullets: List[str]
+ tldr: str
+
+# Mock supplier data for demo
+SUPPLIERS = {
+ "supplier_1": {"name": "FastCo Supply", "lead_time": 2, "cost_multiplier": 1.0},
+ "supplier_2": {"name": "Budget Wholesale", "lead_time": 5, "cost_multiplier": 0.85},
+ "supplier_3": {"name": "Premium Direct", "lead_time": 1, "cost_multiplier": 1.2},
+}
+
+@app.get("/")
+def read_root():
+ return {"status": "MINLP Service Running", "version": "1.0.0"}
+
+@app.post("/solve", response_model=OptimizationSolution)
+async def solve_optimization(input_data: OptimizationInput):
+ """
+ Run MINLP optimization to minimize procurement costs while respecting constraints.
+ This is a simplified demo implementation.
+ """
+ try:
+ # Simulate optimization computation
+ items = input_data.items
+
+ # Create mock assignments (in real implementation, this would use Pyomo/PuLP)
+ assignments = []
+ total_cost = 0
+ suppliers_used = set()
+
+ for item in items:
+ # Randomly assign to supplier (in real MINLP, this would be optimized)
+ supplier_id = random.choice(list(SUPPLIERS.keys()))
+ supplier = SUPPLIERS[supplier_id]
+ suppliers_used.add(supplier_id)
+
+ # Calculate cost (simplified)
+ base_cost = item.get("price_cents", 1000) * item.get("qty", 1)
+ cost = base_cost * supplier["cost_multiplier"] / 100
+ total_cost += cost
+
+ assignments.append({
+ "sku": item.get("sku", "UNKNOWN"),
+ "supplier": supplier["name"],
+ "quantity": item.get("qty", 1),
+ "cost": cost,
+ "lead_time": supplier["lead_time"]
+ })
+
+ # Calculate KPIs
+ avg_lead_time = sum(SUPPLIERS[s]["lead_time"] for s in suppliers_used) / len(suppliers_used) if suppliers_used else 0
+
+ solution = OptimizationSolution(
+ optimal_cost=total_cost,
+ assignments=assignments,
+ kpis={
+ "total_cost": int(total_cost * 100), # Convert back to cents
+ "supplier_count": len(suppliers_used),
+ "avg_lead_time": round(avg_lead_time, 1),
+ "optimization_time_ms": random.randint(500, 2000),
+ "constraints_satisfied": True
+ }
+ )
+
+ return solution
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/explain", response_model=ExplanationResponse)
+async def explain_solution(request: ExplanationRequest):
+ """
+ Generate human-readable explanation of the optimization solution.
+ """
+ try:
+ solution = request.solution
+ kpis = solution.get("kpis", {})
+
+ # Generate explanation bullets based on solution
+ bullets = []
+
+ # Cost savings bullet
+ if kpis.get("total_cost"):
+ bullets.append(f"Optimized procurement cost to ${kpis['total_cost']/100:.2f} through strategic supplier selection")
+
+ # Supplier diversity bullet
+ if kpis.get("supplier_count"):
+ bullets.append(f"Distributed orders across {kpis['supplier_count']} suppliers to minimize risk and ensure availability")
+
+ # Lead time bullet
+ if kpis.get("avg_lead_time"):
+ bullets.append(f"Achieved average lead time of {kpis['avg_lead_time']} days while maintaining cost efficiency")
+
+ # Generate TL;DR
+ tldr = f"MINLP optimization saved 15% on costs using {kpis.get('supplier_count', 2)} suppliers with {kpis.get('avg_lead_time', 3)}-day delivery"
+
+ return ExplanationResponse(
+ bullets=bullets[:3], # Ensure exactly 3 bullets
+ tldr=tldr
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.get("/health")
+def health_check():
+ return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/services/minlp/requirements.txt b/services/minlp/requirements.txt
new file mode 100644
index 0000000..c007f43
--- /dev/null
+++ b/services/minlp/requirements.txt
@@ -0,0 +1,4 @@
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+pydantic==2.5.0
+python-multipart==0.0.6
diff --git a/supabase/fix-rls.sql b/supabase/fix-rls.sql
new file mode 100644
index 0000000..16b14d3
--- /dev/null
+++ b/supabase/fix-rls.sql
@@ -0,0 +1,20 @@
+-- Fix RLS policies to allow product sync
+-- Run this in Supabase SQL editor
+
+-- Drop existing policies
+DROP POLICY IF EXISTS "Products are viewable by everyone" ON products;
+
+-- Create new policies that allow both SELECT and INSERT
+CREATE POLICY "Products public read" ON products
+ FOR SELECT USING (true);
+
+CREATE POLICY "Products public insert" ON products
+ FOR INSERT WITH CHECK (true);
+
+CREATE POLICY "Products public update" ON products
+ FOR UPDATE USING (true) WITH CHECK (true);
+
+-- Verify policies
+SELECT tablename, policyname, permissive, roles, cmd
+FROM pg_policies
+WHERE tablename = 'products';
diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql
new file mode 100644
index 0000000..cb07b48
--- /dev/null
+++ b/supabase/migrations/001_initial_schema.sql
@@ -0,0 +1,82 @@
+-- Create products table
+CREATE TABLE IF NOT EXISTS products (
+ product_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL,
+ sku VARCHAR(100) UNIQUE NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
+ barcode VARCHAR(100) UNIQUE,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Create orders table
+CREATE TABLE IF NOT EXISTS orders (
+ order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL,
+ subtotal_cents INTEGER NOT NULL CHECK (subtotal_cents >= 0),
+ tax_cents INTEGER NOT NULL CHECK (tax_cents >= 0),
+ total_cents INTEGER NOT NULL CHECK (total_cents >= 0),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ status VARCHAR(50) DEFAULT 'pending'
+);
+
+-- Create order_items table
+CREATE TABLE IF NOT EXISTS order_items (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ order_id UUID NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
+ sku VARCHAR(100) NOT NULL,
+ qty INTEGER NOT NULL CHECK (qty > 0),
+ price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Create minlp_runs table
+CREATE TABLE IF NOT EXISTS minlp_runs (
+ run_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL,
+ order_id UUID REFERENCES orders(order_id),
+ input_json JSONB NOT NULL,
+ solution_json JSONB,
+ rationale_text TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Create indexes for performance
+CREATE INDEX idx_products_merchant_id ON products(merchant_id);
+CREATE INDEX idx_products_barcode ON products(barcode);
+CREATE INDEX idx_orders_merchant_id ON orders(merchant_id);
+CREATE INDEX idx_orders_status ON orders(status);
+CREATE INDEX idx_order_items_order_id ON order_items(order_id);
+CREATE INDEX idx_minlp_runs_merchant_id ON minlp_runs(merchant_id);
+
+-- Enable Row Level Security
+ALTER TABLE products ENABLE ROW LEVEL SECURITY;
+ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
+ALTER TABLE order_items ENABLE ROW LEVEL SECURITY;
+ALTER TABLE minlp_runs ENABLE ROW LEVEL SECURITY;
+
+-- Create RLS policies (open for demo, tighten for production)
+CREATE POLICY "Products are viewable by everyone" ON products
+ FOR SELECT USING (true);
+
+CREATE POLICY "Orders are viewable by everyone" ON orders
+ FOR SELECT USING (true);
+
+CREATE POLICY "Orders can be created by everyone" ON orders
+ FOR INSERT WITH CHECK (true);
+
+CREATE POLICY "Order items are viewable by everyone" ON order_items
+ FOR SELECT USING (true);
+
+CREATE POLICY "Order items can be created by everyone" ON order_items
+ FOR INSERT WITH CHECK (true);
+
+CREATE POLICY "MINLP runs are viewable by everyone" ON minlp_runs
+ FOR SELECT USING (true);
+
+CREATE POLICY "MINLP runs can be created by everyone" ON minlp_runs
+ FOR INSERT WITH CHECK (true);
+
+-- Enable Realtime
+ALTER PUBLICATION supabase_realtime ADD TABLE orders;
+ALTER PUBLICATION supabase_realtime ADD TABLE minlp_runs;
diff --git a/supabase/migrations/002_seed_data.sql b/supabase/migrations/002_seed_data.sql
new file mode 100644
index 0000000..c2f1af5
--- /dev/null
+++ b/supabase/migrations/002_seed_data.sql
@@ -0,0 +1,11 @@
+-- Seed merchant data
+INSERT INTO products (merchant_id, sku, name, price_cents, barcode) VALUES
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU001', 'Organic Coffee Beans 1kg', 2499, '1234567890123'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU002', 'Premium Dark Chocolate 200g', 899, '2345678901234'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU003', 'Artisan Sourdough Bread', 599, '3456789012345'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU004', 'Organic Almond Butter 500g', 1299, '4567890123456'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU005', 'Free Range Eggs (Dozen)', 799, '5678901234567'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU006', 'Greek Yogurt 1L', 699, '6789012345678'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU007', 'Honey Raw 500ml', 1499, '7890123456789'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU008', 'Olive Oil Extra Virgin 1L', 1899, '8901234567890')
+ON CONFLICT (sku) DO NOTHING;
diff --git a/supabase/migrations/003_shopify_integration.sql b/supabase/migrations/003_shopify_integration.sql
new file mode 100644
index 0000000..3b18e2c
--- /dev/null
+++ b/supabase/migrations/003_shopify_integration.sql
@@ -0,0 +1,13 @@
+-- Add Shopify integration fields to products table
+ALTER TABLE products
+ADD COLUMN IF NOT EXISTS shopify_id TEXT,
+ADD COLUMN IF NOT EXISTS shopify_handle TEXT,
+ADD COLUMN IF NOT EXISTS image_url TEXT;
+
+-- Update product_id to be text to support Shopify IDs
+ALTER TABLE products
+ALTER COLUMN product_id TYPE TEXT USING product_id::TEXT;
+
+-- Create index for Shopify lookups
+CREATE INDEX IF NOT EXISTS idx_products_shopify_id ON products(shopify_id);
+CREATE INDEX IF NOT EXISTS idx_products_shopify_handle ON products(shopify_handle);
diff --git a/supabase/new-update.sql b/supabase/new-update.sql
new file mode 100644
index 0000000..c2f3d64
--- /dev/null
+++ b/supabase/new-update.sql
@@ -0,0 +1,9 @@
+-- Add discount tracking for demo zero-dollar invoice
+-- Run this in your Supabase SQL Editor
+
+ALTER TABLE orders
+ ADD COLUMN IF NOT EXISTS discount_cents INTEGER NOT NULL DEFAULT 0,
+ ADD COLUMN IF NOT EXISTS discount_label TEXT;
+
+-- Optional: index for querying discounted orders
+CREATE INDEX IF NOT EXISTS idx_orders_discount_cents ON orders(discount_cents);
diff --git a/supabase/setup-all.sql b/supabase/setup-all.sql
new file mode 100644
index 0000000..1115866
--- /dev/null
+++ b/supabase/setup-all.sql
@@ -0,0 +1,102 @@
+-- TARAZOO DATABASE SETUP
+-- Run this entire file in Supabase SQL Editor
+
+-- Create products table
+CREATE TABLE IF NOT EXISTS products (
+ product_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL,
+ sku VARCHAR(100) UNIQUE NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
+ barcode VARCHAR(100) UNIQUE,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Create orders tablea
+CREATE TABLE IF NOT EXISTS orders (
+ order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL,
+ subtotal_cents INTEGER NOT NULL CHECK (subtotal_cents >= 0),
+ tax_cents INTEGER NOT NULL CHECK (tax_cents >= 0),
+ total_cents INTEGER NOT NULL CHECK (total_cents >= 0),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ status VARCHAR(50) DEFAULT 'pending'
+);
+
+-- Create order_items table
+CREATE TABLE IF NOT EXISTS order_items (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ order_id UUID NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
+ sku VARCHAR(100) NOT NULL,
+ qty INTEGER NOT NULL CHECK (qty > 0),
+ price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Create minlp_runs table
+CREATE TABLE IF NOT EXISTS minlp_runs (
+ run_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL,
+ order_id UUID REFERENCES orders(order_id),
+ input_json JSONB NOT NULL,
+ solution_json JSONB,
+ rationale_text TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Create indexes for performance
+CREATE INDEX IF NOT EXISTS idx_products_merchant_id ON products(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_products_barcode ON products(barcode);
+CREATE INDEX IF NOT EXISTS idx_orders_merchant_id ON orders(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
+CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
+CREATE INDEX IF NOT EXISTS idx_minlp_runs_merchant_id ON minlp_runs(merchant_id);
+
+-- Enable Row Level Security
+ALTER TABLE products ENABLE ROW LEVEL SECURITY;
+ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
+ALTER TABLE order_items ENABLE ROW LEVEL SECURITY;
+ALTER TABLE minlp_runs ENABLE ROW LEVEL SECURITY;
+
+-- Create RLS policies (open for demo, tighten for production)
+CREATE POLICY "Products are viewable by everyone" ON products
+ FOR SELECT USING (true);
+
+CREATE POLICY "Orders are viewable by everyone" ON orders
+ FOR SELECT USING (true);
+
+CREATE POLICY "Orders can be created by everyone" ON orders
+ FOR INSERT WITH CHECK (true);
+
+CREATE POLICY "Order items are viewable by everyone" ON order_items
+ FOR SELECT USING (true);
+
+CREATE POLICY "Order items can be created by everyone" ON order_items
+ FOR INSERT WITH CHECK (true);
+
+CREATE POLICY "MINLP runs are viewable by everyone" ON minlp_runs
+ FOR SELECT USING (true);
+
+CREATE POLICY "MINLP runs can be created by everyone" ON minlp_runs
+ FOR INSERT WITH CHECK (true);
+
+-- Enable Realtime
+ALTER PUBLICATION supabase_realtime ADD TABLE orders;
+ALTER PUBLICATION supabase_realtime ADD TABLE minlp_runs;
+
+-- Insert seed products WITH BARCODES
+INSERT INTO products (merchant_id, sku, name, price_cents, barcode) VALUES
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU001', 'Organic Coffee Beans 1kg', 2499, '1234567890123'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU002', 'Premium Dark Chocolate 200g', 899, '2345678901234'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU003', 'Artisan Sourdough Bread', 599, '3456789012345'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU004', 'Organic Almond Butter 500g', 1299, '4567890123456'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU005', 'Free Range Eggs (Dozen)', 799, '5678901234567'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU006', 'Greek Yogurt 1L', 699, '6789012345678'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU007', 'Honey Raw 500ml', 1499, '7890123456789'),
+ ('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'SKU008', 'Olive Oil Extra Virgin 1L', 1899, '8901234567890')
+ON CONFLICT (sku) DO NOTHING;
+
+-- Verify setup
+SELECT 'Setup Complete!' as status;
+SELECT COUNT(*) as product_count FROM products;
+SELECT name, barcode, price_cents FROM products LIMIT 3;
diff --git a/supabase/shopify-setup.sql b/supabase/shopify-setup.sql
new file mode 100644
index 0000000..0a8087a
--- /dev/null
+++ b/supabase/shopify-setup.sql
@@ -0,0 +1,26 @@
+-- Update products table for Shopify integration
+-- Run this in your Supabase SQL editor
+
+-- First, modify the product_id column to support text (for Shopify IDs)
+ALTER TABLE products DROP CONSTRAINT IF EXISTS products_pkey CASCADE;
+ALTER TABLE products ALTER COLUMN product_id TYPE TEXT USING product_id::TEXT;
+ALTER TABLE products ADD PRIMARY KEY (product_id);
+
+-- Add Shopify-specific columns
+ALTER TABLE products
+ADD COLUMN IF NOT EXISTS shopify_id TEXT,
+ADD COLUMN IF NOT EXISTS shopify_handle TEXT,
+ADD COLUMN IF NOT EXISTS image_url TEXT;
+
+-- Create indexes for better performance
+CREATE INDEX IF NOT EXISTS idx_products_shopify_id ON products(shopify_id);
+CREATE INDEX IF NOT EXISTS idx_products_shopify_handle ON products(shopify_handle);
+
+-- Clear old demo data (optional - comment out if you want to keep it)
+-- DELETE FROM products WHERE merchant_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+
+-- Verify the schema update
+SELECT column_name, data_type
+FROM information_schema.columns
+WHERE table_name = 'products'
+ORDER BY ordinal_position;