From be0871e467764954762cf778791709c791a58bbd Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 11:06:26 -0400 Subject: [PATCH 01/10] 11:06 update --- .env.example | 28 +- README_DEMO.md | 176 ++++++++++ app/api/minlp/explain/route.ts | 32 ++ app/api/minlp/solve/route.ts | 55 +++ app/api/sync-products/route.ts | 32 ++ app/cart/page.tsx | 152 +++++++++ app/checkout/page.tsx | 249 ++++++++++++++ app/checkout/success/page.tsx | 52 +++ app/dashboard/page.tsx | 315 ++++++++++++++++++ app/layout.tsx | 36 +- app/merchant/[merchantId]/page.tsx | 7 + app/page.tsx | 16 +- app/product/[handle]/page.tsx | 9 +- app/search/[collection]/page.tsx | 4 +- app/search/page.tsx | 4 +- components/camera-scanner.tsx | 230 +++++++++++++ components/carousel.tsx | 2 +- components/cart/supabase-cart-context.tsx | 113 +++++++ components/dashboard/DashboardClient.tsx | 312 +++++++++++++++++ components/grid/three-items.tsx | 23 +- components/layout/navbar/cart-button.tsx | 35 ++ components/layout/navbar/scanner-button.tsx | 58 ++++ components/layout/navbar/tarazoo-navbar.tsx | 65 ++++ components/providers.tsx | 13 + components/setup/shopify-setup.tsx | 98 ++++++ docs/shopify-setup.md | 35 ++ lib/shopify-supabase-sync.ts | 190 +++++++++++ lib/shopify/config.ts | 9 + lib/shopify/fragments/product.ts | 2 + lib/shopify/index.ts | 70 +++- lib/shopify/mock-data.ts | 248 ++++++++++++++ lib/supabase.ts | 226 +++++++++++++ lib/utils.ts | 17 +- middleware.ts | 33 ++ package.json | 8 +- packages/shared/types.ts | 66 ++++ pages/api/auth/[...auth0].ts | 4 + pnpm-lock.yaml | 310 +++++++++++++++++ public/manifest.json | 43 +++ scripts/add-google-bottle.js | 38 +++ scripts/setup-supabase.js | 109 ++++++ scripts/test-shopify-connection.js | 93 ++++++ services/minlp/main.py | 142 ++++++++ services/minlp/requirements.txt | 4 + supabase/fix-rls.sql | 20 ++ supabase/migrations/001_initial_schema.sql | 82 +++++ supabase/migrations/002_seed_data.sql | 11 + .../migrations/003_shopify_integration.sql | 13 + supabase/setup-all.sql | 102 ++++++ supabase/shopify-setup.sql | 26 ++ 50 files changed, 3976 insertions(+), 41 deletions(-) create mode 100644 README_DEMO.md create mode 100644 app/api/minlp/explain/route.ts create mode 100644 app/api/minlp/solve/route.ts create mode 100644 app/api/sync-products/route.ts create mode 100644 app/cart/page.tsx create mode 100644 app/checkout/page.tsx create mode 100644 app/checkout/success/page.tsx create mode 100644 app/dashboard/page.tsx create mode 100644 app/merchant/[merchantId]/page.tsx create mode 100644 components/camera-scanner.tsx create mode 100644 components/cart/supabase-cart-context.tsx create mode 100644 components/dashboard/DashboardClient.tsx create mode 100644 components/layout/navbar/cart-button.tsx create mode 100644 components/layout/navbar/scanner-button.tsx create mode 100644 components/layout/navbar/tarazoo-navbar.tsx create mode 100644 components/providers.tsx create mode 100644 components/setup/shopify-setup.tsx create mode 100644 docs/shopify-setup.md create mode 100644 lib/shopify-supabase-sync.ts create mode 100644 lib/shopify/config.ts create mode 100644 lib/shopify/mock-data.ts create mode 100644 lib/supabase.ts create mode 100644 middleware.ts create mode 100644 packages/shared/types.ts create mode 100644 pages/api/auth/[...auth0].ts create mode 100644 public/manifest.json create mode 100644 scripts/add-google-bottle.js create mode 100644 scripts/setup-supabase.js create mode 100644 scripts/test-shopify-connection.js create mode 100644 services/minlp/main.py create mode 100644 services/minlp/requirements.txt create mode 100644 supabase/fix-rls.sql create mode 100644 supabase/migrations/001_initial_schema.sql create mode 100644 supabase/migrations/002_seed_data.sql create mode 100644 supabase/migrations/003_shopify_integration.sql create mode 100644 supabase/setup-all.sql create mode 100644 supabase/shopify-setup.sql diff --git a/.env.example b/.env.example index f7fa990..594cd84 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,27 @@ -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" +NEXT_PUBLIC_MERCHANT_ID_DEFAULT="a1b2c3d4-e5f6-7890-abcd-ef1234567890" + +# Auth0 Configuration (for Merchant Dashboard) +AUTH0_SECRET="your-auth0-secret" +AUTH0_BASE_URL="https://your-app-domain.vercel.app" +AUTH0_ISSUER_BASE_URL="https://your-tenant.auth0.com" +AUTH0_CLIENT_ID="your-auth0-client-id" +AUTH0_CLIENT_SECRET="your-auth0-client-secret" + +# 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..3d9c625 --- /dev/null +++ b/app/api/minlp/explain/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { withApiAuthRequired } from '@auth0/nextjs-auth0'; + +const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000'; + +export const POST = withApiAuthRequired(async function POST(request: NextRequest) { + try { + const body = await request.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..20a95a1 --- /dev/null +++ b/app/api/minlp/solve/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createMinlpRun } from 'lib/supabase'; +import { withApiAuthRequired } from '@auth0/nextjs-auth0'; + +const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000'; + +export const POST = withApiAuthRequired(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, + null + ); + + 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/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/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..d670672 --- /dev/null +++ b/app/checkout/page.tsx @@ -0,0 +1,249 @@ +'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 [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 + const order = await createOrder( + MERCHANT_ID, + orderItems, + subtotal, + tax, + total + ); + + 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)}
+
+
+
Order total
+
{formatPrice(total / 100)}
+
+
+ +
+

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..c890db1 --- /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..72c7f0c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,20 +1,21 @@ -import { CartProvider } from 'components/cart/cart-context'; -import { Navbar } from 'components/layout/navbar'; +import { Providers } from 'components/providers'; +import { UserProvider } from '@auth0/nextjs-auth0/client'; +import { TarazooNavbar } from 'components/layout/navbar/tarazoo-navbar'; import { WelcomeToast } from 'components/welcome-toast'; import { GeistSans } from 'geist/font/sans'; -import { getCart } from 'lib/shopify'; import { ReactNode } from 'react'; import { Toaster } from 'sonner'; import './globals.css'; import { baseUrl } from 'lib/utils'; +import { getCart } from 'lib/shopify'; 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 +28,21 @@ 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} - - -
-
+ + + +
+ {children} + + +
+
+
); diff --git a/app/merchant/[merchantId]/page.tsx b/app/merchant/[merchantId]/page.tsx new file mode 100644 index 0000000..83f921a --- /dev/null +++ b/app/merchant/[merchantId]/page.tsx @@ -0,0 +1,7 @@ +import { DashboardClient } from 'components/dashboard/DashboardClient'; + +export default 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/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..1226d4c --- /dev/null +++ b/components/camera-scanner.tsx @@ -0,0 +1,230 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { BrowserMultiFormatReader } from '@zxing/browser'; +import type { Result, Exception } from '@zxing/library'; +import { getProductByBarcode } from 'lib/supabase'; +import type { Product } from '../packages/shared/types'; + +interface CameraScannerProps { + onProductScanned: (product: Product) => void; + onClose: () => void; + autoCloseOnScan?: boolean; +} + +export default function CameraScanner({ onProductScanned, onClose, autoCloseOnScan = true }: CameraScannerProps) { + const videoRef = useRef(null); + const [isScanning, setIsScanning] = useState(false); + const [error, setError] = useState(null); + const [manualBarcode, setManualBarcode] = useState(''); + const [torchEnabled, setTorchEnabled] = useState(false); + const codeReaderRef = useRef(null); + const streamRef = useRef(null); + + useEffect(() => { + startScanning(); + + return () => { + stopScanning(); + }; + }, []); + + const startScanning = async () => { + try { + setIsScanning(true); + setError(null); + + const codeReader = new BrowserMultiFormatReader(); + codeReaderRef.current = codeReader; + + 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; + + // Check for torch support + const track = stream.getVideoTracks()[0]; + if (track && (track as any).getCapabilities) { + const capabilities = (track as any).getCapabilities(); + if (capabilities && 'torch' in capabilities) { + setTorchEnabled(true); + } + } + + // Start continuous scanning + codeReader.decodeFromVideoDevice(undefined, videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { + if (result) { + await handleBarcodeDetected(result.getText()); + } + if (err && (err as any).name !== 'NotFoundException') { + console.error('Scanning error:', err); + } + }); + } + } catch (err) { + console.error('Camera error:', err); + setError('Unable to access camera. Please check permissions.'); + setIsScanning(false); + } + }; + + const stopScanning = () => { + if (codeReaderRef.current) { + (codeReaderRef.current as any).stopContinuousDecode?.(); + (codeReaderRef.current as any).reset?.(); + codeReaderRef.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 handleBarcodeDetected = async (barcode: string) => { + // Stop decoding to prevent multiple reads + if (codeReaderRef.current) { + (codeReaderRef.current as any).stopContinuousDecode?.(); + (codeReaderRef.current as any).reset?.(); + } + + const product = await getProductByBarcode(barcode); + + if (product) { + // Haptic feedback + if ('vibrate' in navigator) { + navigator.vibrate(200); + } + // Stop camera and bubble up + stopScanning(); + onProductScanned(product); + if (autoCloseOnScan) { + onClose(); + } + } else { + setError(`Product not found for barcode: ${barcode}`); + // Optionally resume decoding after error display + setTimeout(() => { + setError(null); + if (videoRef.current) { + const reader = new BrowserMultiFormatReader(); + codeReaderRef.current = reader; + reader.decodeFromVideoDevice(undefined, videoRef.current!, async (result: Result | undefined, err: Exception | undefined) => { + if (result) { + await handleBarcodeDetected(result.getText()); + } + }); + } + }, 2000); + } + }; + + const handleManualSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (manualBarcode) { + await handleBarcodeDetected(manualBarcode); + setManualBarcode(''); + } + }; + + return ( +
+
+ + + {torchEnabled && ( + + )} + +
+ + {/* Manual barcode input fallback */} +
+
+ setManualBarcode(e.target.value)} + placeholder="Enter barcode manually" + 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/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..d5aa8f5 --- /dev/null +++ b/components/layout/navbar/scanner-button.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import CameraScanner from 'components/camera-scanner'; +import { useSupabaseCart } from 'components/cart/supabase-cart-context'; + +export default function ScannerButton() { + const [isScanning, setIsScanning] = useState(false); + const { addItem } = useSupabaseCart(); + const router = useRouter(); + + return ( + <> + + + {isScanning && ( + { + addItem(product); + // Show a toast or notification + 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); + + // Navigate to the product listing if we have a handle + if (product.shopify_handle) { + router.push(`/product/${product.shopify_handle}`); + } + }} + onClose={() => setIsScanning(false)} + /> + )} + + ); +} diff --git a/components/layout/navbar/tarazoo-navbar.tsx b/components/layout/navbar/tarazoo-navbar.tsx new file mode 100644 index 0000000..7ea46fa --- /dev/null +++ b/components/layout/navbar/tarazoo-navbar.tsx @@ -0,0 +1,65 @@ +'use client'; + +import Link from 'next/link'; +import { Suspense } from 'react'; +import ScannerButton from './scanner-button'; +import CartButton from 'components/layout/navbar/cart-button'; +import { useUser } from '@auth0/nextjs-auth0/client'; + +export function TarazooNavbar() { + const { user, isLoading } = useUser(); + const defaultMerchantId = process.env.NEXT_PUBLIC_MERCHANT_ID_DEFAULT || 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; + 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/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..f616f16 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -5,10 +5,13 @@ 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_cacheLife as cacheLife, + unstable_noStore as noStore } from 'next/cache'; import { cookies, headers } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; @@ -62,7 +65,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'] @@ -77,6 +80,17 @@ export async function shopifyFetch({ query: 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', @@ -264,6 +278,10 @@ export async function updateCart( } export async function getCart(): Promise { + if (!isShopifyConfigured()) { + return mockCart; + } + const cartId = (await cookies()).get('cartId')?.value; if (!cartId) { @@ -290,6 +308,10 @@ export async function getCollection( cacheTag(TAGS.collections); cacheLife('days'); + if (!isShopifyConfigured()) { + return mockCollections.find(c => c.handle === handle); + } + const res = await shopifyFetch({ query: getCollectionQuery, variables: { @@ -303,16 +325,28 @@ export async function getCollection( export async function getCollectionProducts({ collection, reverse, - sortKey + sortKey, + fresh }: { collection: string; reverse?: boolean; sortKey?: string; + fresh?: boolean; }): Promise { 'use cache'; + if (fresh) { + noStore(); + revalidateTag(TAGS.collections); + revalidateTag(TAGS.products); + } cacheTag(TAGS.collections, TAGS.products); cacheLife('days'); + if (!isShopifyConfigured()) { + // Return mock products for demo mode + return mockProducts; + } + const res = await shopifyFetch({ query: getCollectionProductsQuery, variables: { @@ -337,6 +371,10 @@ export async function getCollections(): Promise { cacheTag(TAGS.collections); cacheLife('days'); + if (!isShopifyConfigured()) { + return mockCollections; + } + const res = await shopifyFetch({ query: getCollectionsQuery }); @@ -368,6 +406,10 @@ export async function getMenu(handle: string): Promise { cacheTag(TAGS.collections); cacheLife('days'); + if (!isShopifyConfigured()) { + return mockMenu; + } + const res = await shopifyFetch({ query: getMenuQuery, variables: { @@ -403,11 +445,19 @@ export async function getPages(): Promise { return removeEdgesAndNodes(res.body.data.pages); } -export async function getProduct(handle: string): Promise { +export async function getProduct(handle: string, opts?: { fresh?: boolean }): Promise { 'use cache'; + if (opts?.fresh) { + noStore(); + revalidateTag(TAGS.products); + } cacheTag(TAGS.products); cacheLife('days'); + if (!isShopifyConfigured()) { + return mockProducts.find(p => p.handle === handle); + } + const res = await shopifyFetch({ query: getProductQuery, variables: { @@ -438,16 +488,26 @@ export async function getProductRecommendations( export async function getProducts({ query, reverse, - sortKey + sortKey, + fresh }: { query?: string; reverse?: boolean; sortKey?: string; + fresh?: boolean; }): Promise { 'use cache'; + if (fresh) { + noStore(); + revalidateTag(TAGS.products); + } cacheTag(TAGS.products); cacheLife('days'); + if (!isShopifyConfigured()) { + return mockProducts; + } + const res = await shopifyFetch({ query: getProductsQuery, variables: { 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..1cf92fa --- /dev/null +++ b/lib/supabase.ts @@ -0,0 +1,226 @@ +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 || []; +} + +// Order operations +export async function createOrder( + merchantId: string, + items: Array<{ sku: string; qty: number; price_cents: number }>, + subtotal: number, + tax: number, + total: 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' + }) + .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..3b828c5 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,33 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +// Simple Auth0 session presence check in middleware. +// If no session cookie for protected routes, redirect to Auth0 login. +export function middleware(req: NextRequest) { + const { pathname, search } = req.nextUrl; + + const isProtected = ( + pathname.startsWith('/merchant') || + pathname.startsWith('/api/minlp') || + pathname === '/dashboard' + ); + + if (!isProtected) { + return NextResponse.next(); + } + + // Default cookie set by @auth0/nextjs-auth0 + const hasSession = Boolean(req.cookies.get('appSession')); + if (!hasSession) { + const loginUrl = new URL('/api/auth/login', req.url); + const returnTo = `${pathname}${search}`; + loginUrl.searchParams.set('returnTo', returnTo); + return NextResponse.redirect(loginUrl); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ['/merchant/:path*', '/api/minlp/:path*', '/dashboard'] +}; diff --git a/package.json b/package.json index d0ca7fa..8702679 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,20 @@ "test": "pnpm prettier:check" }, "dependencies": { + "@auth0/nextjs-auth0": "^3.5.0", "@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" + "sonner": "^2.0.1", + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@tailwindcss/container-queries": "^0.1.1", 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/pages/api/auth/[...auth0].ts b/pages/api/auth/[...auth0].ts new file mode 100644 index 0000000..f79a2e7 --- /dev/null +++ b/pages/api/auth/[...auth0].ts @@ -0,0 +1,4 @@ +import { handleAuth } from '@auth0/nextjs-auth0'; + +export default handleAuth(); + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b6ef33..28ec28f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,30 @@ importers: .: dependencies: + '@auth0/nextjs-auth0': + specifier: ^3.5.0 + version: 3.8.0(next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) '@headlessui/react': specifier: ^2.2.0 version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@heroicons/react': specifier: ^2.2.0 version: 2.2.0(react@19.0.0) + '@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)) @@ -32,6 +47,9 @@ importers: sonner: specifier: ^2.0.1 version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 devDependencies: '@tailwindcss/container-queries': specifier: ^0.1.1 @@ -73,6 +91,12 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@auth0/nextjs-auth0@3.8.0': + resolution: {integrity: sha512-xMzpkCuJAZ7tquJOZr7W4Jm9155EDotAACtdd9R9t4ATZgNGUzDQfPMogYJ2O14WB86sZSrp3rpJo4cspQYcSA==} + engines: {node: '>=16'} + peerDependencies: + next: ^10.0.0 || ^11.0.0 || ^12.3.5 || ^13.5.9 || ^14.2.25 || ^15.2.3 + '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} @@ -97,6 +121,12 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@headlessui/react@2.2.0': resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} engines: {node: '>=10'} @@ -265,6 +295,9 @@ packages: cpu: [x64] os: [win32] + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@react-aria/focus@3.20.1': resolution: {integrity: sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==} peerDependencies: @@ -302,6 +335,37 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@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==} @@ -406,6 +470,9 @@ packages: '@types/node@22.13.10': resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} + '@types/phoenix@1.6.6': + resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + '@types/react-dom@19.0.4': resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} peerDependencies: @@ -414,6 +481,21 @@ packages: '@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'} @@ -442,6 +524,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -450,10 +536,23 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + dotenv@17.2.2: + resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + engines: {node: '>=12'} + enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} @@ -473,6 +572,12 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + lightningcss-darwin-arm64@1.29.2: resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} engines: {node: '>= 12.0.0'} @@ -546,6 +651,13 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -572,6 +684,20 @@ packages: sass: optional: true + oauth4webapi@2.17.0: + resolution: {integrity: sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==} + + object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + + oidc-token-hash@5.1.1: + resolution: {integrity: sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==} + engines: {node: ^10.13.0 || >=12.0.0} + + openid-client@5.7.1: + resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -701,6 +827,9 @@ packages: tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss@4.0.14: resolution: {integrity: sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==} @@ -708,6 +837,13 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + 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,13 +855,52 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + 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@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + snapshots: '@alloc/quick-lru@5.2.0': {} + '@auth0/nextjs-auth0@3.8.0(next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': + dependencies: + '@panva/hkdf': 1.2.1 + cookie: 0.7.2 + debug: 4.4.1 + joi: 17.13.3 + jose: 4.15.9 + next: 15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + oauth4webapi: 2.17.0 + openid-client: 5.7.1 + tslib: 2.8.1 + url-join: 4.0.1 + transitivePeerDependencies: + - supports-color + '@emnapi/runtime@1.3.1': dependencies: tslib: 2.8.1 @@ -756,6 +931,12 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + '@headlessui/react@2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@floating-ui/react': 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -870,6 +1051,8 @@ snapshots: '@next/swc-win32-x64-msvc@15.3.0-canary.13': optional: true + '@panva/hkdf@1.2.1': {} + '@react-aria/focus@3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -919,6 +1102,56 @@ snapshots: dependencies: react: 19.0.0 + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@supabase/auth-js@2.71.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.6': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.21.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.15.5': + dependencies: + '@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 + + '@supabase/storage-js@2.12.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.57.4': + dependencies: + '@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': @@ -1011,6 +1244,8 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/phoenix@1.6.6': {} + '@types/react-dom@19.0.4(@types/react@19.0.12)': dependencies: '@types/react': 19.0.12 @@ -1019,6 +1254,25 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.13.10 + + '@zxing/browser@0.1.5(@zxing/library@0.21.3)': + dependencies: + '@zxing/library': 0.21.3 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + + '@zxing/library@0.21.3': + dependencies: + ts-custom-error: 3.3.1 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + + '@zxing/text-encoding@0.9.0': + optional: true + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -1049,12 +1303,20 @@ snapshots: color-string: 1.9.1 optional: true + cookie@0.7.2: {} + cssesc@3.0.0: {} csstype@3.1.3: {} + debug@4.4.1: + dependencies: + ms: 2.1.3 + detect-libc@2.0.3: {} + dotenv@17.2.2: {} + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 @@ -1071,6 +1333,16 @@ snapshots: jiti@2.4.2: {} + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + jose@4.15.9: {} + lightningcss-darwin-arm64@1.29.2: optional: true @@ -1122,6 +1394,12 @@ snapshots: lodash.merge@4.6.2: {} + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + ms@2.1.3: {} + nanoid@3.3.11: {} next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0): @@ -1149,6 +1427,19 @@ snapshots: - '@babel/core' - babel-plugin-macros + oauth4webapi@2.17.0: {} + + object-hash@2.2.0: {} + + oidc-token-hash@5.1.1: {} + + openid-client@5.7.1: + dependencies: + jose: 4.15.9 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.1.1 + picocolors@1.1.1: {} postcss-selector-parser@6.0.10: @@ -1234,14 +1525,33 @@ snapshots: tabbable@6.2.0: {} + tailwind-merge@3.3.1: {} + tailwindcss@4.0.14: {} tapable@2.2.1: {} + tr46@0.0.3: {} + + ts-custom-error@3.3.1: {} + tslib@2.8.1: {} typescript@5.8.2: {} undici-types@6.20.0: {} + url-join@4.0.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@4.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-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/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; From 114728730d61ccafff780a1d1e4ac78d2b73fc23 Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 11:21:37 -0400 Subject: [PATCH 02/10] chore: snapshot before pushing to preprod --- components/cart/add-to-cart.tsx | 6 +++++- lib/shopify/index.ts | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index 85e1307..243948d 100644 --- a/components/cart/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -75,6 +75,10 @@ export function AddToCart({ product }: { product: Product }) { (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 (
{ @@ -83,7 +87,7 @@ export function AddToCart({ product }: { product: Product }) { }} >

diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index f616f16..991a93c 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -336,8 +336,6 @@ export async function getCollectionProducts({ 'use cache'; if (fresh) { noStore(); - revalidateTag(TAGS.collections); - revalidateTag(TAGS.products); } cacheTag(TAGS.collections, TAGS.products); cacheLife('days'); @@ -449,7 +447,6 @@ export async function getProduct(handle: string, opts?: { fresh?: boolean }): Pr 'use cache'; if (opts?.fresh) { noStore(); - revalidateTag(TAGS.products); } cacheTag(TAGS.products); cacheLife('days'); @@ -499,7 +496,6 @@ export async function getProducts({ 'use cache'; if (fresh) { noStore(); - revalidateTag(TAGS.products); } cacheTag(TAGS.products); cacheLife('days'); From cc38e471015121b7e62e5ef56729c99ac2244a4e Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 14:48:58 -0400 Subject: [PATCH 03/10] feat: restore frontend and project state --- .../webpack/client-development/0.pack.gz | Bin 0 -> 1030 bytes .../webpack/client-development/index.pack.gz | Bin 0 -> 1260 bytes .../client-development/index.pack.gz.old | Bin 0 -> 1261 bytes frontend/.next/types/routes.d.ts | 55 ++++++++++++++++++ frontend/.next/types/validator.ts | 16 +++++ 5 files changed, 71 insertions(+) create mode 100644 frontend/.next/cache/webpack/client-development/0.pack.gz create mode 100644 frontend/.next/cache/webpack/client-development/index.pack.gz create mode 100644 frontend/.next/cache/webpack/client-development/index.pack.gz.old create mode 100644 frontend/.next/types/routes.d.ts create mode 100644 frontend/.next/types/validator.ts 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 0000000000000000000000000000000000000000..fba978790ecdfcef3f8ad7a7b2d45f63c3dcee40 GIT binary patch literal 1030 zcmV+h1o`_PiwFP!000046Wv-(h!jT@u3mQr0=kS~(GinoZvkPtSb~Qf7KR9dH_2{} zL2J9;%(UBG-RY`vcfuTo{J?q$k^JLD5oAFE=0`o4TaI4De?kbDkVHHwfj}<#p=#n% zrJHs%*`4Z{L(g>6_1^b>>%CXAlm(815PIzegeGdMOR$j9V3EYp0tsl)26BJ~i~FJ& z_R70Cky8P!?lJjvZ^{HPv0p&Kx7PZTlfS(JJdKY)uHHQmcUlqa#VHLT?}gB$omliT zD%!I>@+ya%lLXo{Uu;vxD4C}@eIrdtGgoB8UJkE!fQ#84gJn@~#!;&+>OmX<6LnxB z@74#Ty*mh@^*FOMPiVZxIfM*Y2rTH%cQcsn0Z*O=jZb`^ur!2&Ng8%y;Ka{ZmJo@u zF|)_t1?K~ z1!md=4WvmH#SoH)(#k-mIW%+N?RrN*`qA)SCvdiJ zs~y*0l+i!eM7fqv+JJc`N#g&TpB|@lx&&IpuLMMtt zvZf!E-MEVWq8xR;l(1z?ov;(R?oi$*c40KIy=MnD9n;jbi!08#VZ%sYy0SZoIFDG% zt@SGo{=>zNgC`q(gb=P)KQAEpU|%OXIq&GGwPT=pdxU6oAb%C;aicPv&i~iV;jgp2 z_+%{grBe-8esJYkJo(V3RCVxA_^ykq&v+k<(6&Vt-N$y~lBs{>X?6V~*iS6RCit>v zt4W2uCOyoTy-%%;gp^qeZi({5J7$UIjI8`7LN2a4_!oRfRS7?<>%E6E_3eM!hpL0G z<8NJDJ+#&PV&vis*?Q{odY_d5wE=ka@n)*o0QbH%+B9IDwFcJkQ<0Ik%zDgOO@!sx&C zy#Zgv*YGtLI}`X%d_{eKw>BC%w8OhzZ15*#y)mZ_LuP7SZtwDV2xiFib@^m6@IPkW zY?rec$RATiTY=mFylS_r7+6nPi`9VnkxejlWZ(afd&eMw>(np(H|jd6Z=Q~nPU6Dw zB=lw8Rhx`w?AF@p`jGpacD8(DEX9(I*3I#hOiqc%R5aPHjj2D1u{D}(`fiP9sHwhi zOl4E8F&rVBy%9P@5Q^W+pBaQrOOQo`o7GyuY*C-fe&O z)FKEDVw3IfTyO8RcPe0USUIh#a!SHj5{FSCRXnTo%}#uiv&`f%IxnG*ce31ACcpE< zSA~!<2Bk^A<;TDnB%1t|uS^V-2I$@`WHI|09K=Yn zuR|$JscAn&h!qkQGF%v{VL_q{aGhdJDOy3{)hi(xC7t2@6dSZ_FQl1*mDK1YN0KI? zd$pMa%}EgdJ@!7iy{S=y+s}hat1I-P_UD#-a-;7W-e(9 zAXmVNvDq3Za2>G@kE|IJ+O?!Y*I*i!9xrSURglpdY)V6QbyY0L`wf7GN-{QmqM-`r zl&EQXK^Ozo!>fr@Eu(?us2-@zMa{U)MUXPPhFT>D z;JOac6p5Qa;u@zelECLp#AR-UrKzO<9AFwEeb-ra9cfb}m9AatEYekY1!!uOX1Bqj zWHTNmqb5($0J8H@Q{^#lI%l5)+}BR92Q6A>k&Cdwjh<%{hk6!{Z0aku;rIS`wPKxBzf93Bv~5mcl^jO zn=DY6xcQP>6BojfZdAjeX9_Fu0CHMtDg*4Xm4Xl4Wa;Qasn+ruxJl=%?#}25D|3Ei z>;UaKg@iRq9U`%@P`R>j6bQS(so(M4 z_hv}h|M8|Bw%e=A*-P2;sz5`?^Pr4B#!>r-wm-P=Dr0>?nO^>;D@>M4g>krFZok!N zx0qO@G$>dqsy1R<4HK804_|3^+Tm-KpZDMV$n$>X+dock{M-M=v&)w@|J?j*^Y6{G WE%}L8{;s_D&elI&pH=oo4FCWDcw|rj literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6baf498344787ae9cc96bf5d87a3fc9c96098c71 GIT binary patch literal 1261 zcmV98hJZo>nL|(4?|pyt zt2fTYg^Ql&ef+NH{c(17=HB1f_;rj?4q^~08HEBwieVST;I>qF)17*3fw4dR(V;hu zW{?`~_AhUFUidYkr7*)3>>vfLA$$lHj*SVmiScP>;zA*X?Of+ssByykJG<@O_GeEm zg5V%F+5XP;_D*}J0v3mr)2b?`B#b3-7!^{*vr6CW#5Xz1Odg~268d;2%Z+97J5PL7 z2pMBgn)F+K42(gd$#40}#6W3)?%hHbgWvK&bL@_#gOM>zY$11q)R=+e+y~7;j3oOy zl){vn_EUsdAyFa2g`pZ2B+3BSDb|#t6%<~*5|UBU8QxE^LA&-snkiUGjZShTX%f1J zJFvyf*riE3IVye63K?d=XeB7e=FX)jK@GJiQgGQ@kv(mxCtb#aq1!oeBMM{=2lpmO8UVGrdDZo8!SpT z<54nd@)QjqJ0CSw9`mMi_Bp_P-F4rn$wBTjsdI4C4^Emg+yq9{%>7vU-?jW4`gm)? zs7boX*LmV|Id95gaa6t@kz-^FQc2zKDWKHvB%laeemS(i5G+TMr(HvmrO|%Jj|{WP z0)>g2FS#{wAsp#OH5_`TumTStr=_Mcz#dyE_`pq;jxLmHEw6!_bk6GTjGnME=SRj4 z(4JFBSfkV-5*rJZD+@<~unU~}9p6p9iBLt1Ut$7<;sfJ8%=<=wLh?Io3vBs7gxs~5 z$Vnv=OLE`bgZaG_-!~9T-Cyuax)t~Fu+S_sT#c%`48LnlQf6_o^-mW!sNayQ?$s66 z)0QtY=ws!WTP~_B5!w;jHTA6shnS#6E$}O#vuA9MGPBy76;;zj20pQ)0~S=*0Zc(~whY*xTB8cG*2H zd0U$oy{)(1YEUawMhyS}Jpp}j literal 0 HcmV?d00001 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 + + + + + + + + + + + + + From 6b804442ac6def25fb7d2ca772a7fa273ecb9680 Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 15:27:05 -0400 Subject: [PATCH 04/10] update --- app/api/minlp/explain/route.ts | 13 +- app/api/minlp/solve/route.ts | 13 +- app/checkout/success/page.tsx | 2 +- app/merchant/[merchantId]/page.tsx | 2 +- app/search/children-wrapper.tsx | 2 +- components/camera-scanner.tsx | 11 +- components/layout/navbar/search.tsx | 5 +- components/layout/navbar/tarazoo-navbar.tsx | 2 +- components/layout/search/filter/dropdown.tsx | 2 +- components/layout/search/filter/item.tsx | 24 +- .../client-development-fallback/0.pack.gz | Bin 0 -> 36456 bytes .../client-development-fallback/index.pack.gz | Bin 0 -> 1866 bytes .../webpack/client-development/0.pack.gz | Bin 1030 -> 1045 bytes .../webpack/client-development/index.pack.gz | Bin 1260 -> 1259 bytes .../client-development/index.pack.gz.old | Bin 1261 -> 1260 bytes frontend/.next/server/app-paths-manifest.json | 1 + frontend/.next/server/pages-manifest.json | 1 + .../.next/server/server-reference-manifest.js | 1 + .../server/server-reference-manifest.json | 5 + frontend/.next/types/cache-life.d.ts | 141 +++++ frontend/.next/types/package.json | 1 + lib/auth0.ts | 3 + middleware.ts | 6 +- next.config.ts => next.config.mjs | 5 - package.json | 10 +- pages/api/auth/[...auth0].ts | 21 +- pnpm-lock.yaml | 589 ++++++------------ 27 files changed, 404 insertions(+), 456 deletions(-) create mode 100644 frontend/.next/cache/webpack/client-development-fallback/0.pack.gz create mode 100644 frontend/.next/cache/webpack/client-development-fallback/index.pack.gz create mode 100644 frontend/.next/server/app-paths-manifest.json create mode 100644 frontend/.next/server/pages-manifest.json create mode 100644 frontend/.next/server/server-reference-manifest.js create mode 100644 frontend/.next/server/server-reference-manifest.json create mode 100644 frontend/.next/types/cache-life.d.ts create mode 100644 frontend/.next/types/package.json create mode 100644 lib/auth0.ts rename next.config.ts => next.config.mjs (73%) diff --git a/app/api/minlp/explain/route.ts b/app/api/minlp/explain/route.ts index 3d9c625..65f637c 100644 --- a/app/api/minlp/explain/route.ts +++ b/app/api/minlp/explain/route.ts @@ -1,11 +1,16 @@ import { NextRequest, NextResponse } from 'next/server'; -import { withApiAuthRequired } from '@auth0/nextjs-auth0'; +import { getSession } from '@auth0/nextjs-auth0'; const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000'; -export const POST = withApiAuthRequired(async function POST(request: NextRequest) { +export async function POST(req: NextRequest) { + const session = await getSession(); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { - const body = await request.json(); + const body = await req.json(); const { solution } = body; // Call MINLP service for explanation @@ -29,4 +34,4 @@ export const POST = withApiAuthRequired(async function POST(request: NextRequest { status: 500 } ); } -}); +} diff --git a/app/api/minlp/solve/route.ts b/app/api/minlp/solve/route.ts index 20a95a1..f7e8934 100644 --- a/app/api/minlp/solve/route.ts +++ b/app/api/minlp/solve/route.ts @@ -1,10 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { createMinlpRun } from 'lib/supabase'; -import { withApiAuthRequired } from '@auth0/nextjs-auth0'; +import { getSession } from '@auth0/nextjs-auth0'; const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000'; -export const POST = withApiAuthRequired(async function POST(request: NextRequest) { +export async function POST(request: NextRequest) { + const session = await getSession(); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const body = await request.json(); const { merchantId, orders } = body; @@ -41,7 +46,7 @@ export const POST = withApiAuthRequired(async function POST(request: NextRequest orders[0]?.order_id || null, { items }, solution, - null + undefined ); return NextResponse.json({ success: true, solution, runId: minlpRun?.run_id }); @@ -52,4 +57,4 @@ export const POST = withApiAuthRequired(async function POST(request: NextRequest { status: 500 } ); } -}); +} diff --git a/app/checkout/success/page.tsx b/app/checkout/success/page.tsx index c890db1..649ff26 100644 --- a/app/checkout/success/page.tsx +++ b/app/checkout/success/page.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; export default function CheckoutSuccessPage() { const searchParams = useSearchParams(); - const orderId = searchParams.get('orderId'); + const orderId = searchParams?.get('orderId'); return (
diff --git a/app/merchant/[merchantId]/page.tsx b/app/merchant/[merchantId]/page.tsx index 83f921a..933c675 100644 --- a/app/merchant/[merchantId]/page.tsx +++ b/app/merchant/[merchantId]/page.tsx @@ -1,6 +1,6 @@ import { DashboardClient } from 'components/dashboard/DashboardClient'; -export default function MerchantDashboardPage({ params }: { params: { merchantId: string } }) { +export default async function MerchantDashboardPage({ params }: { params: { merchantId: string } }) { const { merchantId } = params; 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/components/camera-scanner.tsx b/components/camera-scanner.tsx index 1226d4c..fff8a3e 100644 --- a/components/camera-scanner.tsx +++ b/components/camera-scanner.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { BrowserMultiFormatReader } from '@zxing/browser'; +import { BarcodeFormat, DecodeHintType } from '@zxing/library'; import type { Result, Exception } from '@zxing/library'; import { getProductByBarcode } from 'lib/supabase'; import type { Product } from '../packages/shared/types'; @@ -34,7 +35,10 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc setIsScanning(true); setError(null); - const codeReader = new BrowserMultiFormatReader(); + const hints = new Map(); + const formats = [BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX]; + hints.set(DecodeHintType.POSSIBLE_FORMATS, formats); + const codeReader = new BrowserMultiFormatReader(hints); codeReaderRef.current = codeReader; const constraints: MediaStreamConstraints = { @@ -132,7 +136,10 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc setTimeout(() => { setError(null); if (videoRef.current) { - const reader = new BrowserMultiFormatReader(); + const hints = new Map(); + const formats = [BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX]; + hints.set(DecodeHintType.POSSIBLE_FORMATS, formats); + const reader = new BrowserMultiFormatReader(hints); codeReaderRef.current = reader; reader.decodeFromVideoDevice(undefined, videoRef.current!, async (result: Result | undefined, err: Exception | undefined) => { if (result) { 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 index 7ea46fa..bbf86f0 100644 --- a/components/layout/navbar/tarazoo-navbar.tsx +++ b/components/layout/navbar/tarazoo-navbar.tsx @@ -45,7 +45,7 @@ export function TarazooNavbar() { {/* Merchant auth controls */} {!isLoading && !user && ( Merchant Sign in 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..f59f7af 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(); + + if (q) { + 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/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 0000000000000000000000000000000000000000..4fa48a9026f48eecf6862389ffc7b9be59645914 GIT binary patch literal 36456 zcmZ_VV{m6d*C_hfwr$(S#OB1dZQIGjwkEdok0-Wm+dg^T=bUrz{c!t3S8Y_Ss_w46 zx>o--VKfxbe;OF-MYq2LnPnPeAw3_cgdH9AO`+h6gzAL+p*F-*sgipd)sP0G< zJ4`P&4uDOv6_L7MZh*RqRP7-1?%LiR+Q~t!F>qYwzpB zQ$U-ev6a=CJ-@&0Su>~K>4Fu!OHu32N;SABXU9@M>gI{=L$h;`9`F0NGjSoe<%-_(({xR6?x!9YU)5qS{CUs33x>?X$(kE+T z4m7w1gEiqc1CP(ZOUwf=jT4DZ-9|%~qI^zO3)hg|f(;evtws z$gl8V$dQ%}hrT9xjEAqC8p|A?f@SC|_~?+&kGvkhdj%qddrI?Ht)f*`2WgpkYuX!J}z-bC*2t@_B>#-Czk$Y3T-?}U(5ap-pN z_+?d)C^EGT`UQe;BI67Fvj7+m&$StMuwnmw^i(WD>mBYEW|aL8PggvCs46Cp$Y}E= zzkNnnf_S(Gw8;;J&l6n1G7hkJUw^a^_1?Xf?z|26N8y>r{_D@d>%7dl(yf~`X+eIP zsV=|$G>UQ%E{qfWI%EFl*z0YN{rB4Lr$2#PExD_{=kWodxFKHt4);$BwI8gP)p-Qy zWnUnl-JZ@zw1QvFoZo8;3kxB$g5d1$V}(zXyvrV4=X zr+#i`2lsu-vY&U5CGkfXYUW@SqU(qLCsmH$-DM9xSj&3GYu_Yi&v(kQ*;ii1_w&(2 z?6SGvf%kS^W@hH=nTEe(PqIt;W#YGC0T%w*iHSy^|9u*T%BDHI1Kd)thpl03 z;4AAgv@CkVw;M=?e^acw#J7Ur*2$To|9cU93Bv8QjxSX6`ttWqa{u)IjMDgdQwH-Y z67z$a{p3Pu&+Bt97Ho9&>Gu0aH8sR{@26*wAjGTS3!kvx1V=VeFSf%8ZP zq$WXM_)*xB16MFOM&hqQ_CC!EI>(&45|F3IJfPke4f~0YqlxWKZs+~siS429FZGtF zm&J_UpM~(Z-MWZ`i6vp5t~T06$)H1?Mg;tvrveJS=De6>&p{VAwlc^yM^y}uVY-{6 zLk1j9b#OxP?Tyf={`s8sTL7j@f8hJ;SIf%#IO{h>RQI`&I(dK1_Q$Gq;UkJe;LH2` zG8!9F;4(S~Qt+~?%)ogx2U2*RFBV7+>IiqAb1l6C6^6e=)KD+SJuMZ|H_g|xy;<~7 zUe0TK7OVc}SeE}`)$*^58;@v*K_CwugsT=ANzU&Tb2+#{RahmKIah8s znTP=MuLD<6)Q+PXLRAAhonCmj+I@(iNJrfdIGSqjD|Rs0YrK=H$F&KC>5qkp>4}et z$qd}u$&ZSRy#8K6L`O4wp(^962A7a{j7d=QUImtnEE`_6h~8J)KcN$@LE0!2$Iz=N z76~V5V2}DTCPg#fIv@yMyGe8f=aIKJ&eGKRx&oNUDrAsqOAi~l(`?(9ieG5bXKr6G+_(>%#@xPaV z18&1#Uvd4#2?8&dKvXe*-&DuphopYr&^7sJxIfWvFX6v$+de=%tJ%SeXMvl^GX2igcQ`Ppu2wh6ZL2$u`hv+Mei9Gl*scR&2`=iM z?X%Jh8W{DL7-G0(?UEgP+Q`G_X_r}IvLASWCjvLWC^`0*fCG?;=-ul5HTt}^2e zqnE}+PP$tPCf7=hC+8%~bl2Xgy&({ONgXvKuM>$J{>g0r9q(=fb9=Wi^L~(@}dk$IdoNwLsDiQ={Szh#?OBs(?n3gWzvt%zV zO*Svz(f#=8>!Z-<6G;CDZZZCJy#1X1+ z83kREKvUIW;NI8-d{N0_+e1iIC+xW8a%yH-JFdAV^lK&)GM~uWW>f$SND)*juOIL% zf;i@dw#WWk692wta-YfBKyJsw!c?=d3x^=m1|e|PXT3!4o{niK8fwu~jPdFaoa$6( zojyP_Vg*OWUv+BWppbELg@4}P2xFUniTEbTBm`c> zma;@sSrHZ{dpVs3sF(4r1HK_5Qpr>lkaRMjD&YlGZeb{l`|zuDS*v~@@>d(4+p)JoF`ZdDUY7ZT#1%_s>Np%{Jurncecx>YOIi zxgE_EUCMYTIr`Od4S3D)^6Sr-3xh-liIK$`Ji6CB&V;o=X^Tq{XYKDXc_vyQK#Ip?+jwmUwS>b}I!4{iq_(qG=M%7(k#O{v04 zLy(b)^(;|Qsycd6^4U<6YC@6lOc2m9Ob`$jN+x1QO<=k@j^E&|x|ZLKPl%7lYHsmp z^b8zw0#ususQZHi0r<1oNFq4e;t@pP!!WTDlOYF0);b&}Efa?>^FBz{uh6mQ+P>F= z9d|QCYqs}raR5LkbUo!{8P`@ft|~}h`Qx#?hp(PgcR}+_=(3tF*cE)7ED1xs8ms&2}yB=`{4gW4SMmLhEee$VM)Sx<}KfQTeZszHYPTaGf}31ER;( z$a_G@*h@6nfG3UhQBfcBiTStAjzl!VyGaylhZ?9CX~QjvcK3OxqcOjc?=p{?iN4Ku z6TDnc3PFy+i5Nn&sy7uXb!8%3&iE@voU2K&+r*iRiC~!ux~AbhGT)9rG9~g;Gdcis zEO-jNzvhqz1w*#7oeLq1q2u6=>LD=hdQ0x@WQIbQtQx66R&qR z+*#y>WSy}K{+NdH;%%#LesN0=$ehPYZx$|84+ZwRD|p4w!UENU@tFM!a#Ts6!b#?& z1ZKXz)e$ZG%&qP4HPYBA!M-!eASnUbDUs4djQ!iGS|yfRtw1IZW*aqgJ?Kh>&vn4t z=w8RX_%v=mM~mWw=p>BB0xO@`k@!_*)nB>EdA=R*Qah~rp2`XtTT0b26*Jz8@<~iG z*n&f+-sme45B|=1?H%X7u4T%M02WR;cQ%_(8=)B8OH~*S*%{@#PR$iK+<2BE$Uq&* zDU+|iT?llRCFEFd*KRA@rU*6M;GZhBt6jz19ZVP&)d=FpL2!>xLbi91BklWO{l91;ZY0}SEKQqTz7^vY4(IT}8nZD_ zPi_=qh;rxb?U@sdfmMf+lQV=h&B$Rmd=*U`-TLm_f&BuiY|z?6f=(MI1GG3*i<>|FnE_Riw7#(M4sHDci}NGU6Htfa_RaAVnQ5qEzVp1 zjG}NSWe0JEghSe*VDXIOBj?fUwve;KWfKpolPd{cEf@Ok(AvyM10m!p4A%2l-Qp)e zy)Zl`rEw-HfnF+R)DMH)TjN=qj8dc_>AM*rlMM$*phzQG63ayeqjlnq&+NIi@N-g5 zy-^qHI~-E(=uQ-dpvlK`KwR6`9;Bf8u278YG5pp!gV=jA3&dPjaVlFa@?-YBjUHtx zd5BMN=$iWmzLowyo`}`{cCfO78HmxD#FZs7Fea)*w_!bOmOK#rmN)DMddqTbB29TREYa~{dQ);fYK4n~k8Z*auLK7dMQm~@9J zm?Z}1s}hQQ8xMs1z5WxZ(^^FZ7B=(TlAgZ~p%Q%%X0rrHJ_v~~j;NXrwFEnyZ&pGH zBw`^c8iExI-k_~-{*oXc%8IB()hKT`ic|J@tfyx32KOn<> zlOHC4Z*re|wxePSKruB8B&A*fvHESBe?YVlo6F&%&|P2+61b0P3-XuW@&Mb7-N5=ihR}>#kf-tv<%T zoa%sx2a~m_znCnxUmqFoo(ITGM0f*_5 z{ev#=cfxzmj1r({!ylk3h%#4n5jt4T_@u{|4Q_{248t7LGjvtqS>Oo<-po%}loa9? zdLKKNTRvh}rpM*wSyBR(QkCv3>%?q`q|0D|6if;nl}j3S(bC3RvRKYSW;k44t2)pX zWSDCa9ekN=u9kJ8dl+w^Oo#sow4s-nN8yah-ACD~#EtNTCN5Yf1!r4qZDcqSlAbJ~ z)}_eF+If?Nej8^-x&1xQ;;_fxp63dSC>=#6RjbrE z^m*MjMw*PpH=;L)uL91C;=)HrslN&WmtzY};mS&e9Y!YPU($(qBN89&?Yvb#J+RWf zMdclLz=V3y-}S2wHDvYiTSXN_h@!7cI`-`3(}52DLZTN(LsgwiL!K5_Jy26J33D+K)rlbL-W+%h|9}u85{$SOgzHnfcE!qSIF`yM9a@nu5Ya8U+nO63Bb77g2xnd ziLt#RPT%t{;#E5g$e_<1Yjhu4HqENjct@RJ6WDd4k$n7Q_$Or)BEQ5TU4N=2+%rA67N~@a~)l}&UPs5Xwhelkn{fsZR zc25F2iN8yi8sL1fG&BF1m_Al-1ntQ&ZAnG?VvLF+vA{rJ8)QLZApPnK#)~JN3_5Rj z111RB)2^{>A3mw1N*`Jm0jHD&6IrFUuZf`_0+Ab94s239HaRLf{-wZJmR3&h`1)66 zqUuTB#mrM8D>Q<@VMV$m)-IDaDf}Y2LnpDffI*#4hK?Nti$qHxZ9G<`2+6QwzTSd) z@pQ1U{*;6}S)c%iF?HX&C4$ggcw6`l`CzyZ3$m%w2#g#H2D=F(z*b2W<`qSdui)8Q zH@c1RmBkUu2rm-Z2!>mkkva<0Fi;2B5|aFZugzIWl`OYWQwW@<1gdmoNbB!XNB}~p zZU=|lM9CAua`1}1(A(Li=|3TB>lqk9h-5M~G`?k@h-kOV6asj*_V{`3@-XOIHoj(AR?e5jI$ zgw#qleNlzts-W3b%J23ioC0X+8!KQxWj?!bk>RWaDCjH|E!&Y~2iGcJiK%h9l6F73 zq*}mJ#6G;j{;fpcZ2f~Uv(4$U@UT*s@z9_jV5Y8pY{=zazM`lW8op0c(Zj4T%GE;zO6}^%O_IK1L^c;F+*R+c*a!p1w z6=SHB&H#fXHa=}r(e~S?}|eCOcFjnS&2-xaJDUE z^w24iLt>;}3_KtcoHg=;jT~x`i{!Gz)?x-0DlYY>a8W$Y`hv6M8E85%ASAgKfFeb- z9gCZUp}Hh1)iL{9CPqO-0xmuU3~uv;#QB&)hh%VL)`$s>Gz2vP({TBYo`r^ex#1_f zp?oK;hiy`Fx1(7ht1xdgUOc8R5=8-8bq0(~0DOrfq-jNsy$!uFc(W9){(**dedHl@ z=c0J{f|%;yCt>R=zsW7xxTu6twnraqchAtQP5XuSq1ROGiX9e*-{Z`20)0ePcm1hZ zbStqcFjE>)l#t{I25|F9zEX?f<&(uicBx97cHzmyFxJqO z^!t*VPCk)VD+Q2+Nu$26K@=iMGn7z;xW*eJRvhe*hIhDT=g`d*OY9 zRB!Dppeu;8mdIP>W=AjDqjoLmHl70MG>WezTZ&18>GIP1k8o1RX;T`h7&EaaA^?n0 zo{|T3VGISO67FO{1H`Nq6mq_(*h-mUQOJn8US&uyj`(1#OAcs8B~hXTBZgH166O&` zSYf?izRV3|vx(`rYeP>NQ#5Bql9|6)cv+Cn0vWQTv~1?HVcEa zfAc#6#OoQOLG~EiD~t3dkjU+%NNK@wCTHrgf+p|*#Is*Ot*Og#sGmyL+z_c1i|Qe+ zNh+1-b5xFE1dq9*4vSq~X@Fl~Yhnn7Ux4-Lzfnz6<0{3oN&Qf$VoFUSjvsB6VA6HKDexpU z(p^y|3Q5=tNl9t@PAD{uOxUQLe-t?xXh;^I>T1a{8!@JXX9zzSp@H0dVTOwbM1Zm( zo*@$Ovz*ZSoW#*AD%^|+4%0_8%Z#Ymbh9L3-KnDRmJACT$1$T}{kfgF9iH`u@b6L$ zT;|DLHo2v%b&W}ZBvB}nMZeb?30U*21VPQC6czxG*{l3JdjJoAe-hM{gQl$ept15;f6T91VB6o2=EOIF~R>@k`#9 zd|=ktDX7(epwP1}iX%3FHUCZqIQBNP9-kV2xP=UJdYX(SRWisE8*Wq#l{46?f$Xe- z96}9tE^wx#T>KxC%z%39wO~*caw1ltCT+U71V$7Y0aT>SlKtv^Q|fgNL~1Sg0I-R1 z?H})4h_>(Ipg<+M$6Le9+KPk>zs2X)JLhZ~_*o3lX?QwdrX#PLMvz4`(e!Oy9}QKc z&N3xCd}vZR!C70#D_$lU9)Il^%^NT3zw7-vBv4?*hc`t36$T=x9t|hqrbH};kv)8Ct-l_5w#j?IcST2+FNOhxih^P8K@`xet{WIDf&oOuA zg<;oN+x{W%EBfMVCUou1JQy6{z=_dhJlHO8!3EKi_et6S>46&HfrQsRPa+3@j(W<>o*wd{Dp+1>7=N)_YEZTL1=Eb@irEJ z_Y7PY_u$9nRF!Z2TM?30I@AggzwBJwHs+VqI7qecB+5#bu z<|cz6EPX&~ne#@=j&_197w$hx;vU5=Fj)Alu%+K^24KKl!I6CK%WmFTdj0|2?8d9U zKeYnCz17mCa1ThCYw*z1Y`jJ1S%-NJFaS7WF9y&ow~i!Kdb;z)-m4d#jmaWfQ#;x% z+|2Gtp5z;041}kH>9&Mn51Zd8f%RFn%~^3%5zc}G0;9;xT=0WeA$ z3l-ApcztH-HRjG9P>ZFr|M!sDtAYLT(AEAsqMprW?S+7k&~4#NjJrx9Byce1ZLy#x zT@Q+?5NDKkt|j`vr2a84Prq2qze;{YQSec@$e)xK?%4@wW1?gt!1jSZ2cdKiB<2wI z8L2OMBwxfuH$z{I1DoLyh-j7WAxQ1P$;l@%f~TT%gjR!gVwv}unuFqEEziT|;lL>K zM}~N(3H;5v^Stz2xq%gT;5pi_xiDu_=j~nK!#^JI!=X=7wSCP_;m~ zUnUTUUh#^zS-U}(F;OPs&>Pa>ql(PE%oVP+69J*LiW3@YHJw$17IQ`P=cWVM#4ryg z+VZUd5J&`lAT$fS;81~xqT6=xqO^jrjMx1^X2(Jlv7GttfNM8i#9Ad{K&wAm3J~?w zt3_@sJedR*)01MM>-AwG`U1#wGlm_P_H`6k%M=xt4q(CQ1RK|(#6HtP-}F7nL0XF!2trf~sdMC@e^Z3-sP0twow`*;5hatj!Y#R;h!?S2Px%x|nzOnh_HWQBXD z2=4Y><{{Z(i6%v0kp;(%Y+$E85>_jAXaFWg3{V^Yh{;|cuB6AFR#_zF*(u%ym76H} z%zHsZXX18B=CDi|SHs6=HM!XW#cLA>tgKN~9}ZxcMi1^)Q-Jqe3`r99%kEbwa3v8f-L@*xx8etiCWIACRpZyrPKMYo!Z1y7w+&lr> zLBRgrN5k}##sItRkzl%NdyH&|mU^P$&>|5WyN*|GE>*NP{6W}la&sV_Y_sF5&ju_J z{$Yww@I8kKz!xUag<$&6H_yFS2#n$5&a3xBgI2wEK_OMEC$5Iw3RvvIuAS-drIg{o17u z*#7K{`_3O-tv%#hd^i?&g}M?q;Sqfh$TK_bqfe%VSO)Z+lix*cWl8dXi4e=j(cyks zNrrc#WrdGK%D8sfjr`8{AvI!FY*I|9g9hDB4@C`!xQa_-Y(DT_`P(TAz2M;Uyk!X4 zxRMD^-&Uq1k759QEnG)6Y8w>JgiI`Zd#ePxm>kDTf2A1Ad(!f6Ha=)}@e|0G%stHT z{mNT&Cu}>^ZTl{4NAS0n9vO5Nf$1sEh5SyO@PvyS|8kw#vC8jJ_To7COI*=iKCMy5 zGs+J`)kZ8JTDX#1TQ0DuFzDSUaVJb=hK)_2#C~?BGkkZ#XZ^=Xq01Ie8YQyDowjfY zl1&=KK7|3GNYq-Q3P(nGAN+a$Un^6rOUR5s&_d0JAVZWwVfcv!pCYO|Mmz8d1h05I z7tBvgW^A+C*_yI%OWHF2t*n4GAb!2}`)j|8IjkZT#jmIOSavs&cGU#`k z1x8jdNnaLv8*G~uTstsnr*r8DIpAgewWX&sKa!6!dGOqi+~w3BsPOC0&*DvOL=&XT zZvJYmvd^H6-sXr|eCoXZO)JyrY^*(7-(Ov@pZqA4j^U4M_Ja766f4apkVx!=w}={x z$BLK)M}7bZ$4*@kf%EgQY}pJUM8Y3NQG_Zjze7C(5v>*G%dzdZAuM53fNmK$)ynwzh8WXVaLX2T2btyRI zk6GI5cAtX8)FN(Df{O5E(G;YC^UyF|=D$zt(@xpgPBjlR5l)t=w?jLKj*&1TBb@O> zjR>y69;;!2r--h`fz0e+}JwF#RK@NZxwIif5=04dijhuIJs zd=s8=74ackS8RSc*bQW;T&ypwyqoB_O?wz}XVEtBBqMa~QrjUvDxf$wz_M~HU$r|? zBF59a&CY!AEE(#5jk8MaASkLc$nv1;7t?XtogD;cf~7kw5eM|UOE8$}Kh^V0_quTbiuwEjdjn zjeemPpoxBt?q76#2GoV53I$S65^xB@r_r$=;}E@T2gH=8{40?{{tyf*Ra9p@mZ0gqWD#&jtUds+x>Lj%xL4^587FqIQuvlC2-gWP#T#8{s7ns6thA|g0UFX`fiSt%< z26A}EDjEG<#QhhppTO605VYfRxd{90w>$cAwnKS|JxPrC+xk2`yYqmbo_i}KHf{Ci zj`7vyPmfRg@g2PHee|fkol(UwuVZ}*b0l+I?vgjr@2XfjeXqqO!N{4s$IEyH?nCMO zEap5bd_)@qN5jNH3D_YolhQj=&|lVW zzb1oE8nb30OxaqgRYkZuk_|c$Q3=Eo;iPdX1aspMIMR_eQ*=nRLoN&*Ixe;E8>p&c zIaAmm2J8Oif?2M@o}%_TfoXSq1LlYD+hx_8?Yc?5*?SbjUBbO2?`j$xf$y{4Y^|FjQd2PYgq&zpWZ4)OW=CbUzRl2K5q9SR)Pq{?XBA{Vw;{) zB4@3cpsWN)<=1zlovwhH%?#kI~KqGvb9JJKg>lje8j+dfk6u{T5aR zy~N!%-oe1hU2owPB~)Ht5+2u1N3w4p=t=R3Ff$P=3xe|C3cH3nD!A+d6o4=Z6zNfR zuqLZb8v8cE!|VTA2Wh2wP7ZoaPYZnXLq`7(;PN-{+3_xbZ0!zh(+}BrJ(vk z$Z?^U?_GOg*rG;cf5!P5vR@{2eVzP2KtYB>K1PJ}sO-0K_IhiG5PkWhtcoeF;m7Am zjWX)8Df*joEsjcT9@!%>0kaevS+-l|Q1<&3FANewYLxy}L+d#zy4->a4a>gti0 z5HY4&plGjmOiYl3bF_oKr)Z|O$8?l+J5CyCjL4shAaa(V=}SPX0*!*Yg1bs!!(dJ! z<%|&QZ>3>L=f+yW$?20i2*(Pf3RJ*uCW7G&8-)?*gI2%jCh{b~>9KLiehrUNrKb46{L;>~Jw|{EQFc!hd2Ec~ zV31Q$Q)doYp{_kpU$}38z~3n_t0a415F4Z|Si=&_~Lchnu614MY^OwS5s}fRk@*e3o@f_D2#~`WxtFa_ubR^Jb zY7qXn$nr9Cp)bPp?uWzi4wI7pAWM5q^t_!MNEYRmd@VY59vtqX?O!R#Gm1wm-`76~ zXlTI46gY)dH3cUQV^^AEIEd2JESB9A!RW8*a@Dvt=mS#UX}fbZ(1c3|vD$-cs=+Mm z1tw5Bh57%OtOA;`y9Oi_rU!f*STrNCSZWs8iT`m~x;%Ph^(+z?%F#q}5q~H2DoCPR z0B)VB_Tes0!4WR&xDYt=xHw?>O#dNSgy&HkY_o=WvW~x6t%PWAEfTA@9{bzs3F>>S zb7&eETl`gL1Gc2&5qyBbgX)-4$rlO6#Gq0TiRxzTH+ zG6d|{@}2c7{(&fbtB=DE@TOf&=ngU&GVM}jlg|_fB`w1#z5%hKZcGp+gp`KlTm3#*GRSILQeVW8IONc!c~;?yr-3bnzfy zpJpwTk#8lNlZqP|OmOLE$%&MeONUsFRtl4%!A#hjQw z;(iE)mi)`uO6>$6aZwOUDE_s`@R6{m)QV4eX!H((|A%80MCt5zc`WeOQ}#0=PlnN| zPY5Oz3H!-=>l+g+Ee&}=6U_DhXe?X|hanb`!l>AfBOoQMDmMeLK(Yfi&k*N8h(gnd zCX3KcPv?eMdAy)gnUtWW7+Nf=Ea!dpqw>ZHWefnnmIjQp4rFJb9xz5^!IMpLl}t&@*SbClM-w z7~D_+LlB697k1PlqOp#c0|VC2B_d%lsan=sP__JePCVmEI@8x+=Q1J82E^WLnEra8 zg|^&U2Q>bv2!Y_$8iWNZ)5^mOd)`8mM$)v3Ja*l^A8Pu7o?-Pla1KNeM5~2iUKUgAQ`X= z>tK)ME0q8=hy$gW8@Wn4^&7^s#dUqu1o}&s(vDr)17m-u4ie6M2x`#$M+i&7gPKFt zt4S@i{*PHjR}vmS`2T@eL*Pv^46Zs!`Jjx(+aW=+bRZDqo=IHvB;aLIj*eLAF{UQN zHd1{5Z-f=ZEIucWE2?pW-J&Pn{3u4)GTM+9J(lj4Y>Dn~@*jvb*J2p|HuNyW81-{B zu#!a391_WaBqT_DOjP-Ewf_K(7L?)nxpZbi_jgg8C2o zaA(&2g;dkqgD^{suJzhyR@DRFGu^mcClZ3)K0rX{Z1O>Ba49X?5LtKbh5nE!5(S5K zhkEo(_S_mlzoNiLJd1l3o6!mq^!4Y@Hj-DaeHk2?F~(33KpU$q4D zl%xfR@l_p>AQrj#^Mt~Y+&?&5d%36TLgsLx;Ko?hM^zKY!$N*xPi;KW_xCDi$;{t! zi|8q8r5r4oZol8F8cTBu)G9-eijRemcw(01pUBOE2U;aRCaYbfqN_Y9_jgk+^S|fu zu^u$1ub=R!G8?kud1bE+4Yiw7{=Zi-bxe#yPm_yK2_&xN1ma44s3<-Mn>)kMOLPzg zH=Go!cPdiGheC2OLXgmhN+R-6P(D!O{$e2!0+2O2&~_@p$E2)lFrCsoWA0NJ+o&2$ z8Q^wg*oe{CgNfr%Kz#L3zYJGXj&qhVjN|E0Q9DfBVm_=iGmA>spP%)StQHE{>Lo>H zffzDNof}D5y>+!it_f!nbJI@SGc6y{izAke95n-TtawIK6yhoKp{44guQZrycGMT# zcn?~tk{SkZbZ%>Z!;Xo%Z&DJ=B(JLZuB9!f3{$Wj97M6m!U{i_rqhFuM$_BU+ccS0qb5By^vW($f2K;5~pu-TV=t!(sv;gj(~I%em? zt|~ZP2ipwh!zB{1Ee{TC3 zQa29ND~m9EyDa?Qdyf4{wl8+ql{Z&Mf;tY0exO^WzjprE-vqGc$8iUuRMcGP#(Y5YSK{u_w;m~&5w&c>QP(w77zz(bsyIl?%4 z^eWRI3(?MC7sUHrU)?HbynV47zL-joapqjfWG>L|7=h9K*9&n>SE~7tNKv&$wIA-b zGcC$;?UJuk1hxBB zC2B#5tsrXm4|gaAXFGhm-~ZKa4!Sm9*WVx<1%dr-Lwt zyG8*;-({0e%QD7UHoj3$1(O^DB}(3uPeSn3$#esOcM1JBVPdqlz_PFOgOLn7K=ffK z4GP;ybwN;cl{(Mb45PR<^vG_$X>B}>D)iM1oHcg;RM)<{#Bu$k#Az3%o}BvwQRgP4 zg_leTx<@pVP=QxbEiVq4}ONSPVryuR^hGARHyFnB?-bwNdh+Vtki{UyUZQk9n>|l5l+ar zGZRYY9!&+o48Ws+6WMw@c#PicHTjHCVm7a;PEQENbZ#H5Hh~gGxTm|~onp1$hPnUa zJ!w6bn{bG1&X1sY&@b@^vgoT2ejT}(bI9ml@@V}D4#XxO>%}4mR&gm-6qjm!ED^>p z^o|3*Jak;)CNw2U9YmrWG9i71s5ebg*T%xsGZl^%A{#j?rcJiPrIUNDXe{E`ac$~) zMWRC6q_64|RAMc?v{2X`TU9M#%8Px`6?`^VCS^Iq=G4VNA&L>hajpbrT}4~n9T08N z=H=hFChCL6I32KrK{zM)lFMPdrZq!G#=a}aK7+gRR3aq?a7yOH1ev9KF=`BB6wJgb zN%;;!ASvIA>DY-qg2%Q21A_`<8#rs2ZX!L>IZsZ&0QVY<8HzYf zG{@ZX(|#u)P!{lcdgvsp`0X!(aLP z%?jfJGTd)ZyP(fV&OgSPkxY9fH~Myr#0pItGmd|Ka@xwh_phS>#V|vZ4{k2_lj#3q z>n@|>2-=3y|RM4uKFLFu2>`?(S}3fI(+qhJnrd z?r)v5_gUw6_m5t^s=Dv0y6P#FP!?V{hj0ykR1U)az1AcxzQ@=e8^YMvh$h=FSP^xC z&dwa9eJsAl1Rx$sFKE!%_0BZ```Gj^>#kSlX1Bo4_7)8ygA>t#B%@GZzF(a_ zgpLR9Vc*7XQ-j6AIs20+cld=$Ui?6uuz=Brh*>60k5zXnY^uvWwqw!A)cPI8qiu(x zDP?-lFES~X1Zj-#vCI0yqzjJbpv5*4ei;Qts&Fzn^BV_<|)&`;ZsxR2kbcup0~OhW^Y)n4IF7)6-7UT`J-j3hx+SK!g}iqAcuC>e`OBgxzz{?|Zj!JUf?xR7ZYk8ISqSe>c61%>#}8x~3MXdU=ph^KY`WkN5pT#gE&X0+ zszWm;zk1>^tbZO7UNuk)4WrKN{mk(8N`@>`_YEh$1;e-*+9(mJ?-j_u^j<70DfT=|IC%vEr}%h_fww63i#Z>z=2A_cMMfkadjI6V zUwAL&#`w-q6yUXF(^mv=ys_|zki-U5;`VNz*e|E5PqHe%q|N`U;eLwsUpQ@nV_bxK zuGofNvmX?*&oD@jQ?p~pa7+85`4M`vZ@n*mBkB0E)PsO$#HsH*CD-tEV`MqmxgHcS zF3l5hU{Gbo&10HByqDCMkmBHH6-+K%qkoPf<0|B|4nxQ(en7B!jdqw=W(j=vbc{Qe;Dr`xC#L2n)pT^F3ul+rv4klq_!vRBBCA z4S%bXb5;%fThw{;a{j`s*+^fMnKz{lZ`EkzoY`?CIkx;OmU8kBNpTE}TIsuyw?O%A z2_r_?nQBoLML|Fj4Uszq#fQIH!jIZ@IZo&X`S^cE@3`EFB;tQZDju;aVjs(T(wW9s z=9OK=r6&u8m(5Zs@YO{qJ7VeCeN^Wl#}CY!byT1vSQf)8+QwW)_r;!%vrU6>jzAMx z1cUTmA&a{*aq^D8j^m!qGoX8FtI>X$m?5)^-{KxBM`{{Jg(AcMXgKtO zlDewgvd`*?kOB6O`1r&6OLvFY9O`369MsaF%Hz$SZH8_;gi&!EdaXAqJ9n>oMNT)} zAASx42fWPD(5(U_4!>YcBuIwbzsVr|SEhcV)*ZwtH;=LRVRO*<^=6_sn`h+0T3VuE zory*D-db9n!3eT-7y4Bppk(72n)ZmdGP?du@=ZKf{Tr-NXi%*A9~AdP_nz|-|4}jc z0yLZWeOTcJ<4e3`uflpu2xa?#e|G8J(X64wd+WiN&*YKfT?VA~k|jq;L$5KjW1K=< z1U?lKBN=CakzWFajNn1d?PjB`=^U4f_Z0oo27T78lj%ssEmNsrDAg^+sAJ z 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4c17c3f2f099470e178d1aac30fdc24049fed024 GIT binary patch literal 1866 zcmV-Q2etSgiwFP!000046Wv%%jO0cY?j%G)0>KJo4G3w~@|E$}?zY|CBds)K*$qi{ zwWFOtq(o78u6l0QOu5QaRqmPI?Ui!izy*l|CoWu&xNyml1Bg=)LV_a_|ByiV4csD5 zi^^`#_IO7-+eFzw=Fnrmdf)r6SFe0jlE_i4;B@{qOT8?G0VYsg{Pu@?J^zaumG%qFK^^2E1o+Q`f?HDh zLQ;ICkhJ2c{jE}zg)i{f?1b@Fyfu79ArRwHS?q+9t#B)B2<^+Mx6`7m5T7z_{Ip`6 zE*g1@+pa7PE7&cxR>BmYDN3n~cDG-0c|{mSuU6@9x)cn0nb7X0%OwS!duX2(6cpS| z7lfhqwKg!7(rBZgvUQA>^s0L+kfvk=7&cpVRFz~! zxhjd58)FNq4VsykNI2*^KTGfMxJ1PY6sAejk}OLU200dJ)ZB__ljc>-CsNgVS7|5p zX&~jOmZ@$lw5XW3L^Z}YH3{q_@eLupk8rOv8V+IuyeflqA+$@#Y~idzGDx}<8@W>~ zT_Mx^2(km2+R9p$k#`|Ev*ohN!fb|55xP>Dj5{oxb^YNi?ebI|LUFIvG;u7u#;N-V zc6YDZrn?;4Zj)6G)3w87U5x1xX1W{uBj&!m>6S3XE5`qPmW3^|-9#O0`FGzKO>fVYbN^*V^wUG)M zQ(8ko<1U!o-F9u~J0`rM_<78r;^w-v8}phZz)`!~Mpd}!g4Squ6=v+X!BD2{p1ZL; zm*_PKl!@J4t_>E?h)jNUPe7@2X?hzMUVYO3e5kkK zJki_uh#ifteY{uyJ-*hM^c6dRynPe3fB3hJS@=J>cj8K`9 z!l0lFg@+Ytb7^4<*(02xFrM9r+G}K9e52RvouyJpRRBLxNwGvhQIPrb^AayDP#6D= z8C(9=*6FG2JsI#CN9(cCqd42z*B9UV0Q(7L>D38d`uai6DaDx&MQPJ|@tsGS(+BLg z_x{SVtY{@GgUZ(PzaOdZTmJu+_n&_3QFg8O*LPWzj3f1T>I-Z$ohi%|>fEagk{dTR5Dr1&%nY=*c+d*Uyn*<6DA%W$`=KVCyXE=|Ar zn9)~Qav*3Yz*f@t){sPGQNjuG^)-sv=_&d=90XCG$JNN6op_lIUE5%z`KU%}Z)3Qp*8JV|ozO^c+zFO%;?fi0UfAV9mWdo7{4w8^2 zX`01!F$`%!G7M;#08WNsIvSFALPr>oU_2qgYcw2=l6Vx2(<~Z9ad19@gefEg8V_kY zh{$AoJ|c|9BQhAJ7>t8>lB5rTfFus1kPZkSMk$_;fM7Hjj|anSG>PH>lk*WVKr~En zFc}4Okc_kQ5r7kz(3HjjnT*p6&PQM{8Iy4U>1ddaVfYaFh=Y(sBpyQ&MP!gZM69AX zn9w8)qGU9T2U&Q&RT)8qaGa1aCK19zST!KyV2~sc9g%nxj>-8tAf}@v01AUKjV96L zT*TK0=`abyC>SU4Fi9p!(ks6^c={Xjr~cpmUwHM&FK;-%bAIjo()pG13+Fe^Z=K&e ze{kM#-gN%x{KHKTs^_T69y4MRn@zIU{07QUaos$>< E0GN%b!vFvP literal 0 HcmV?d00001 diff --git a/frontend/.next/cache/webpack/client-development/0.pack.gz b/frontend/.next/cache/webpack/client-development/0.pack.gz index fba978790ecdfcef3f8ad7a7b2d45f63c3dcee40..c8e657d53ad46cde3839532a1fd8b9578637dd4d 100644 GIT binary patch literal 1045 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 Vf993H%kRIt^*426Y3W7{005i7ZSepA literal 1260 zcmVF&rVBy%9P@5Q^W+pBaQrOOQo`o7GyuY*C-fe&O z)FKEDVw3IfTyO8RcPe0USUIh#a!SHj5{FSCRXnTo%}#uiv&`f%IxnG*ce31ACcpE< zSA~!<2Bk^A<;TDnB%1t|uS^V-2I$@`WHI|09K=Yn zuR|$JscAn&h!qkQGF%v{VL_q{aGhdJDOy3{)hi(xC7t2@6dSZ_FQl1*mDK1YN0KI? zd$pMa%}EgdJ@!7iy{S=y+s}hat1I-P_UD#-a-;7W-e(9 zAXmVNvDq3Za2>G@kE|IJ+O?!Y*I*i!9xrSURglpdY)V6QbyY0L`wf7GN-{QmqM-`r zl&EQXK^Ozo!>fr@Eu(?us2-@zMa{U)MUXPPhFT>D z;JOac6p5Qa;u@zelECLp#AR-UrKzO<9AFwEeb-ra9cfb}m9AatEYekY1!!uOX1Bqj zWHTNmqb5($0J8H@Q{^#lI%l5)+}BR92Q6A>k&Cdwjh<%{hk6!{Z0aku;rIS`wPKxBzf93Bv~5mcl^jO zn=DY6xcQP>6BojfZdAjeX9_Fu0CHMtDg*4Xm4Xl4Wa;Qasn+ruxJl=%?#}25D|3Ei z>;UaKg@iRq9U`%@P`R>j6bQS(so(M4 z_hv}h|M8|Bw%e=A*-P2;sz5`?^Pr4B#!>r-wm-P=Dr0>?nO^>;D@>M4g>krFZok!N zx0qO@G$>dqsy1R<4HK804_|3^+Tm-KpZDMV$n$>X+dock{M-M=v&)w@|J?j*^Y6{G WE%}L8{;s_D&elI&pH=oo4FCWDcw|rj diff --git a/frontend/.next/cache/webpack/client-development/index.pack.gz.old b/frontend/.next/cache/webpack/client-development/index.pack.gz.old index 6baf498344787ae9cc96bf5d87a3fc9c96098c71..e95a9993e32a36279dcc7b163de9f6340c81a6fb 100644 GIT binary patch delta 1208 zcmV;p1V{Vr3G4}wF98yfFd2Ur{}KNJ@uIF&_snchXACGLkU8{p{oeOSzk1_bT)60Y z-pB8H-XCXYXYT!tjbFwXH{Gen78v`}9~^qqXa=d#ZvXt2 z=Y?MrS_(5v!46W;8p4NQ;n|09K=YnuR|$JscAn& zh!qkQGF%v{VL_q{aGif*O(|MI;ngc486}Ui3m%T+E3UUT8Nl>tps@_5pDP}Hd3m{j(iLu!l zC~zII4v(xE6WX<;Lf2p#mL4x`4^@!S8f;2Kb#+xN$oma|hDv`jHhrR@3g(okX?j5z z1J%Q;iBv74f#s+ksLe&qxXne7GP{OaB#oyz3I$ZwXC56+aYKQ*I9KPX;UPXu3hRZ(p7i`Xlj*ax51)hGae6BojfZdAjeX9_Fu0CHMtDg*4Xm4Xl4Wa;Qasn+ruxJl=%?#}25D|3Ei>;UaK zg@iRq9U`%@P`R>j6bQS(so(M4qCmCiD+S612ba8|F4Y}%GU12?K z`7(n(R*t#lqRJAX9id%Q-->XE30l+wzXD2opxA%!_z!PQ??qAZ>7#ph2aj)ma|NQE zj>E`cltSS(8~@HPI{wPWNUnSfLVY zG^2lxXBLMA67S7iBL|6+{geZtBx(*&vz-Q&>h0e=&s%3%8=HYzT8=SC9itZ0htnMQ zW=Prp@unTN+pEjjOWE_PKtss$po~ApQTvFtKe+HJV|_uHUjC*lOqNT9akyV@ztw2B zm{_DVC|D}0Hey>16PKM2UukyQ;cJ$k_unl1$n$>X+dock{M-M=v&)w@|J?j*^Y6{G WE%}L8{;s_D&elI&pH=oo4FCWk;!isO delta 1209 zcmV;q1V;Pp3GE4xF98#gFd2U|{{r#g$4Yh2%=UDKfIT5eSh?;H_pX{i=O9w z{I2Kyadvj*-rv~xb&OFCVh}1Bg#tv1VHd>Uwp4i2oqBA6u|NIMp*M|YkQ(jwFK>BX z_%)%WFvAq=AO)=zY$11q)R=+e+y~7;j3oOyl){vn_EUsd zAyFa2g`pZ2B+3BSDb|0Kq7@Wgy%Lg9(iz@Qu|d1`LYgU9NsUf&Bxw@5hdZ#v%-E$# zJ2@(S&k7l4z-T2X$L7wZCqWIhC{l3QTjZf2X8@A~1v{zgEhLd*=90Dmas`|io2`KY z*AeUR$eJ;sT}vu-4W?n~@xt~{1sScurZiMnSH*(7-vDT+Bx8TmCmO0?PKlbP7lbiT zJ-nJo)iN4bj_QHhT-1!)Tm&hzYp6xic$%Y7KxKXA(cvUVD>Kwwm1z!}95tiD4B1Yd zibc_aLWaWF(rz7F_9sK6(9TP}=+LZ?!Cb0xzUoPk%feSc^c-;B8Wo3d39jo9O_8_> zB(8DlA_;unL|lL7R#=)!`p*HTA<}o9Ro9U=MN;Y7rOqNFDTLGClDb8yoSPMR{@1V+@%{aE_nwfr3Vcx%F_NxI3` zdE#?9Z^~hDRK6aOV`K|bN!{-$pw#aqpa@%jIkdkJEJuHmr(HvmrO|%Jj|{WP0)>g2 zFS#{wAsp#OH5_`TumTStr=_Mcz#dyE_`pq;jxLmHEw6!_bk6GTjGnME=SRj4(4JFB zSfkV-5*rJZD+@<~unU~}9p6p9iBLt1Ut$7<;sfJ8%=<=wLh?Io3vBs7gxs~5$Vnv= zOLE`bgZY2G6yG-xOWj}aOS%>J@vzV=GhB_Ty9~c;O;ToY$uPgHm**7d5XV-s^JMNj zjfFFYc44c$hr!C@^K0>B?db2ExVmX7XLx5FOL%gUVHTl1J@ro)H>lr`tM1hm*3*_R zGw5UGm|HHYED_of+BNm92#1)UMJ@0vptJ{y{f>YC@YeKR6cwL7x_5W*`1UtfAlf-T zID%1eefL54+T91?V5F28J{}yZ+nI^-PsDnZ;ET&2r#$Z}x%}DzQd0 z>Ue)}5eb(XcU8K|Y@7<1GyYB7B{&2evr zl>HxX+F`rBx}3d~J+BHhggg(*_+uQkk7)aY3$HTP7nJGcZ@R){xl|a3`{njqjdqKP zMM{H$rJ`yhw$(6k+4=C5W~Uv#X8C#l&5td-{LgQ1|1`bfJzM|3@$B-Y&A&GP-u!3t XY)gLPmA@UawMhyS}&k<-{ 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/lib/auth0.ts b/lib/auth0.ts new file mode 100644 index 0000000..85bfcbd --- /dev/null +++ b/lib/auth0.ts @@ -0,0 +1,3 @@ +import { initAuth0 } from '@auth0/nextjs-auth0'; + +export const auth0 = initAuth0(); diff --git a/middleware.ts b/middleware.ts index 3b828c5..7686e4a 100644 --- a/middleware.ts +++ b/middleware.ts @@ -20,8 +20,10 @@ export function middleware(req: NextRequest) { const hasSession = Boolean(req.cookies.get('appSession')); if (!hasSession) { const loginUrl = new URL('/api/auth/login', req.url); - const returnTo = `${pathname}${search}`; - loginUrl.searchParams.set('returnTo', returnTo); + // 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); } 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 8702679..828fdf3 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "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", + "next": "^14.2.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "sonner": "^2.0.1", "tailwind-merge": "^3.3.1" }, @@ -29,8 +29,8 @@ "@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.0.0", + "@types/react-dom": "^18.0.0", "postcss": "^8.5.3", "prettier": "3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", diff --git a/pages/api/auth/[...auth0].ts b/pages/api/auth/[...auth0].ts index f79a2e7..8d4a2fb 100644 --- a/pages/api/auth/[...auth0].ts +++ b/pages/api/auth/[...auth0].ts @@ -1,4 +1,19 @@ -import { handleAuth } from '@auth0/nextjs-auth0'; - -export default handleAuth(); +import { handleAuth, handleLogin } from '@auth0/nextjs-auth0'; +// Customize Auth0 to always use Google and land on /dashboard +export default handleAuth({ + async login(req, res) { + try { + await handleLogin(req, res, { + returnTo: '/dashboard', + authorizationParams: { + // Force Google as the IdP + connection: 'google-oauth2' + } + }); + } catch (error) { + // Let the SDK handle error responses + throw error; + } + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28ec28f..241216a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,13 +10,13 @@ importers: dependencies: '@auth0/nextjs-auth0': specifier: ^3.5.0 - version: 3.8.0(next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) + version: 3.8.0(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@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.0(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 @@ -34,19 +34,19 @@ importers: 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.3.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.2.0 + version: 18.3.1 react-dom: - specifier: 19.0.0 - version: 19.0.0(react@19.0.0) + specifier: ^18.2.0 + 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.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -64,11 +64,11 @@ importers: specifier: 22.13.10 version: 22.13.10 '@types/react': - specifier: 19.0.12 - version: 19.0.12 + specifier: ^18.0.0 + version: 18.3.24 '@types/react-dom': - specifier: 19.0.4 - version: 19.0.4(@types/react@19.0.12) + specifier: ^18.0.0 + version: 18.3.7(@types/react@18.3.24) postcss: specifier: ^8.5.3 version: 8.5.3 @@ -97,9 +97,6 @@ packages: peerDependencies: next: ^10.0.0 || ^11.0.0 || ^12.3.5 || ^13.5.9 || ^14.2.25 || ^15.2.3 - '@emnapi/runtime@1.3.1': - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} - '@floating-ui/core@1.6.9': resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} @@ -139,158 +136,59 @@ 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] - - '@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] - - '@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] - - '@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] - - '@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] - - '@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] - - '@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] @@ -372,6 +270,9 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@tailwindcss/container-queries@0.1.1': resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} peerDependencies: @@ -473,13 +374,16 @@ packages: '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} - '@types/react-dom@19.0.4': - resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} + '@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@19.0.12': - resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} + '@types/react@18.3.24': + resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==} '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -510,20 +414,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'} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -565,9 +455,6 @@ packages: 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==} hasBin: true @@ -578,6 +465,9 @@ packages: jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + lightningcss-darwin-arm64@1.29.2: resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} engines: {node: '>= 12.0.0'} @@ -651,6 +541,10 @@ 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 + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -663,24 +557,21 @@ packages: 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 @@ -773,29 +664,17 @@ 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==} @@ -811,13 +690,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 @@ -886,14 +765,14 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@auth0/nextjs-auth0@3.8.0(next@15.3.0-canary.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': + '@auth0/nextjs-auth0@3.8.0(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@panva/hkdf': 1.2.1 cookie: 0.7.2 debug: 4.4.1 joi: 17.13.3 jose: 4.15.9 - 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) oauth4webapi: 2.17.0 openid-client: 5.7.1 tslib: 2.8.1 @@ -901,11 +780,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@emnapi/runtime@1.3.1': - dependencies: - tslib: 2.8.1 - optional: true - '@floating-ui/core@1.6.9': dependencies: '@floating-ui/utils': 0.2.9 @@ -915,18 +789,18 @@ snapshots: '@floating-ui/core': 1.6.9 '@floating-ui/utils': 0.2.9 - '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@floating-ui/react-dom@2.1.2(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) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - '@floating-ui/react@0.26.28(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/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@floating-ui/utils': 0.2.9 - 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) tabbable: 6.2.0 '@floating-ui/utils@0.2.9': {} @@ -937,170 +811,98 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@headlessui/react@2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@headlessui/react@2.2.0(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.20.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': 3.13.4(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) + + '@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 - - '@img/sharp-darwin-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.0.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.0.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.0.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.0.5': - optional: true - - '@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 - optional: true - - '@img/sharp-linux-s390x@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 - optional: true - - '@img/sharp-linux-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - optional: true - - '@img/sharp-wasm32@0.33.5': - 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': + '@next/swc-linux-x64-gnu@14.2.32': optional: true - '@next/swc-darwin-x64@15.3.0-canary.13': + '@next/swc-linux-x64-musl@14.2.32': optional: true - '@next/swc-linux-arm64-gnu@15.3.0-canary.13': + '@next/swc-win32-arm64-msvc@14.2.32': optional: true - '@next/swc-linux-arm64-musl@15.3.0-canary.13': + '@next/swc-win32-ia32-msvc@14.2.32': optional: true - '@next/swc-linux-x64-gnu@15.3.0-canary.13': - optional: true - - '@next/swc-linux-x64-musl@15.3.0-canary.13': - optional: true - - '@next/swc-win32-arm64-msvc@15.3.0-canary.13': - optional: true - - '@next/swc-win32-x64-msvc@15.3.0-canary.13': + '@next/swc-win32-x64-msvc@14.2.32': optional: true '@panva/hkdf@1.2.1': {} - '@react-aria/focus@3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/focus@3.20.1(react-dom@18.3.1(react@18.3.1))(react@18.3.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) + '@react-aria/interactions': 3.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.28.0(react@18.3.1) '@swc/helpers': 0.5.15 clsx: 2.1.1 - 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) - '@react-aria/interactions@3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/interactions@3.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': 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-aria/ssr': 3.9.7(react@18.3.1) + '@react-aria/utils': 3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-stately/flags': 3.1.0 - '@react-types/shared': 3.28.0(react@19.0.0) + '@react-types/shared': 3.28.0(react@18.3.1) '@swc/helpers': 0.5.15 - 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) - '@react-aria/ssr@3.9.7(react@19.0.0)': + '@react-aria/ssr@3.9.7(react@18.3.1)': dependencies: '@swc/helpers': 0.5.15 - react: 19.0.0 + react: 18.3.1 - '@react-aria/utils@3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/utils@3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/ssr': 3.9.7(react@19.0.0) + '@react-aria/ssr': 3.9.7(react@18.3.1) '@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) + '@react-stately/utils': 3.10.5(react@18.3.1) + '@react-types/shared': 3.28.0(react@18.3.1) '@swc/helpers': 0.5.15 clsx: 2.1.1 - 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) '@react-stately/flags@3.1.0': dependencies: '@swc/helpers': 0.5.15 - '@react-stately/utils@3.10.5(react@19.0.0)': + '@react-stately/utils@3.10.5(react@18.3.1)': dependencies: '@swc/helpers': 0.5.15 - react: 19.0.0 + react: 18.3.1 - '@react-types/shared@3.28.0(react@19.0.0)': + '@react-types/shared@3.28.0(react@18.3.1)': dependencies: - react: 19.0.0 + react: 18.3.1 '@sideway/address@4.1.5': dependencies: @@ -1158,6 +960,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + '@tailwindcss/container-queries@0.1.1(tailwindcss@4.0.14)': dependencies: tailwindcss: 4.0.14 @@ -1232,11 +1039,11 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.0.14 - '@tanstack/react-virtual@3.13.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-virtual@3.13.4(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) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) '@tanstack/virtual-core@3.13.4': {} @@ -1246,12 +1053,15 @@ snapshots: '@types/phoenix@1.6.6': {} - '@types/react-dom@19.0.4(@types/react@19.0.12)': + '@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 '@types/ws@8.18.1': @@ -1283,26 +1093,6 @@ snapshots: clsx@2.1.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - optional: true - - color-name@1.1.4: - optional: true - - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - cookie@0.7.2: {} cssesc@3.0.0: {} @@ -1322,15 +1112,12 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 - 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.3.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.4.2: {} joi@17.13.3: @@ -1343,6 +1130,8 @@ snapshots: jose@4.15.9: {} + js-tokens@4.0.0: {} + lightningcss-darwin-arm64@1.29.2: optional: true @@ -1394,6 +1183,10 @@ snapshots: lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -1402,27 +1195,27 @@ snapshots: 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 + 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 @@ -1465,63 +1258,33 @@ snapshots: 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: {} - - scheduler@0.25.0: {} - - semver@7.7.1: - optional: true + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 - 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.1(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: {} From fcbcf4c26050622de98dabcb942346a5d0705004 Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 15:36:37 -0400 Subject: [PATCH 05/10] update error res --- components/layout/search/filter/item.tsx | 4 +- .../webpack/client-development/0.pack.gz | Bin 1045 -> 1032 bytes .../webpack/client-development/index.pack.gz | Bin 1259 -> 1261 bytes .../client-development/index.pack.gz.old | Bin 1260 -> 1259 bytes lib/shopify/index.ts | 70 +++++++----------- package.json | 2 +- 6 files changed, 29 insertions(+), 47 deletions(-) diff --git a/components/layout/search/filter/item.tsx b/components/layout/search/filter/item.tsx index f59f7af..c62aabd 100644 --- a/components/layout/search/filter/item.tsx +++ b/components/layout/search/filter/item.tsx @@ -38,9 +38,9 @@ function SortFilterItem({ item }: { item: SortFilterItem }) { const searchParams = useSearchParams(); const active = searchParams?.get('sort') === item.slug; const q = searchParams?.get('q'); - const params = new URLSearchParams(); + const params = new URLSearchParams(searchParams?.toString() || ''); - if (q) { + if (q && typeof q === 'string') { params.set('q', q); } diff --git a/frontend/.next/cache/webpack/client-development/0.pack.gz b/frontend/.next/cache/webpack/client-development/0.pack.gz index c8e657d53ad46cde3839532a1fd8b9578637dd4d..801ef27d255dc31969f48a156bc5875ea4c8044c 100644 GIT binary patch delta 1023 zcmVZvkPtSR@Az3xfe+Z^7LhLsHxQW~Q^<)ooWzc1Pwg z_ye1RxR@Wsiy+8C5VA3A4)G!$y@+l!hzKfb$Vm|dFUC+cW~oXxo!#i}RL>lGrkk$! zzW2R)?^V5;#Vl|fgwXLr2#wWN7hyi5!2*e+c@of|4df^d7JpudVmKo2W<^c~w7M(u z>5-HPU}8o%}PzA@7CIq@7swGAi2BJ#vgg&Pf7o znlH2|W0cI%oW7i5zrkHM0tH{+<)7WE*GfQdRVk$39@(cVpj&{mpR znkO{g)Eq(vEPn(RbmzJmO!t5%kAlW0zE4;h!igjeJ27zLXDmyIMA?`+>>u`@J&^+q zL_JKC#?*8Els`3iaQDWzGet=uB#gKq149YTvbwTAgwy^h?j3aY($)eu?EwR6l0`9u zq@h?DaGFCi2i~rCL=^J@Q+bDpC;`dN(|gK}PfEs5?SF&&!+VRsdHg`_UHwHF{j*&z zAemv!ba|H1jJH$Kc=aGclj!#=Pt2U_K9Bb~s|9q(q(WQLNhz!Komziv-!i97n2Qov zra{ojY0y0*{m2DPGItVvdRfUulW1*C{c>GgsdyhDgbMWR{foR8Ef-fG^Ue;@w?!+u3wGj?segayX?5#Gu%B3rP4FepR+kEUU3!Wy zdGA;o2`RG{+#=u>jD>L32yK2#li9e?ZM>dF1y zrJMY^j}8YvNV)&nuu~v6IoK$OE|q{i=>pmRVlh$7RL2j z<$rxx0@McJ;jfjkzHcp71Lk>~VCu-e{~s&IAb5f6(m(%i(sfavJ(?(8#MSMK(1&?n+2lN< zR%@s0+pKfi+49#TX_o8(d^4Js$u9ATnkG95BRU_&$U05-BW{dlsHv94h}tH*8WV3I tgzh_JTv;Fd^QG$Gi};f2bzeK6zJ9`2RkQm?b^S|?yaVajemrR>008L?0?q&c delta 1037 zcmV+o1oHcc2$cws7k@f}MH_@>Z-OvgF0uy?3xk1>+=9E_gx%WiH#42>u5P<(vO6+| zfhbH4;^IGE6hZc&VAQB!ZXUe|VPilPRMe2G7>x&is2a0Yk=@R2;_g(>9Hyt6uJ^w8 z)%&S>HLF?RI0&KeA%rGs8>_IK(O`wd(J~2W&<1jf1}n#-7=Mn*yLpjQ0j=)3e0nTp z0+=`{AmN8={mIGePXJHjGmxuyPsN>9#CmZ`L&$p}G-)Rmy^M^Jp=j?aLTEe9EX@-d zZ)px80~P`cx_=AZ4CZ>klRH7<6W=E+4Ph}!!%hsG_!-L*B2hMGX8l?J(Zw8SAnIY7 zG-e+2XZ)GrgPS+ToheERAz{P?85&AxmerO0A)N6~bML5gkhT^~(*ZD$CRr3iNE(Wj zA*VSsbKvcIM?^6nGL?6jh!T+OJb0k&_@rd~)ZV!@ynnX|ocoT{-q2r^(Ldki0+N%g znGWU|&3HQ%jk8A)nnrIe&7OF<`#3)2Y!uKTlL~FgNlIC*@6`GehX$NBVIfLjK!c!@ z)1Z4!&LbBz$=qr5-J+6`S2o>nrf45xjL~%&A_)*!7tLQJvQ6HBQ zwv4Ga?SDkBJCygDT^J2)FW7-i$22wV;)-)&w_&6Y9Xy;woJXwX*7_9(|LJ1K!IOpib;$QJ~{2Trq-@t!(FMo}2qKlSyuiA-A)`5}d*zH%uequ2; z!Ph)nr&QQGrKkCt_qw%_kTPq*tx}$N>y|KQr04-cF0MNGXM9yH8GiaNVXIxl`oFt~ zs)H}%uVunM?7cs7L-xJ{9r^Yo*fVCC+&?NG)hh9q5w|P~Wpoo!PB)Ro-D^?&k0B_jsDhAfKt;K4zsDPe0eO*l0AU0#?vy{A0AWFWG7)v{ZWkVq{)87mGKNU z)v_2<+hkW`>Uo5ae%7d3*N@QQA7H8ueixrtUG9rV)a3{KscLqAufA`nk^cafZ3bw^ Ha3=r&N*@h| diff --git a/frontend/.next/cache/webpack/client-development/index.pack.gz b/frontend/.next/cache/webpack/client-development/index.pack.gz index 0fc8f5e5dd7033f6408f5dcb26ed99902728bed0..62b02560aaa916fe0e364553a9e9bfc3f44431ef 100644 GIT binary patch literal 1261 zcmVV>Jp zhfnk&w1UDbmqRj2I>UP@HfUF0NHYa1snJP}Buzs1 za0j-S8M`!TCx@l)Ss}v=7_9{5*xb4FB&eYlMG7u^i#!zM3}BL=U?)|*g(OnUT+$Xm zu7DF`vo%oQI$|9jSu-ZIYe|K!!89yAUf3S0Afq+dl!ofcs#uWs8vqTJWNi9GLlw*^ zQPcE-Fb1lJR}-mPMgz-HJy4sAnsJ+pAZ2z9wMZIIa})}wtj|0;oaAU_hMKD~&0&+H zW>lCV+o@BrC|Xd+P#9a(9|l;PJ>0s zW;{wpO`f6wWM`wM%46Pi&OQgYueF7eK*76#-L1(P)&gcm%bAD*- z0PPutgf&VXAhEGfxw3E+2)n?k-|^k#n+R3J_!TBlDBd^j!@Oq%C?vnbw!oGTM95u> ziJVk2u_X7+-Jjn}@jU~v)cpm&q?>Uc4+_mP!_}y|%kaC_BxM$t4D-u+c}{^2aBL+z zPv*YUSU6*77q-fK7_2-xyB3evj{eTEtDB~BhIiJngvZAjW)a$x6aQp!gZd4*>Rw%8 zJ!$zegFaS{x#gnD5}_TTT~ptRaDWL~)B?W-N_(K#@AwaHPVYuh@!7+>cLtAceR~Px?Qp101jHZ}vbv>aoOIz}y~52iWp z&XBVI<4rqkw^x_5m$K(ofrgOhK^cFHqxKYSd-?kwcN2VZ>i$!Ecz<}VOm)RpR`#Ai=nbP8 zq(*z`=eImB{F=~Gm|+Tbkb>3_J^~9z#)R6$_$)JVp^(CMuJbI^IN`nRo%T-qi)R)= za1fhpZ~Iz%yS-fji^IxkRh3f`#*#RU3aR2*rEhlZo1A4PkI;DueY~6H#xnVxr@ktL zj4>!p`Yk^O#vsw;w|r${pfo`DZXt`oZ~34(cE{4e$QUNJkUK(Z%)oK(f#x7al6@UY zVM?f#NrHl%RP`2;NHKFsTL8HN zPK?dgK!NLsb$DdWn9!~z6}kq~u=IFgd#Hkp)?iZ_sw=BvLEdiwG*ptY=@SiAFsDRK z(+k2Fs2*NTq-q%rEJyV~Z7yoYZ7zb8*)`N6X*|tQD4?=F^XPDrqm>zIuF5orO^%vT zVTNp{PQ{{VK_Nq7Y-zWSE&G!pQfTL;UUXit28?e7A2eU zC>b?*iUyFKjhZTtdDA)j0^q*xx^L9vAorQnIk@QuCrue{0wZeXek}cOTYe6GyftCe zB;DleJoUMpH|4N6EME`FF|q}zr0(|=Q0jLQP=qbN9NJ$AmLtj2t|7_NXusn}hS_9+ z!oK(XKPAKjYXkD}u9$M^3Ip4|TCGDO=) z`-dCbPI|qFD}{?#^DaLM7H{ zMjg*A4htmSow-I15+(a72SQ2I9H3@94Jy@_e)T+Won>uo25MH(g<}Tq=yiy>k1lM!Utt zBBepWQc<-L+iIA&?0ocEv(pa$XZiWy%}>0K-@bY0$LaNd`rmkd@xtaGn}2TpwRyUg Vf993H%kRIt^*426Y3W7{005i7ZSepA diff --git a/frontend/.next/cache/webpack/client-development/index.pack.gz.old b/frontend/.next/cache/webpack/client-development/index.pack.gz.old index e95a9993e32a36279dcc7b163de9f6340c81a6fb..0fc8f5e5dd7033f6408f5dcb26ed99902728bed0 100644 GIT binary patch literal 1259 zcmVYSd-?kwcN2VZ>i$!Ecz<}VOm)RpR`#Ai=nbP8 zq(*z`=eImB{F=~Gm|+Tbkb>3_J^~9z#)R6$_$)JVp^(CMuJbI^IN`nRo%T-qi)R)= za1fhpZ~Iz%yS-fji^IxkRh3f`#*#RU3aR2*rEhlZo1A4PkI;DueY~6H#xnVxr@ktL zj4>!p`Yk^O#vsw;w|r${pfo`DZXt`oZ~34(cE{4e$QUNJkUK(Z%)oK(f#x7al6@UY zVM?f#NrHl%RP`2;NHKFsTL8HN zPK?dgK!NLsb$DdWn9!~z6}kq~u=IFgd#Hkp)?iZ_sw=BvLEdiwG*ptY=@SiAFsDRK z(+k2Fs2*NTq-q%rEJyV~Z7yoYZ7zb8*)`N6X*|tQD4?=F^XPDrqm>zIuF5orO^%vT zVTNp{PQ{{VK_Nq7Y-zWSE&G!pQfTL;UUXit28?e7A2eU zC>b?*iUyFKjhZTtdDA)j0^q*xx^L9vAorQnIk@QuCrue{0wZeXek}cOTYe6GyftCe zB;DleJoUMpH|4N6EME`FF|q}zr0(|=Q0jLQP=qbN9NJ$AmLtj2t|7_NXusn}hS_9+ z!oK(XKPAKjYXkD}u9$M^3Ip4|TCGDO=) z`-dCbPI|qFD}{?#^DaLM7H{ zMjg*A4htmSow-I15+(a72SQ2I9H3@94Jy@_e)T+Won>uo25MH(g<}Tq=yiy>k1lM!Utt zBBepWQc<-L+iIA&?0ocEv(pa$XZiWy%}>0K-@bY0$LaNd`rmkd@xtaGn}2TpwRyUg Vf993H%kRIt^*426Y3W7{005i7ZSepA literal 1260 zcmVF&rVBy%9P@5Q^W+pBaQrOOQo`o7GyuY*C-fe&O z)FKEDVw3IfTyO8RcPe0USUIh#a!SHj5{FSCRXnTo%}#uiv&`f%IxnG*ce31ACcpE< zSA~!<2Bk^A<;TDnB%1t|uS^V-2I$@`WHI|09K=Yn zuR|$JscAn&h!qkQGF%v{VL_q{aGhdJDOy3{)hi(xC7t2@6dSZ_FQl1*mDK1YN0KI? zd$pMa%}EgdJ@!7iy{S=y+s}hat1I-P_UD#-a-;7W-e(9 zAXmVNvDq3Za2>G@kE|IJ+O?!Y*I*i!9xrSURglpdY)V6QbyY0L`wf7GN-{QmqM-`r zl&EQXK^Ozo!>fr@Eu(?us2-@zMa{U)MUXPPhFT>D z;JOac6p5Qa;u@zelECLp#AR-UrKzO<9AFwEeb-ra9cfb}m9AatEYekY1!!uOX1Bqj zWHTNmqb5($0J8H@Q{^#lI%l5)+}BR92Q6A>k&Cdwjh<%{hk6!{Z0aku;rIS`wPKxBzf93Bv~5mcl^jO zn=DY6xcQP>6BojfZdAjeX9_Fu0CHMtDg*4Xm4Xl4Wa;Qasn+ruxJl=%?#}25D|3Ei z>;UaKg@iRq9U`%@P`R>j6bQS(so(M4 z_hv}h|M8|Bw%e=A*-P2;sz5`?^Pr4B#!>r-wm-P=Dr0>?nO^>;D@>M4g>krFZok!N zx0qO@G$>dqsy1R<4HK804_|3^+Tm-KpZDMV$n$>X+dock{M-M=v&)w@|J?j*^Y6{G WE%}L8{;s_D&elI&pH=oo4FCWDcw|rj diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 991a93c..2dce971 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -9,8 +9,6 @@ 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'; @@ -74,10 +72,12 @@ 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 @@ -102,7 +102,8 @@ export async function shopifyFetch({ body: JSON.stringify({ ...(query && { query }), ...(variables && { variables }) - }) + }), + next: { tags } }); const body = await result.json(); @@ -304,19 +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); + return mockCollections.find((c) => c.handle === handle); } const res = await shopifyFetch({ query: getCollectionQuery, variables: { handle - } + }, + tags: [TAGS.collections] }); return reshapeCollection(res.body.data.collection); @@ -333,12 +331,9 @@ export async function getCollectionProducts({ sortKey?: string; fresh?: boolean; }): Promise { - 'use cache'; if (fresh) { noStore(); } - cacheTag(TAGS.collections, TAGS.products); - cacheLife('days'); if (!isShopifyConfigured()) { // Return mock products for demo mode @@ -351,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) { @@ -359,22 +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 = [ @@ -400,10 +391,6 @@ export async function getCollections(): Promise { } export async function getMenu(handle: string): Promise { - 'use cache'; - cacheTag(TAGS.collections); - cacheLife('days'); - if (!isShopifyConfigured()) { return mockMenu; } @@ -412,7 +399,8 @@ export async function getMenu(handle: string): Promise { query: getMenuQuery, variables: { handle - } + }, + tags: [TAGS.collections] }); return ( @@ -443,40 +431,36 @@ export async function getPages(): Promise { return removeEdgesAndNodes(res.body.data.pages); } -export async function getProduct(handle: string, opts?: { fresh?: boolean }): Promise { - 'use cache'; +export async function getProduct( + handle: string, + opts?: { fresh?: boolean } +): Promise { if (opts?.fresh) { noStore(); } - cacheTag(TAGS.products); - cacheLife('days'); if (!isShopifyConfigured()) { - return mockProducts.find(p => p.handle === handle); + 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); @@ -493,12 +477,9 @@ export async function getProducts({ sortKey?: string; fresh?: boolean; }): Promise { - 'use cache'; if (fresh) { noStore(); } - cacheTag(TAGS.products); - cacheLife('days'); if (!isShopifyConfigured()) { return mockProducts; @@ -510,7 +491,8 @@ export async function getProducts({ query, reverse, sortKey - } + }, + tags: [TAGS.products] }); return reshapeProducts(removeEdgesAndNodes(res.body.data.products)); diff --git a/package.json b/package.json index 828fdf3..ed5ef8f 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 .", From cc46c31d53b3d637a6128fdee861984d2bc951ff Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 15:49:44 -0400 Subject: [PATCH 06/10] 4 error resolved --- components/cart/add-to-cart.tsx | 11 +- .../webpack/client-development/0.pack.gz | Bin 1032 -> 1032 bytes .../webpack/client-development/index.pack.gz | Bin 1261 -> 1261 bytes .../client-development/index.pack.gz.old | Bin 1259 -> 1261 bytes package.json | 8 +- pnpm-lock.yaml | 596 +++++++++++------- 6 files changed, 374 insertions(+), 241 deletions(-) diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index 243948d..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,8 +70,7 @@ 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 )!; @@ -80,10 +79,10 @@ export function AddToCart({ product }: { product: Product }) { const isAvailableForSale = selectedVariant?.availableForSale ?? availableForSale; return ( -
{ + await formAction(selectedVariantId); addCartItem(finalVariant, product); - addItemAction(); }} > LbkU1%}i&zt2^2}H>cIRym4i$7G2S*nsvXE$+os%H*8(@ocV z-}_#@_o`maQWiK4Lg?s=2#wWNmtY~I!6J#H1rpGp4df6F7JpxfVmK)8W<^c~w7RF| z(}O7!z{HGzgzv8P$H)J84R{)#gj~IQDDJc(){9dbLf#9ZNjtIVWmL4Md*m>OoRb9F zG+%5}#weMiIek4%Ni$bs!(I+2I>5zrkHNC2H{+<)7WE*GfQdRVk$39@(cT?|&_xVwSW$pRA@^&DP^_3Q|piITH&+_b5Q~- zGzdC54Z5eKAGx4O=1!v8oRW(sQR7wh%XM+3;(dh>D$uk2({iB`#UWYeM`bszqQ59d zeN#%m{}k}JT^UZ-|NGA9*HvEHno51>M8lOI zTzMXkKTwpa4!()+y14p`cV>vbEn3l?wG)?2{eMGGs~azZ{lsEyf-ifvx>VTf(&K#D z`^efzNSU?ZmMBlW)0Qx2WaT#ya>cxA7gdO1P!2_a4a9=l|P2R2_T;|Lo%G{BG~u z(9PNU)^q6ll3>f2Ws3j0d{nFSUq;-rG?a0gh;mL7SyC>PaC*xYNhyJ=QuJk67}sl+ z_kTqRP#b`Uzg9BKjREdGYuMCbO}woZBx88(TESj(y-o4x6AEJou4}++c+JJm82%go zsJ?$$Z;i}9=UpjgWvhy}=hVUU>LYvZy4BtfqhXj~)3@cb$-w`ZezRSZ%|QN`RBQ!u z1MsTdQ^mmgskK-Qm><{#Q%83Ff2!QAUGEusSi<=jr5A!bDpz{PZr!IG#qF>vB%EK7()*_b-)ANHR;kpm4x zJxr6v)N}rnKQ(x8_r|z0MM)tfjJO~JLkZ2Yy0Sln)BY*$9d!26)&e)}0Rw50MKOe= zp;#GknnN=O-mZ5<6!QU7d54K80m;tOd&-VaO2$v^gMa(OdyBw%{6Otp{Y4r5vt2GA znPJUzd6vFDXaCJT7PWcGN(_o0lDDQ2%FdEq2v;&)tX=2jF6=!|BVWbaTekO@Hk66pC^(zkk+r^H9 z#~Xcw5Uy6&3Pj$m>qHmlof$Se2AbD~h(-tUPXUj+mEm;#f9#EZUFEf%snmx~G+gVN;Ggj=7grzi&JNMHMJu`scH)w$e}Cv{b?Zg2pID4d@FmYymkN7bdWtW3 z?^qiNDYF*bBISv@79CBcp{%M^d5d{nFSUq;-rG?a0gh;mL7SyI+ZIK5?yq?Eu_DY{%1#`Rj| zeScU2)CS<;ua(SlV}N_h8a8!U6R)cU$r#?aQLxwCY*Re=gu>W?>l*L|-f*!qhHv99 z)%SOst&x*Yde@6t*{R~)Idw3-`oNyMZngKtXc%VL^hx<_GVnj7-)z@pGmt+d6=6Rc7>d3zTA1lWofp_cDKmTviby1%^nkZew)$NPWhk0MwTzI!*Q?Zj5HAsg}iv+9tai6K^1d s?mJ{$Ss(oKrRv~|_>$^%Upt__e!^E(v-?MN{Y#C!1L@d)JZUEY0Pj%(j{pDw diff --git a/frontend/.next/cache/webpack/client-development/index.pack.gz b/frontend/.next/cache/webpack/client-development/index.pack.gz index 62b02560aaa916fe0e364553a9e9bfc3f44431ef..da29a89681817dd92da1a11ae6801d7fcbbd4d3d 100644 GIT binary patch delta 943 zcmV;g15o_!3GE4x8h^;4g?Mm!IvcY>1{T)D1oN?E*og)bB)qPEJzYE1RXz1y&&Td8 z?7@Q~ur9}`N#3tL@zTJ`1A_yO3xv@-s z=Y^5{P!5|nLNZD^!+R+#Vwtv?-EG*DiGy=_dPt6uEl4GGzo&pwzmtF>Z29HT{!*|UNuG8MNtQ1;Q?H>UVrM`6faYF@A*!6pHta`!IhW8vzQ*@31Yf z;@z2R6SM9l$ew$q?eeeE~T^VV6`#%7?FmSfCO$Ed~h=`_dP8B+FNylIE+ z_Udx>TK2pu&=B%GDC3WD)IOwZA6Rf*y|Ve|=3kqCZ=P+*PrdSY R{efrT|O!F((kcA|lV5MEcmp01ths-AkU=VNyk za`52AnE}y zjg4Q%808=ap^{N3K%^LUK@4t5g*V)(#}*j-(;pmq!)OMn(K%lF`7O^2zag{~W|)E< zq@Xo~55U5aF`+gwKFv&AD5S8R>pTlJPIzy7r@hnu{Ha9{9KUP@HfUF0NRwFsVgbsNya8{2o&(NWqv8-Q!F3&?DH1n<#5GP` zB!MrQh|AmxOH)bzIlweT`mVF;I?|>{DqXwOS){A*3eeOl%}#?w$!0uCMopfg0c2;R zrpja9bk05pxUakJ8#Ot|eI|7dZu-GVQ-+(sh?=<{OaDpB&!LYuCybh;n|z(eK9}>R z92SRvIEL;Fj?awK`$H6&RY?RWghFqcLtAceR~c0PQi*=dKbS$^Js^JDMVCm-GZaeDpV{x_aoys-J_=3kqCZ=P<+PrdSY R`Mr0x{sA_QPdP>n002KW-J<{i diff --git a/frontend/.next/cache/webpack/client-development/index.pack.gz.old b/frontend/.next/cache/webpack/client-development/index.pack.gz.old index 0fc8f5e5dd7033f6408f5dcb26ed99902728bed0..62b02560aaa916fe0e364553a9e9bfc3f44431ef 100644 GIT binary patch literal 1261 zcmVV>Jp zhfnk&w1UDbmqRj2I>UP@HfUF0NHYa1snJP}Buzs1 za0j-S8M`!TCx@l)Ss}v=7_9{5*xb4FB&eYlMG7u^i#!zM3}BL=U?)|*g(OnUT+$Xm zu7DF`vo%oQI$|9jSu-ZIYe|K!!89yAUf3S0Afq+dl!ofcs#uWs8vqTJWNi9GLlw*^ zQPcE-Fb1lJR}-mPMgz-HJy4sAnsJ+pAZ2z9wMZIIa})}wtj|0;oaAU_hMKD~&0&+H zW>lCV+o@BrC|Xd+P#9a(9|l;PJ>0s zW;{wpO`f6wWM`wM%46Pi&OQgYueF7eK*76#-L1(P)&gcm%bAD*- z0PPutgf&VXAhEGfxw3E+2)n?k-|^k#n+R3J_!TBlDBd^j!@Oq%C?vnbw!oGTM95u> ziJVk2u_X7+-Jjn}@jU~v)cpm&q?>Uc4+_mP!_}y|%kaC_BxM$t4D-u+c}{^2aBL+z zPv*YUSU6*77q-fK7_2-xyB3evj{eTEtDB~BhIiJngvZAjW)a$x6aQp!gZd4*>Rw%8 zJ!$zegFaS{x#gnD5}_TTT~ptRaDWL~)B?W-N_(K#@AwaHPVYuh@!7+>cLtAceR~Px?Qp101jHZ}vbv>aoOIz}y~52iWp z&XBVI<4rqkw^x_5m$K(ofrgOhK^cFHqxKYSd-?kwcN2VZ>i$!Ecz<}VOm)RpR`#Ai=nbP8 zq(*z`=eImB{F=~Gm|+Tbkb>3_J^~9z#)R6$_$)JVp^(CMuJbI^IN`nRo%T-qi)R)= za1fhpZ~Iz%yS-fji^IxkRh3f`#*#RU3aR2*rEhlZo1A4PkI;DueY~6H#xnVxr@ktL zj4>!p`Yk^O#vsw;w|r${pfo`DZXt`oZ~34(cE{4e$QUNJkUK(Z%)oK(f#x7al6@UY zVM?f#NrHl%RP`2;NHKFsTL8HN zPK?dgK!NLsb$DdWn9!~z6}kq~u=IFgd#Hkp)?iZ_sw=BvLEdiwG*ptY=@SiAFsDRK z(+k2Fs2*NTq-q%rEJyV~Z7yoYZ7zb8*)`N6X*|tQD4?=F^XPDrqm>zIuF5orO^%vT zVTNp{PQ{{VK_Nq7Y-zWSE&G!pQfTL;UUXit28?e7A2eU zC>b?*iUyFKjhZTtdDA)j0^q*xx^L9vAorQnIk@QuCrue{0wZeXek}cOTYe6GyftCe zB;DleJoUMpH|4N6EME`FF|q}zr0(|=Q0jLQP=qbN9NJ$AmLtj2t|7_NXusn}hS_9+ z!oK(XKPAKjYXkD}u9$M^3Ip4|TCGDO=) z`-dCbPI|qFD}{?#^DaLM7H{ zMjg*A4htmSow-I15+(a72SQ2I9H3@94Jy@_e)T+Won>uo25MH(g<}Tq=yiy>k1lM!Utt zBBepWQc<-L+iIA&?0ocEv(pa$XZiWy%}>0K-@bY0$LaNd`rmkd@xtaGn}2TpwRyUg Vf993H%kRIt^*426Y3W7{005i7ZSepA diff --git a/package.json b/package.json index ed5ef8f..e2c5e37 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "dotenv": "^17.2.2", "geist": "^1.3.1", "next": "^14.2.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "sonner": "^2.0.1", "tailwind-merge": "^3.3.1" }, @@ -29,8 +29,8 @@ "@tailwindcss/postcss": "^4.0.14", "@tailwindcss/typography": "^0.5.16", "@types/node": "22.13.10", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", + "@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/pnpm-lock.yaml b/pnpm-lock.yaml index 241216a..310f665 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 3.8.0(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@headlessui/react': specifier: ^2.2.0 - version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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@18.3.1) @@ -34,53 +34,53 @@ importers: version: 17.2.2 geist: specifier: ^1.3.1 - version: 1.3.1(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) next: specifier: ^14.2.3 version: 14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1 react-dom: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1(react@18.3.1) sonner: specifier: ^2.0.1 - version: 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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: ^18.0.0 + specifier: ^18.3.0 version: 18.3.24 '@types/react-dom': - specifier: ^18.0.0 + 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 @@ -97,14 +97,14 @@ packages: peerDependencies: next: ^10.0.0 || ^11.0.0 || ^12.3.5 || ^13.5.9 || ^14.2.25 || ^15.2.3 - '@floating-ui/core@1.6.9': - resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.6.13': - resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - '@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' @@ -115,8 +115,8 @@ 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==} '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -124,8 +124,8 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@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 @@ -136,6 +136,26 @@ packages: peerDependencies: react: '>= 16 || ^19.0.0-rc' + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@next/env@14.2.32': resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==} @@ -196,40 +216,40 @@ packages: '@panva/hkdf@1.2.1': resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} - '@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 @@ -267,8 +287,8 @@ packages: '@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==} @@ -278,95 +298,107 @@ packages: 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==} @@ -404,8 +436,12 @@ packages: 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==} @@ -426,8 +462,8 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -435,28 +471,28 @@ packages: supports-color: optional: true - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} dotenv@17.2.2: resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} engines: {node: '>=12'} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + 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==} - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true joi@17.13.3: @@ -468,68 +504,68 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - lightningcss-darwin-arm64@1.29.2: - resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + 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: @@ -549,6 +585,22 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + 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 + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -600,15 +652,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': '*' @@ -628,6 +682,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': @@ -676,8 +734,8 @@ packages: 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 @@ -709,13 +767,17 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} - tailwindcss@4.0.14: - resolution: {integrity: sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==} + tailwindcss@4.1.13: + resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + 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==} @@ -737,6 +799,11 @@ packages: url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + 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==} @@ -761,6 +828,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -769,7 +840,7 @@ snapshots: dependencies: '@panva/hkdf': 1.2.1 cookie: 0.7.2 - debug: 4.4.1 + debug: 4.4.3 joi: 17.13.3 jose: 4.15.9 next: 14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -780,30 +851,30 @@ snapshots: transitivePeerDependencies: - supports-color - '@floating-ui/core@1.6.9': + '@floating-ui/core@1.7.3': dependencies: - '@floating-ui/utils': 0.2.9 + '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.6.13': + '@floating-ui/dom@1.7.4': dependencies: - '@floating-ui/core': 1.6.9 - '@floating-ui/utils': 0.2.9 + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.6.13 + '@floating-ui/dom': 1.7.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@floating-ui/utils': 0.2.9 + '@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': {} '@hapi/hoek@9.3.0': {} @@ -811,19 +882,43 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@headlessui/react@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@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@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/focus': 3.20.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/interactions': 3.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/react-virtual': 3.13.4(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: 18.3.1 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@next/env@14.2.32': {} '@next/swc-darwin-arm64@14.2.32': @@ -855,52 +950,52 @@ snapshots: '@panva/hkdf@1.2.1': {} - '@react-aria/focus@3.20.1(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)': dependencies: - '@react-aria/interactions': 3.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/utils': 3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-types/shared': 3.28.0(react@18.3.1) - '@swc/helpers': 0.5.15 + '@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) - '@react-aria/interactions@3.24.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)': dependencies: - '@react-aria/ssr': 3.9.7(react@18.3.1) - '@react-aria/utils': 3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-stately/flags': 3.1.0 - '@react-types/shared': 3.28.0(react@18.3.1) - '@swc/helpers': 0.5.15 + '@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.7(react@18.3.1)': + '@react-aria/ssr@3.9.10(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-aria/utils@3.28.1(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)': dependencies: - '@react-aria/ssr': 3.9.7(react@18.3.1) - '@react-stately/flags': 3.1.0 - '@react-stately/utils': 3.10.5(react@18.3.1) - '@react-types/shared': 3.28.0(react@18.3.1) - '@swc/helpers': 0.5.15 + '@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) - '@react-stately/flags@3.1.0': + '@react-stately/flags@3.1.2': dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.17 - '@react-stately/utils@3.10.5(react@18.3.1)': + '@react-stately/utils@3.10.8(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-types/shared@3.28.0(react@18.3.1)': + '@react-types/shared@3.32.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -956,7 +1051,7 @@ snapshots: '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -965,87 +1060,97 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 - '@tailwindcss/container-queries@0.1.1(tailwindcss@4.0.14)': + '@tailwindcss/container-queries@0.1.1(tailwindcss@4.1.13)': dependencies: - tailwindcss: 4.0.14 + tailwindcss: 4.1.13 - '@tailwindcss/node@4.0.14': + '@tailwindcss/node@4.1.13': dependencies: - enhanced-resolve: 5.18.1 - jiti: 2.4.2 - tailwindcss: 4.0.14 + '@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-android-arm64@4.0.14': + '@tailwindcss/oxide-darwin-arm64@4.1.13': optional: true - '@tailwindcss/oxide-darwin-arm64@4.0.14': + '@tailwindcss/oxide-darwin-x64@4.1.13': optional: true - '@tailwindcss/oxide-darwin-x64@4.0.14': + '@tailwindcss/oxide-freebsd-x64@4.1.13': optional: true - '@tailwindcss/oxide-freebsd-x64@4.0.14': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.14': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.0.14': + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.0.14': + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.0.14': + '@tailwindcss/oxide-linux-x64-musl@4.1.13': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.0.14': + '@tailwindcss/oxide-wasm32-wasi@4.1.13': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.0.14': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.0.14': + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': optional: true - '@tailwindcss/oxide@4.0.14': + '@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@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)': dependencies: - '@tanstack/virtual-core': 3.13.4 + '@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: @@ -1087,7 +1192,9 @@ snapshots: dependencies: streamsearch: 1.1.0 - caniuse-lite@1.0.30001706: {} + caniuse-lite@1.0.30001741: {} + + chownr@3.0.0: {} client-only@0.0.1: {} @@ -1099,26 +1206,26 @@ snapshots: csstype@3.1.3: {} - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.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@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.5.1(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: next: 14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graceful-fs@4.2.11: {} - jiti@2.4.2: {} + jiti@2.5.1: {} joi@17.13.3: dependencies: @@ -1132,50 +1239,50 @@ snapshots: 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: {} @@ -1191,6 +1298,18 @@ snapshots: dependencies: yallist: 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: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -1200,7 +1319,7 @@ snapshots: '@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: 18.3.1 @@ -1246,13 +1365,13 @@ 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 @@ -1272,7 +1391,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - sonner@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -1290,9 +1409,18 @@ snapshots: tailwind-merge@3.3.1: {} - tailwindcss@4.0.14: {} + 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: {} @@ -1306,6 +1434,10 @@ snapshots: url-join@4.0.1: {} + 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: {} @@ -1318,3 +1450,5 @@ snapshots: ws@8.18.3: {} yallist@4.0.0: {} + + yallist@5.0.0: {} From 285f929829a8c85bd7256f96bd180fc0a270b411 Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 16:30:17 -0400 Subject: [PATCH 07/10] merchant --- .env.example | 7 - app/api/minlp/explain/route.ts | 5 - app/api/minlp/solve/route.ts | 7 +- app/api/products/[handle]/route.ts | 24 ++++ app/layout.tsx | 25 ++-- components/camera-scanner.tsx | 132 ++++++++++++------ components/layout/navbar/scanner-button.tsx | 25 ++-- components/layout/navbar/tarazoo-navbar.tsx | 37 +---- lib/auth0.ts | 3 - middleware.ts | 15 +- package.json | 1 - pages/api/auth/[...auth0].ts | 19 --- pnpm-lock.yaml | 143 -------------------- 13 files changed, 156 insertions(+), 287 deletions(-) create mode 100644 app/api/products/[handle]/route.ts delete mode 100644 lib/auth0.ts delete mode 100644 pages/api/auth/[...auth0].ts diff --git a/.env.example b/.env.example index 594cd84..7a4f5e9 100644 --- a/.env.example +++ b/.env.example @@ -11,13 +11,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key" SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" NEXT_PUBLIC_MERCHANT_ID_DEFAULT="a1b2c3d4-e5f6-7890-abcd-ef1234567890" -# Auth0 Configuration (for Merchant Dashboard) -AUTH0_SECRET="your-auth0-secret" -AUTH0_BASE_URL="https://your-app-domain.vercel.app" -AUTH0_ISSUER_BASE_URL="https://your-tenant.auth0.com" -AUTH0_CLIENT_ID="your-auth0-client-id" -AUTH0_CLIENT_SECRET="your-auth0-client-secret" - # MINLP Service Configuration MINLP_BASE_URL="https://your-minlp-service.vercel.app" diff --git a/app/api/minlp/explain/route.ts b/app/api/minlp/explain/route.ts index 65f637c..12de7d8 100644 --- a/app/api/minlp/explain/route.ts +++ b/app/api/minlp/explain/route.ts @@ -1,13 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getSession } from '@auth0/nextjs-auth0'; const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000'; export async function POST(req: NextRequest) { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } try { const body = await req.json(); diff --git a/app/api/minlp/solve/route.ts b/app/api/minlp/solve/route.ts index f7e8934..a749b6c 100644 --- a/app/api/minlp/solve/route.ts +++ b/app/api/minlp/solve/route.ts @@ -1,14 +1,9 @@ -import { NextRequest, NextResponse } from 'next/server'; import { createMinlpRun } from 'lib/supabase'; -import { getSession } from '@auth0/nextjs-auth0'; +import { NextRequest, NextResponse } from 'next/server'; const MINLP_BASE_URL = process.env.MINLP_BASE_URL || 'http://localhost:8000'; export async function POST(request: NextRequest) { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } try { const body = await request.json(); 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/layout.tsx b/app/layout.tsx index 72c7f0c..ec06a99 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,13 +1,12 @@ -import { Providers } from 'components/providers'; -import { UserProvider } from '@auth0/nextjs-auth0/client'; 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'; -import { getCart } from 'lib/shopify'; const { SITE_NAME } = process.env; @@ -33,16 +32,14 @@ export default async function RootLayout({ return ( - - - -
- {children} - - -
-
-
+ + +
+ {children} + + +
+
); diff --git a/components/camera-scanner.tsx b/components/camera-scanner.tsx index fff8a3e..8d4a084 100644 --- a/components/camera-scanner.tsx +++ b/components/camera-scanner.tsx @@ -1,14 +1,14 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; import { BrowserMultiFormatReader } from '@zxing/browser'; +import type { Exception, Result } from '@zxing/library'; import { BarcodeFormat, DecodeHintType } from '@zxing/library'; -import type { Result, Exception } from '@zxing/library'; +import type { Product as ShopifyProduct } from 'lib/shopify/types'; import { getProductByBarcode } from 'lib/supabase'; -import type { Product } from '../packages/shared/types'; +import { useEffect, useRef, useState } from 'react'; interface CameraScannerProps { - onProductScanned: (product: Product) => void; + onProductScanned: (product: ShopifyProduct) => void; onClose: () => void; autoCloseOnScan?: boolean; } @@ -54,10 +54,13 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc 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 && (track as any).getCapabilities) { + if (track && 'getCapabilities' in track) { const capabilities = (track as any).getCapabilities(); if (capabilities && 'torch' in capabilities) { setTorchEnabled(true); @@ -65,18 +68,42 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc } // Start continuous scanning - codeReader.decodeFromVideoDevice(undefined, videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { - if (result) { - await handleBarcodeDetected(result.getText()); + try { + // Try the newer API first + if (typeof codeReader.decodeFromVideoElement === 'function') { + await codeReader.decodeFromVideoElement(videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { + if (result) { + await handleBarcodeDetected(result.getText()); + } + if (err && (err as any).name !== 'NotFoundException') { + console.error('Scanning error:', err); + } + }); + } else { + // Fallback to older API + await codeReader.decodeFromVideoDevice(undefined, videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { + if (result) { + await handleBarcodeDetected(result.getText()); + } + if (err && (err as any).name !== 'NotFoundException') { + console.error('Scanning error:', err); + } + }); } - if (err && (err as any).name !== 'NotFoundException') { - console.error('Scanning error:', err); - } - }); + } catch (decodeErr) { + console.error('Decode setup error:', decodeErr); + setError('Failed to start barcode scanning. Please try again.'); + setIsScanning(false); + } } } catch (err) { console.error('Camera error:', err); - setError('Unable to access camera. Please check permissions.'); + 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); } }; @@ -117,38 +144,59 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc (codeReaderRef.current as any).reset?.(); } - const product = await getProductByBarcode(barcode); + const supabaseProduct = await getProductByBarcode(barcode); - if (product) { - // Haptic feedback - if ('vibrate' in navigator) { - navigator.vibrate(200); - } - // Stop camera and bubble up - stopScanning(); - onProductScanned(product); - if (autoCloseOnScan) { - onClose(); - } - } else { - setError(`Product not found for barcode: ${barcode}`); - // Optionally resume decoding after error display - setTimeout(() => { - setError(null); - if (videoRef.current) { - const hints = new Map(); - const formats = [BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX]; - hints.set(DecodeHintType.POSSIBLE_FORMATS, formats); - const reader = new BrowserMultiFormatReader(hints); - codeReaderRef.current = reader; - reader.decodeFromVideoDevice(undefined, videoRef.current!, async (result: Result | undefined, err: Exception | undefined) => { - if (result) { - await handleBarcodeDetected(result.getText()); - } - }); + if (supabaseProduct && supabaseProduct.shopify_handle) { + // Fetch the full Shopify product via API + const response = await fetch(`/api/products/${supabaseProduct.shopify_handle}`); + if (response.ok) { + const shopifyProduct = await response.json(); + + if (shopifyProduct) { + // Haptic feedback + if ('vibrate' in navigator) { + navigator.vibrate(200); + } + // Stop camera and bubble up + stopScanning(); + onProductScanned(shopifyProduct); + if (autoCloseOnScan) { + onClose(); + } + return; } - }, 2000); + } } + + setError(`Product not found for barcode: ${barcode}. The product may not be in our catalog yet.`); + // Optionally resume decoding after error display + setTimeout(() => { + setError(null); + if (videoRef.current) { + const hints = new Map(); + const formats = [BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX]; + hints.set(DecodeHintType.POSSIBLE_FORMATS, formats); + const reader = new BrowserMultiFormatReader(hints); + codeReaderRef.current = reader; + try { + if (typeof reader.decodeFromVideoElement === 'function') { + reader.decodeFromVideoElement(videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { + if (result) { + await handleBarcodeDetected(result.getText()); + } + }); + } else { + reader.decodeFromVideoDevice(undefined, videoRef.current!, async (result: Result | undefined, err: Exception | undefined) => { + if (result) { + await handleBarcodeDetected(result.getText()); + } + }); + } + } catch (resumeErr) { + console.error('Failed to resume scanning:', resumeErr); + } + } + }, 3000); }; const handleManualSubmit = async (e: React.FormEvent) => { diff --git a/components/layout/navbar/scanner-button.tsx b/components/layout/navbar/scanner-button.tsx index d5aa8f5..b503b47 100644 --- a/components/layout/navbar/scanner-button.tsx +++ b/components/layout/navbar/scanner-button.tsx @@ -1,13 +1,13 @@ 'use client'; -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; import CameraScanner from 'components/camera-scanner'; -import { useSupabaseCart } from 'components/cart/supabase-cart-context'; +import { useCart } from 'components/cart/cart-context'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; export default function ScannerButton() { const [isScanning, setIsScanning] = useState(false); - const { addItem } = useSupabaseCart(); + const { addCartItem } = useCart(); const router = useRouter(); return ( @@ -34,21 +34,24 @@ export default function ScannerButton() { {isScanning && ( { - addItem(product); + onProductScanned={(shopifyProduct) => { + // Use the first variant as default + const defaultVariant = shopifyProduct.variants[0]; + if (defaultVariant) { + addCartItem(defaultVariant, shopifyProduct); + } + // Show a toast or notification 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`; + message.textContent = `Added ${shopifyProduct.title} to cart`; document.body.appendChild(message); setTimeout(() => { message.remove(); }, 3000); - // Navigate to the product listing if we have a handle - if (product.shopify_handle) { - router.push(`/product/${product.shopify_handle}`); - } + // Navigate to the product listing + router.push(`/product/${shopifyProduct.handle}`); }} onClose={() => setIsScanning(false)} /> diff --git a/components/layout/navbar/tarazoo-navbar.tsx b/components/layout/navbar/tarazoo-navbar.tsx index bbf86f0..2a6ce0f 100644 --- a/components/layout/navbar/tarazoo-navbar.tsx +++ b/components/layout/navbar/tarazoo-navbar.tsx @@ -1,13 +1,10 @@ 'use client'; +import CartButton from 'components/layout/navbar/cart-button'; import Link from 'next/link'; -import { Suspense } from 'react'; import ScannerButton from './scanner-button'; -import CartButton from 'components/layout/navbar/cart-button'; -import { useUser } from '@auth0/nextjs-auth0/client'; export function TarazooNavbar() { - const { user, isLoading } = useUser(); const defaultMerchantId = process.env.NEXT_PUBLIC_MERCHANT_ID_DEFAULT || 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; return ( ); diff --git a/lib/auth0.ts b/lib/auth0.ts deleted file mode 100644 index 85bfcbd..0000000 --- a/lib/auth0.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { initAuth0 } from '@auth0/nextjs-auth0'; - -export const auth0 = initAuth0(); diff --git a/middleware.ts b/middleware.ts index 7686e4a..012ec01 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,22 +1,25 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; -// Simple Auth0 session presence check in middleware. -// If no session cookie for protected routes, redirect to Auth0 login. +// 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') || - pathname === '/dashboard' + pathname.startsWith('/api/minlp') ); if (!isProtected) { return NextResponse.next(); } - // Default cookie set by @auth0/nextjs-auth0 + // Default cookie set by auth provider const hasSession = Boolean(req.cookies.get('appSession')); if (!hasSession) { const loginUrl = new URL('/api/auth/login', req.url); @@ -31,5 +34,5 @@ export function middleware(req: NextRequest) { } export const config = { - matcher: ['/merchant/:path*', '/api/minlp/:path*', '/dashboard'] + matcher: ['/merchant/:path*', '/api/minlp/:path*'] }; diff --git a/package.json b/package.json index e2c5e37..300f7e4 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "test": "pnpm prettier:check" }, "dependencies": { - "@auth0/nextjs-auth0": "^3.5.0", "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@supabase/supabase-js": "^2.57.4", diff --git a/pages/api/auth/[...auth0].ts b/pages/api/auth/[...auth0].ts deleted file mode 100644 index 8d4a2fb..0000000 --- a/pages/api/auth/[...auth0].ts +++ /dev/null @@ -1,19 +0,0 @@ -import { handleAuth, handleLogin } from '@auth0/nextjs-auth0'; - -// Customize Auth0 to always use Google and land on /dashboard -export default handleAuth({ - async login(req, res) { - try { - await handleLogin(req, res, { - returnTo: '/dashboard', - authorizationParams: { - // Force Google as the IdP - connection: 'google-oauth2' - } - }); - } catch (error) { - // Let the SDK handle error responses - throw error; - } - } -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 310f665..4f6e726 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@auth0/nextjs-auth0': - specifier: ^3.5.0 - version: 3.8.0(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@headlessui/react': specifier: ^2.2.0 version: 2.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -91,12 +88,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@auth0/nextjs-auth0@3.8.0': - resolution: {integrity: sha512-xMzpkCuJAZ7tquJOZr7W4Jm9155EDotAACtdd9R9t4ATZgNGUzDQfPMogYJ2O14WB86sZSrp3rpJo4cspQYcSA==} - engines: {node: '>=16'} - peerDependencies: - next: ^10.0.0 || ^11.0.0 || ^12.3.5 || ^13.5.9 || ^14.2.25 || ^15.2.3 - '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -118,12 +109,6 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@hapi/hoek@9.3.0': - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - - '@hapi/topo@5.1.0': - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@headlessui/react@2.2.8': resolution: {integrity: sha512-vkiZulDC0lFeTrZTbA4tHvhZHvkUb2PFh5xJ1BvWAZdRK0fayMKO1QEO4inWkXxK1i0I1rcwwu1d6mo0K7Pcbw==} engines: {node: '>=10'} @@ -213,9 +198,6 @@ packages: cpu: [x64] os: [win32] - '@panva/hkdf@1.2.1': - resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} - '@react-aria/focus@3.21.1': resolution: {integrity: sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==} peerDependencies: @@ -253,15 +235,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@sideway/address@4.1.5': - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - - '@sideway/formula@3.0.1': - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - - '@sideway/pinpoint@2.0.0': - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - '@supabase/auth-js@2.71.1': resolution: {integrity: sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==} @@ -450,10 +423,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -462,15 +431,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -495,12 +455,6 @@ packages: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true - joi@17.13.3: - resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -581,10 +535,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -601,9 +551,6 @@ packages: engines: {node: '>=10'} hasBin: true - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -627,20 +574,6 @@ packages: sass: optional: true - oauth4webapi@2.17.0: - resolution: {integrity: sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==} - - object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - - oidc-token-hash@5.1.1: - resolution: {integrity: sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==} - engines: {node: ^10.13.0 || >=12.0.0} - - openid-client@5.7.1: - resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -796,9 +729,6 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - url-join@4.0.1: - resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: @@ -825,9 +755,6 @@ packages: utf-8-validate: optional: true - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -836,21 +763,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@auth0/nextjs-auth0@3.8.0(next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': - dependencies: - '@panva/hkdf': 1.2.1 - cookie: 0.7.2 - debug: 4.4.3 - joi: 17.13.3 - jose: 4.15.9 - next: 14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - oauth4webapi: 2.17.0 - openid-client: 5.7.1 - tslib: 2.8.1 - url-join: 4.0.1 - transitivePeerDependencies: - - supports-color - '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -876,12 +788,6 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@hapi/hoek@9.3.0': {} - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.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@18.3.1(react@18.3.1))(react@18.3.1) @@ -948,8 +854,6 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.32': optional: true - '@panva/hkdf@1.2.1': {} - '@react-aria/focus@3.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@react-aria/interactions': 3.25.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -999,14 +903,6 @@ snapshots: dependencies: react: 18.3.1 - '@sideway/address@4.1.5': - dependencies: - '@hapi/hoek': 9.3.0 - - '@sideway/formula@3.0.1': {} - - '@sideway/pinpoint@2.0.0': {} - '@supabase/auth-js@2.71.1': dependencies: '@supabase/node-fetch': 2.6.15 @@ -1200,16 +1096,10 @@ snapshots: clsx@2.1.1: {} - cookie@0.7.2: {} - cssesc@3.0.0: {} csstype@3.1.3: {} - debug@4.4.3: - dependencies: - ms: 2.1.3 - detect-libc@2.0.4: {} dotenv@17.2.2: {} @@ -1227,16 +1117,6 @@ snapshots: jiti@2.5.1: {} - joi@17.13.3: - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - - jose@4.15.9: {} - js-tokens@4.0.0: {} lightningcss-darwin-arm64@1.30.1: @@ -1294,10 +1174,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1310,8 +1186,6 @@ snapshots: mkdirp@3.0.1: {} - ms@2.1.3: {} - nanoid@3.3.11: {} next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -1339,19 +1213,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - oauth4webapi@2.17.0: {} - - object-hash@2.2.0: {} - - oidc-token-hash@5.1.1: {} - - openid-client@5.7.1: - dependencies: - jose: 4.15.9 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.1.1 - picocolors@1.1.1: {} postcss-selector-parser@6.0.10: @@ -1432,8 +1293,6 @@ snapshots: undici-types@6.20.0: {} - url-join@4.0.1: {} - use-sync-external-store@1.5.0(react@18.3.1): dependencies: react: 18.3.1 @@ -1449,6 +1308,4 @@ snapshots: ws@8.18.3: {} - yallist@4.0.0: {} - yallist@5.0.0: {} From e22e280c046390515b01c56930fa921efb2ca8d1 Mon Sep 17 00:00:00 2001 From: Mehar Khanna Date: Sat, 13 Sep 2025 17:06:34 -0400 Subject: [PATCH 08/10] camera cohere --- .env.example | 3 + app/api/vision-detect/route.ts | 88 ++++++++ app/checkout/page.tsx | 13 +- components/camera-scanner.tsx | 232 +++++++++++--------- components/layout/navbar/scanner-button.tsx | 20 +- lib/supabase.ts | 25 ++- scripts/test-checkout.js | 85 +++++++ supabase/new-update.sql | 9 + 8 files changed, 349 insertions(+), 126 deletions(-) create mode 100644 app/api/vision-detect/route.ts create mode 100644 scripts/test-checkout.js create mode 100644 supabase/new-update.sql diff --git a/.env.example b/.env.example index 7a4f5e9..4dfd12b 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,9 @@ SHOPIFY_STORE_DOMAIN="" 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 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/checkout/page.tsx b/app/checkout/page.tsx index d670672..57a10e7 100644 --- a/app/checkout/page.tsx +++ b/app/checkout/page.tsx @@ -11,6 +11,7 @@ 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); @@ -27,13 +28,15 @@ export default function CheckoutPage() { price_cents: item.product.price_cents })); - // Create order in Supabase + // Create order in Supabase with full discount to zero out invoice const order = await createOrder( MERCHANT_ID, orderItems, subtotal, tax, - total + 0, // total is 0 after full discount + DISCOUNT_LABEL, + total // discount equals the cart total (subtotal + tax) ); if (order) { @@ -221,9 +224,13 @@ export default function CheckoutPage() {
{formatPrice(tax / 100)}
+
+
{DISCOUNT_LABEL}
+
- {formatPrice(total / 100)}
+
Order total
-
{formatPrice(total / 100)}
+
{formatPrice(0)}
diff --git a/components/camera-scanner.tsx b/components/camera-scanner.tsx index 8d4a084..9c0da3e 100644 --- a/components/camera-scanner.tsx +++ b/components/camera-scanner.tsx @@ -1,26 +1,24 @@ 'use client'; -import { BrowserMultiFormatReader } from '@zxing/browser'; -import type { Exception, Result } from '@zxing/library'; -import { BarcodeFormat, DecodeHintType } from '@zxing/library'; -import type { Product as ShopifyProduct } from 'lib/shopify/types'; -import { getProductByBarcode } from 'lib/supabase'; import { useEffect, useRef, useState } from 'react'; +import type { Product } from '../packages/shared/types'; +import { getProductByName } from 'lib/supabase'; interface CameraScannerProps { - onProductScanned: (product: ShopifyProduct) => void; + onDetected: (product: Product) => void; onClose: () => void; autoCloseOnScan?: boolean; } -export default function CameraScanner({ onProductScanned, onClose, autoCloseOnScan = true }: CameraScannerProps) { +export default function CameraScanner({ onDetected, onClose, autoCloseOnScan = true }: CameraScannerProps) { const videoRef = useRef(null); const [isScanning, setIsScanning] = useState(false); const [error, setError] = useState(null); - const [manualBarcode, setManualBarcode] = useState(''); + const [manualName, setManualName] = useState(''); const [torchEnabled, setTorchEnabled] = useState(false); - const codeReaderRef = useRef(null); const streamRef = useRef(null); + const timerRef = useRef(null); + const [autoMode, setAutoMode] = useState(false); useEffect(() => { startScanning(); @@ -35,12 +33,6 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc setIsScanning(true); setError(null); - const hints = new Map(); - const formats = [BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX]; - hints.set(DecodeHintType.POSSIBLE_FORMATS, formats); - const codeReader = new BrowserMultiFormatReader(hints); - codeReaderRef.current = codeReader; - const constraints: MediaStreamConstraints = { video: { facingMode: 'environment', @@ -67,34 +59,8 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc } } - // Start continuous scanning - try { - // Try the newer API first - if (typeof codeReader.decodeFromVideoElement === 'function') { - await codeReader.decodeFromVideoElement(videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { - if (result) { - await handleBarcodeDetected(result.getText()); - } - if (err && (err as any).name !== 'NotFoundException') { - console.error('Scanning error:', err); - } - }); - } else { - // Fallback to older API - await codeReader.decodeFromVideoDevice(undefined, videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { - if (result) { - await handleBarcodeDetected(result.getText()); - } - if (err && (err as any).name !== 'NotFoundException') { - console.error('Scanning error:', err); - } - }); - } - } catch (decodeErr) { - console.error('Decode setup error:', decodeErr); - setError('Failed to start barcode scanning. Please try again.'); - setIsScanning(false); - } + // Start periodic vision detection only if auto mode is enabled + if (autoMode) startPeriodicDetection(); } } catch (err) { console.error('Camera error:', err); @@ -109,10 +75,9 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc }; const stopScanning = () => { - if (codeReaderRef.current) { - (codeReaderRef.current as any).stopContinuousDecode?.(); - (codeReaderRef.current as any).reset?.(); - codeReaderRef.current = null; + if (timerRef.current) { + clearInterval(timerRef.current as any); + timerRef.current = null; } if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); @@ -137,74 +102,98 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc } }; - const handleBarcodeDetected = async (barcode: string) => { - // Stop decoding to prevent multiple reads - if (codeReaderRef.current) { - (codeReaderRef.current as any).stopContinuousDecode?.(); - (codeReaderRef.current as any).reset?.(); - } - - const supabaseProduct = await getProductByBarcode(barcode); - - if (supabaseProduct && supabaseProduct.shopify_handle) { - // Fetch the full Shopify product via API - const response = await fetch(`/api/products/${supabaseProduct.shopify_handle}`); - if (response.ok) { - const shopifyProduct = await response.json(); - - if (shopifyProduct) { - // Haptic feedback + 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); } - // Stop camera and bubble up stopScanning(); - onProductScanned(shopifyProduct); - if (autoCloseOnScan) { - onClose(); - } - return; + 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); + }; - setError(`Product not found for barcode: ${barcode}. The product may not be in our catalog yet.`); - // Optionally resume decoding after error display - setTimeout(() => { - setError(null); - if (videoRef.current) { - const hints = new Map(); - const formats = [BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX]; - hints.set(DecodeHintType.POSSIBLE_FORMATS, formats); - const reader = new BrowserMultiFormatReader(hints); - codeReaderRef.current = reader; - try { - if (typeof reader.decodeFromVideoElement === 'function') { - reader.decodeFromVideoElement(videoRef.current, async (result: Result | undefined, err: Exception | undefined) => { - if (result) { - await handleBarcodeDetected(result.getText()); - } - }); - } else { - reader.decodeFromVideoDevice(undefined, videoRef.current!, async (result: Result | undefined, err: Exception | undefined) => { - if (result) { - await handleBarcodeDetected(result.getText()); - } - }); - } - } catch (resumeErr) { - console.error('Failed to resume scanning:', resumeErr); - } + const captureAndDetect = async () => { + try { + const label = await detectCurrentFrame(); + if (!label) { + setError('Could not identify item. Try again.'); + setTimeout(() => setError(null), 1500); + return; } - }, 3000); + 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 (manualBarcode) { - await handleBarcodeDetected(manualBarcode); - setManualBarcode(''); + 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 ( @@ -247,7 +236,7 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc
- {isScanning && ( + {isScanning && autoMode && (
@@ -255,6 +244,35 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc + {/* Controls: torch (left), auto toggle (next), shutter (bottom center in manual mode) */} + + + {!autoMode && ( +
+
+ )} + {error && (
{error} @@ -262,14 +280,14 @@ export default function CameraScanner({ onProductScanned, onClose, autoCloseOnSc )}
- {/* Manual barcode input fallback */} + {/* Manual name input fallback */}
setManualBarcode(e.target.value)} - placeholder="Enter barcode manually" + value={manualName} + onChange={(e) => 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" />