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)} +

    +
    + +
    + + + +
    + +
    +
    +
    +
    +
  • + ))} +
+
+ + {/* 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

+ +
+
+

Contact information

+
+ + +
+
+ +
+

Shipping information

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+

Payment (Demo Mode)

+
+

+ This is a demo checkout. No payment will be processed. +

+
+
+ + {error && ( +
+

{error}

+
+ )} + +
+ +
+
+
+ + {/* 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

+ +
+ + {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 +

+
+
+ + + + {showExplanation && latestMinlpRun.rationale_text && ( +
+

{latestMinlpRun.rationale_text}

+
+ )} +
+ )} +

+ Run ID: {latestMinlpRun.run_id} β€’ {new Date(latestMinlpRun.created_at).toLocaleString()} +

+
+ )} +
+ + {/* Recent Orders */} +
+
+

Recent Orders (Live Feed)

+
+
+ + + + + + + + + + + {orders.slice(0, 10).map((order) => ( + + + + + + + ))} + +
+ Order ID + + Status + + Total + + Date +
+ {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 && ( + + )} + +
+ + {/* Manual name input fallback */} +
+
+ setManualName(e.target.value)} + placeholder="Enter item name (e.g., pen)" + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+
+
+ ); +} 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 ( -
{ + await formAction(selectedVariantId); addCartItem(finalVariant, product); - addItemAction(); }} >

diff --git a/components/cart/supabase-cart-context.tsx b/components/cart/supabase-cart-context.tsx new file mode 100644 index 0000000..52c5d1b --- /dev/null +++ b/components/cart/supabase-cart-context.tsx @@ -0,0 +1,113 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect } from 'react'; +import type { Product, CartItem } from '../../packages/shared/types'; + +const TAX_RATE = 0.13; // 13% tax + +interface CartContextType { + items: CartItem[]; + addItem: (product: Product) => void; + removeItem: (productId: string) => void; + updateQuantity: (productId: string, quantity: number) => void; + clearCart: () => void; + subtotal: number; + tax: number; + total: number; + itemCount: number; +} + +const CartContext = createContext(undefined); + +export function CartProvider({ children }: { children: React.ReactNode }) { + const [items, setItems] = useState([]); + + // Load cart from localStorage on mount + useEffect(() => { + const savedCart = localStorage.getItem('tarazoo_cart'); + if (savedCart) { + try { + setItems(JSON.parse(savedCart)); + } catch (e) { + console.error('Failed to load cart from storage'); + } + } + }, []); + + // Save cart to localStorage whenever it changes + useEffect(() => { + localStorage.setItem('tarazoo_cart', JSON.stringify(items)); + }, [items]); + + const addItem = (product: Product) => { + setItems(prevItems => { + const existingItem = prevItems.find(item => item.product.product_id === product.product_id); + + if (existingItem) { + return prevItems.map(item => + item.product.product_id === product.product_id + ? { ...item, quantity: item.quantity + 1 } + : item + ); + } + + return [...prevItems, { product, quantity: 1 }]; + }); + }; + + const removeItem = (productId: string) => { + setItems(prevItems => prevItems.filter(item => item.product.product_id !== productId)); + }; + + const updateQuantity = (productId: string, quantity: number) => { + if (quantity <= 0) { + removeItem(productId); + return; + } + + setItems(prevItems => + prevItems.map(item => + item.product.product_id === productId + ? { ...item, quantity } + : item + ) + ); + }; + + const clearCart = () => { + setItems([]); + localStorage.removeItem('tarazoo_cart'); + }; + + // Calculate totals + const subtotal = items.reduce((sum, item) => sum + (item.product.price_cents * item.quantity), 0); + const tax = Math.round(subtotal * TAX_RATE); + const total = subtotal + tax; + const itemCount = items.reduce((sum, item) => sum + item.quantity, 0); + + return ( + + {children} + + ); +} + +export function useSupabaseCart() { + const context = useContext(CartContext); + if (context === undefined) { + throw new Error('useSupabaseCart must be used within a CartProvider'); + } + return context; +} diff --git a/components/dashboard/DashboardClient.tsx b/components/dashboard/DashboardClient.tsx new file mode 100644 index 0000000..a4e2db1 --- /dev/null +++ b/components/dashboard/DashboardClient.tsx @@ -0,0 +1,312 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { getOrders, subscribeToOrders, subscribeToMinlpRuns, getLatestMinlpRun } from 'lib/supabase'; +import type { Order, MinlpRun } from 'packages/shared/types'; +import { formatPrice } from 'lib/utils'; + +export function DashboardClient({ merchantId }: { merchantId: string }) { + const [orders, setOrders] = useState([]); + const [latestMinlpRun, setLatestMinlpRun] = useState(null); + const [isRunningMinlp, setIsRunningMinlp] = useState(false); + const [showExplanation, setShowExplanation] = useState(false); + + useEffect(() => { + // Load initial data + loadOrders(); + loadLatestMinlpRun(); + + // Subscribe to realtime updates + const ordersChannel = subscribeToOrders(merchantId, (payload) => { + if (payload.eventType === 'INSERT') { + setOrders((prev) => [payload.new as Order, ...prev]); + } + }); + + const minlpChannel = subscribeToMinlpRuns(merchantId, (payload) => { + if (payload.eventType === 'INSERT') { + setLatestMinlpRun(payload.new as MinlpRun); + } + }); + + return () => { + ordersChannel.unsubscribe(); + minlpChannel.unsubscribe(); + }; + }, [merchantId]); + + const loadOrders = async () => { + const data = await getOrders(merchantId); + setOrders(data); + }; + + const loadLatestMinlpRun = async () => { + const data = await getLatestMinlpRun(merchantId); + 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, + orders: orders.slice(0, 5) + }) + }); + + if (response.ok) { + 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

+
+ +
+
+
+
+
+ + + +
+
+
+
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

+ +
+ + {latestMinlpRun && ( +
+

Latest Optimization Result

+ {latestMinlpRun.solution_json && ( +
+
+
+

Optimal Cost

+

+ {formatPrice((latestMinlpRun.solution_json.kpis?.total_cost || 0) / 100)} +

+
+
+

Suppliers Used

+

+ {latestMinlpRun.solution_json.kpis?.supplier_count || 0} +

+
+
+

Avg Lead Time

+

+ {latestMinlpRun.solution_json.kpis?.avg_lead_time || 0} days +

+
+
+ + + + {showExplanation && latestMinlpRun.rationale_text && ( +
+

{latestMinlpRun.rationale_text}

+
+ )} +
+ )} +

+ Run ID: {latestMinlpRun.run_id} β€’ {new Date(latestMinlpRun.created_at).toLocaleString()} +

+
+ )} +
+ + {/* Recent Orders */} +
+
+

Recent Orders (Live Feed)

+
+
+ + + + + + + + + + + {orders.slice(0, 10).map((order) => ( + + + + + + + ))} + +
+ Order ID + + Status + + Total + + Date +
+ {order.order_id.substring(0, 8)}... + + + {order.status} + + + {formatPrice(order.total_cents / 100)} + + {new Date(order.created_at).toLocaleString()} +
+
+
+
+ ); +} + diff --git a/components/grid/three-items.tsx b/components/grid/three-items.tsx index cd63029..88bba78 100644 --- a/components/grid/three-items.tsx +++ b/components/grid/three-items.tsx @@ -1,5 +1,5 @@ import { GridTileImage } from 'components/grid/tile'; -import { getCollectionProducts } from 'lib/shopify'; +import { getCollectionProducts, getProducts } from 'lib/shopify'; import type { Product } from 'lib/shopify/types'; import Link from 'next/link'; @@ -44,18 +44,27 @@ function ThreeItemGridItem({ export async function ThreeItemGrid() { // Collections that start with `hidden-*` are hidden from the search page. const homepageItems = await getCollectionProducts({ - collection: 'hidden-homepage-featured-items' + collection: 'hidden-homepage-featured-items', + fresh: true }); - if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null; + // Fallback to general product list if the hidden collection is empty + const items = + homepageItems[0] && homepageItems[1] && homepageItems[2] + ? homepageItems + : (await getProducts({ fresh: true })).slice(0, 3); - const [firstProduct, secondProduct, thirdProduct] = homepageItems; + if (!items || items.length === 0) return null; + + const firstProduct = items[0]; + const secondProduct = items[1]; + const thirdProduct = items[2]; return (
- - - + {firstProduct && } + {secondProduct && } + {thirdProduct && }
); } diff --git a/components/layout/navbar/cart-button.tsx b/components/layout/navbar/cart-button.tsx new file mode 100644 index 0000000..79bcda2 --- /dev/null +++ b/components/layout/navbar/cart-button.tsx @@ -0,0 +1,35 @@ +'use client'; + +import Link from 'next/link'; +import { useSupabaseCart } from 'components/cart/supabase-cart-context'; + +export default function CartButton() { + const { itemCount } = useSupabaseCart(); + + return ( + + + + + {itemCount > 0 && ( + + {itemCount} + + )} + + ); +} diff --git a/components/layout/navbar/scanner-button.tsx b/components/layout/navbar/scanner-button.tsx new file mode 100644 index 0000000..0f8a39d --- /dev/null +++ b/components/layout/navbar/scanner-button.tsx @@ -0,0 +1,53 @@ +'use client'; + +import CameraScanner from 'components/camera-scanner'; +import { useSupabaseCart } from 'components/cart/supabase-cart-context'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +export default function ScannerButton() { + const [isScanning, setIsScanning] = useState(false); + const { addItem } = useSupabaseCart(); + const router = useRouter(); + + return ( + <> + + + {isScanning && ( + { + addItem(product); + const message = document.createElement('div'); + message.className = 'fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 animate-fade-in'; + message.textContent = `Added ${product.name} to cart`; + document.body.appendChild(message); + setTimeout(() => { + message.remove(); + }, 3000); + router.push('/cart'); + }} + onClose={() => setIsScanning(false)} + /> + )} + + ); +} diff --git a/components/layout/navbar/search.tsx b/components/layout/navbar/search.tsx index a590db6..0c5af10 100644 --- a/components/layout/navbar/search.tsx +++ b/components/layout/navbar/search.tsx @@ -1,14 +1,13 @@ 'use client'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import Form from 'next/form'; import { useSearchParams } from 'next/navigation'; export default function Search() { const searchParams = useSearchParams(); 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 ( + + ); +} 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

+
    +
  1. + Create a Shopify store if you don't have one: + + Start free trial β†’ + +
  2. +
  3. + 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
    • +
    +
  4. +
  5. + Get your credentials: +
      +
    • Copy your Storefront API access token
    • +
    • Note your store domain (e.g., your-store.myshopify.com)
    • +
    +
  6. +
  7. + Update your .env file with: +
    +{`SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
    +SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-token-here"
    +SHOPIFY_REVALIDATION_SECRET="your-secret"`}
    +                
    +
  8. +
  9. + Restart your development server to apply the changes +
  10. +
+
+ +
+

πŸ“š Resources

+ +
+ +
+

+ ⚠️ 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;