From 0e84d2101b451af9b59d560474762c3e737d9f8e Mon Sep 17 00:00:00 2001 From: pranavjana Date: Mon, 26 Jan 2026 03:17:58 +0800 Subject: [PATCH] Add lego-hunter - Mino use case - ## Demo - Live demo: https://lego-hunter.vercel.app/ - Contributor: Pranav Janakiraman (@pranavjana) --- lego-hunter/.gitignore | 41 + .../75339b8c-4e68-490d-89cf-96c62334598a.jpg | Bin 0 -> 69979 bytes lego-hunter/README.md | 135 + lego-hunter/app/api/generate-urls/route.ts | 22 + lego-hunter/app/api/search-lego/route.ts | 282 + lego-hunter/app/favicon.ico | Bin 0 -> 25931 bytes lego-hunter/app/globals.css | 912 +++ lego-hunter/app/layout.tsx | 35 + lego-hunter/app/page.tsx | 688 ++ lego-hunter/components.json | 22 + lego-hunter/components/best-deal-card.tsx | 124 + lego-hunter/components/browser-preview.tsx | 109 + lego-hunter/components/lego-confetti.tsx | 110 + lego-hunter/components/results-table.tsx | 188 + lego-hunter/components/retailer-card.tsx | 182 + lego-hunter/components/ui/button.tsx | 62 + lego-hunter/components/ui/card.tsx | 92 + lego-hunter/components/ui/input.tsx | 21 + lego-hunter/eslint.config.mjs | 18 + lego-hunter/lib/gemini-client.ts | 137 + lego-hunter/lib/mino-client.ts | 185 + lego-hunter/lib/retailers.ts | 117 + lego-hunter/lib/utils.ts | 49 + lego-hunter/next.config.ts | 7 + lego-hunter/package-lock.json | 6766 +++++++++++++++++ lego-hunter/package.json | 37 + lego-hunter/postcss.config.mjs | 7 + lego-hunter/public/file.svg | 1 + lego-hunter/public/globe.svg | 1 + lego-hunter/public/next.svg | 1 + lego-hunter/public/vercel.svg | 1 + lego-hunter/public/window.svg | 1 + lego-hunter/tsconfig.json | 34 + lego-hunter/types/index.ts | 96 + 34 files changed, 10483 insertions(+) create mode 100644 lego-hunter/.gitignore create mode 100644 lego-hunter/75339b8c-4e68-490d-89cf-96c62334598a.jpg create mode 100644 lego-hunter/README.md create mode 100644 lego-hunter/app/api/generate-urls/route.ts create mode 100644 lego-hunter/app/api/search-lego/route.ts create mode 100644 lego-hunter/app/favicon.ico create mode 100644 lego-hunter/app/globals.css create mode 100644 lego-hunter/app/layout.tsx create mode 100644 lego-hunter/app/page.tsx create mode 100644 lego-hunter/components.json create mode 100644 lego-hunter/components/best-deal-card.tsx create mode 100644 lego-hunter/components/browser-preview.tsx create mode 100644 lego-hunter/components/lego-confetti.tsx create mode 100644 lego-hunter/components/results-table.tsx create mode 100644 lego-hunter/components/retailer-card.tsx create mode 100644 lego-hunter/components/ui/button.tsx create mode 100644 lego-hunter/components/ui/card.tsx create mode 100644 lego-hunter/components/ui/input.tsx create mode 100644 lego-hunter/eslint.config.mjs create mode 100644 lego-hunter/lib/gemini-client.ts create mode 100644 lego-hunter/lib/mino-client.ts create mode 100644 lego-hunter/lib/retailers.ts create mode 100644 lego-hunter/lib/utils.ts create mode 100644 lego-hunter/next.config.ts create mode 100644 lego-hunter/package-lock.json create mode 100644 lego-hunter/package.json create mode 100644 lego-hunter/postcss.config.mjs create mode 100644 lego-hunter/public/file.svg create mode 100644 lego-hunter/public/globe.svg create mode 100644 lego-hunter/public/next.svg create mode 100644 lego-hunter/public/vercel.svg create mode 100644 lego-hunter/public/window.svg create mode 100644 lego-hunter/tsconfig.json create mode 100644 lego-hunter/types/index.ts diff --git a/lego-hunter/.gitignore b/lego-hunter/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/lego-hunter/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/lego-hunter/75339b8c-4e68-490d-89cf-96c62334598a.jpg b/lego-hunter/75339b8c-4e68-490d-89cf-96c62334598a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5fb579aa018545ac11aca1d9311ff3c19d271aea GIT binary patch literal 69979 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

A4g?CTHz8a*SZhVlS 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 X59kI8hTG9#_{uvKb8!aAV*lN5(Rf=? literal 0 HcmV?d00001 diff --git a/lego-hunter/README.md b/lego-hunter/README.md new file mode 100644 index 0000000..1b0d7ee --- /dev/null +++ b/lego-hunter/README.md @@ -0,0 +1,135 @@ +# Lego Restock Hunter - Global Inventory Finder + +## Demo + +![lego-hunter Demo](./75339b8c-4e68-490d-89cf-96c62334598a.jpg) + +**Live Demo:** https://lego-hunter.vercel.app/ + +The Lego Restock Hunter is a powerful inventory search tool designed to find rare or sold-out Lego sets across 15+ global retailers simultaneously. It uses AI to discover the best retailers for a specific set, deploys parallel Mino browser agents to check stock and pricing, and finishes with a Gemini-powered analysis to recommend the single best deal (balancing price and shipping). + +--- + +--- + +## Demo + +*[Demo video/screenshot to be added]* + +--- + +## How Mino API is Used + +The Mino API powers browser automation for this use case. See the code snippet below for implementation details. + +### Code Snippet + +```bash +curl -N -X POST "https://mino.ai/v1/automation/run-sse" \ + -H "X-API-Key: $MINO_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://www.lego.com/en-us/search?q=75192", + "goal": "Search for Millennium Falcon Lego set. Extract inStock, price, and shipping. Return JSON.", + "browser_profile": "lite" + }' +``` + +--- + +## How to Run + +### Prerequisites + +- Node.js 18+ +- Mino API key (get from [mino.ai](https://mino.ai)) + +### Setup + +1. Clone the repository: +```bash +git clone +cd lego-hunter +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Create `.env.local` file: +```bash +# Add your environment variables here +MINO_API_KEY=sk-mino-... +``` + +4. Run the development server: +```bash +npm run dev +``` + +5. Open [http://localhost:3000](http://localhost:3000) in your browser + +--- + +## Architecture Diagram + +```mermaid +graph TD + subgraph Frontend [Next.js Client] + UI[User Interface - Lego Brick Style] + State[Retailer Status & Best Deal] + end + + subgraph Backend [Next.js API Routes] + UrlGen[/api/generate-urls] + Search[/api/search-lego] + end + + subgraph External_APIs [External Services] + Gemini[Gemini 2.0 - URL Gen & Analysis] + Mino[Mino API - Browser Automation] + end + + %% User Interactions + UI -->|Lego Set Name| UrlGen + UrlGen -->|AI Discovery| Gemini + + %% Scrape Phase + UrlGen -->|Return 15 URLs| UI + UI -->|Trigger Parallel Scrape| Search + + Search -->|Deploy 15 Agents| Mino + Mino --.->|SSE Streams| UI + Mino --.->|Product JSON| Search + + %% Final Analysis + Search -->|Analyze All Deals| Gemini + Gemini -->|Best Retailer Recommendation| Search + Search --.->|Final Best Deal Event| UI +``` + +```mermaid +sequenceDiagram + participant U as User + participant S as API (/api/search-lego) + participant G as Gemini (AI) + participant M as Mino (15 Parallel Agents) + + U->>G: Discover Retailers for "Millennium Falcon" + G-->>U: List of 15 Shop URLs + + U->>S: POST Search (Set Name + 15 URLs) + + par Retailer 1 to 15 (Amazon, Walmart, Lego.com, etc.) + S->>M: Scrape Retailer (Goal: Find Stock/Price) + M-->>U: SSE: Progress Step + M-->>S: JSON Result (inStock, price, shipping) + end + + S->>G: Analyze All Results + G-->>S: Best Deal Recommendation + S->>U: Final Trophy Notification (Confetti Trigger) +``` + + diff --git a/lego-hunter/app/api/generate-urls/route.ts b/lego-hunter/app/api/generate-urls/route.ts new file mode 100644 index 0000000..2111660 --- /dev/null +++ b/lego-hunter/app/api/generate-urls/route.ts @@ -0,0 +1,22 @@ +import { generateRetailerUrls } from '@/lib/gemini-client' +import type { GenerateUrlsRequest } from '@/types' + +export async function POST(request: Request) { + try { + const body: GenerateUrlsRequest = await request.json() + + if (!body.legoSetName) { + return Response.json({ error: 'legoSetName is required' }, { status: 400 }) + } + + const retailers = await generateRetailerUrls(body.legoSetName) + + return Response.json({ retailers }) + } catch (error) { + console.error('Error generating URLs:', error) + return Response.json( + { error: 'Failed to generate retailer URLs' }, + { status: 500 } + ) + } +} diff --git a/lego-hunter/app/api/search-lego/route.ts b/lego-hunter/app/api/search-lego/route.ts new file mode 100644 index 0000000..417f046 --- /dev/null +++ b/lego-hunter/app/api/search-lego/route.ts @@ -0,0 +1,282 @@ +import { analyzeBestDeal } from '@/lib/gemini-client' +import type { Retailer, ProductData, SSEEvent, MinoSSEEvent } from '@/types' + +interface SearchLegoRequest { + legoSetName: string + maxBudget: number + retailers: Retailer[] +} + +export async function POST(request: Request) { + const body: SearchLegoRequest = await request.json() + const { legoSetName, maxBudget, retailers } = body + + if (!legoSetName || !retailers || retailers.length === 0) { + return Response.json( + { error: 'legoSetName and retailers are required' }, + { status: 400 } + ) + } + + // Create a TransformStream for SSE + const encoder = new TextEncoder() + const stream = new TransformStream() + const writer = stream.writable.getWriter() + + // Helper to send SSE events + const sendEvent = async (event: SSEEvent) => { + const data = `data: ${JSON.stringify({ ...event, timestamp: Date.now() })}\n\n` + await writer.write(encoder.encode(data)) + } + + // Start processing in background + processRetailers(retailers, legoSetName, maxBudget, sendEvent, writer) + + return new Response(stream.readable, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + } + }) +} + +async function processRetailers( + retailers: Retailer[], + legoSetName: string, + maxBudget: number, + sendEvent: (event: SSEEvent) => Promise, + writer: WritableStreamDefaultWriter +) { + const results: ProductData[] = [] + + try { + // Launch all scraping tasks in parallel + const scrapePromises = retailers.map(retailer => + scrapeRetailer(retailer, legoSetName, sendEvent) + .then(data => { + if (data) { + results.push(data) + } + return data + }) + .catch(async error => { + console.error(`Error scraping ${retailer.name}:`, error) + await sendEvent({ + type: 'retailer_error', + retailer: retailer.name, + error: error.message || 'Scraping failed' + }) + return null + }) + ) + + // Wait for all scraping to complete + await Promise.allSettled(scrapePromises) + + // Analyze results with Gemini if we have any + if (results.length > 0) { + try { + const bestDeal = await analyzeBestDeal(legoSetName, maxBudget, results) + await sendEvent({ + type: 'analysis_complete', + bestDeal + }) + } catch (error) { + console.error('Error analyzing deals:', error) + await sendEvent({ + type: 'error', + error: 'Failed to analyze deals' + }) + } + } else { + await sendEvent({ + type: 'analysis_complete', + bestDeal: { + bestRetailer: 'None', + reason: 'No retailers returned results. Please try again.', + totalCost: 'N/A', + savings: 'N/A' + } + }) + } + } catch (error) { + console.error('Error processing retailers:', error) + await sendEvent({ + type: 'error', + error: 'Failed to process retailers' + }) + } finally { + await writer.close() + } +} + +async function scrapeRetailer( + retailer: Retailer, + legoSetName: string, + sendEvent: (event: SSEEvent) => Promise +): Promise { + const MINO_API_KEY = process.env.MINO_API_KEY + + if (!MINO_API_KEY) { + throw new Error('MINO_API_KEY not configured') + } + + // Send start event + await sendEvent({ + type: 'retailer_start', + retailer: retailer.name + }) + + const minoResponse = await fetch('https://mino.ai/v1/automation/run-sse', { + method: 'POST', + headers: { + 'X-API-Key': MINO_API_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url: retailer.url, + goal: `Search for "${legoSetName}" Lego set on this retailer website and extract product information. + +Your task: +1. Look for the Lego set on this page (it may be a search results page) +2. Find the specific product that matches "${legoSetName}" +3. Extract the following information: + +Return the result as JSON with these exact fields: +{ + "inStock": true or false (whether the product is available to purchase), + "price": "99.99" (just the number, no currency symbol), + "currency": "USD" (or appropriate currency), + "shipping": "Free shipping" or "Shipping: $X.XX" or "Check website for shipping", + "productUrl": "full URL to the product page if found, otherwise the search page URL" +} + +If the product is not found on this page, return: +{ + "inStock": false, + "price": "0", + "currency": "USD", + "shipping": "N/A", + "productUrl": "${retailer.url}" +} + +Important: Return ONLY the JSON object, no additional text.`, + browser_profile: 'lite' + }) + }) + + if (!minoResponse.ok) { + throw new Error(`Mino API error: ${minoResponse.status}`) + } + + const reader = minoResponse.body?.getReader() + if (!reader) { + throw new Error('No response body from Mino') + } + + const decoder = new TextDecoder() + let buffer = '' + let streamingUrl: string | undefined + let finalResult: ProductData | null = null + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() ?? '' + + for (const line of lines) { + if (!line.startsWith('data: ')) continue + + try { + const minoEvent: MinoSSEEvent = JSON.parse(line.slice(6)) + + // Capture streaming URL for browser preview + if (minoEvent.streamingUrl && !streamingUrl) { + streamingUrl = minoEvent.streamingUrl + await sendEvent({ + type: 'retailer_start', + retailer: retailer.name, + streamingUrl + }) + } + + // Forward step events for progress updates + if (minoEvent.type === 'STEP') { + await sendEvent({ + type: 'retailer_step', + retailer: retailer.name, + step: minoEvent.step || minoEvent.message || 'Processing...' + }) + } + + // Handle completion + if (minoEvent.type === 'COMPLETE' && minoEvent.status === 'COMPLETED') { + let resultData = minoEvent.resultJson + + // Try to parse if it's a string + if (typeof resultData === 'string') { + try { + resultData = JSON.parse(resultData) + } catch { + // If parsing fails, create default result + resultData = { + retailer: retailer.name, + inStock: false, + price: '0', + currency: 'USD', + shipping: 'N/A', + productUrl: retailer.url + } + } + } + + finalResult = { + retailer: retailer.name, + inStock: resultData?.inStock ?? false, + price: String(resultData?.price ?? '0'), + currency: resultData?.currency ?? 'USD', + shipping: resultData?.shipping ?? 'N/A', + productUrl: resultData?.productUrl ?? retailer.url + } + + // Send stock found event if in stock + if (finalResult.inStock) { + await sendEvent({ + type: 'retailer_stock_found', + retailer: retailer.name + }) + } + + // Send completion event + await sendEvent({ + type: 'retailer_complete', + retailer: retailer.name, + data: finalResult + }) + + break + } + + // Handle errors from Mino + if (minoEvent.type === 'ERROR' || minoEvent.status === 'FAILED') { + throw new Error(minoEvent.message || 'Mino scraping failed') + } + } catch (parseError) { + // Ignore parse errors for individual events + console.warn('Failed to parse Mino event:', parseError) + } + } + + if (finalResult) break + } + } finally { + reader.releaseLock() + } + + return finalResult +} diff --git a/lego-hunter/app/favicon.ico b/lego-hunter/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/lego-hunter/app/globals.css b/lego-hunter/app/globals.css new file mode 100644 index 0000000..7edabc2 --- /dev/null +++ b/lego-hunter/app/globals.css @@ -0,0 +1,912 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +/* Lego Color Palette - Refined */ +:root { + /* Core Lego colors */ + --lego-yellow: #FFCF00; + --lego-yellow-light: #FFE566; + --lego-yellow-dark: #D4AC00; + --lego-red: #E4002B; + --lego-red-dark: #B8001F; + --lego-blue: #0055BF; + --lego-blue-light: #1A6FCF; + --lego-blue-dark: #003D8F; + --lego-green: #00852B; + --lego-orange: #FE5000; + + /* Neutrals */ + --lego-black: #0D0D0D; + --lego-white: #FFFFFF; + --lego-cream: #FAFAF8; + --lego-gray-100: #F5F5F4; + --lego-gray-200: #E8E8E6; + --lego-gray-300: #D4D4D2; + --lego-gray-400: #A3A3A1; + --lego-gray-500: #737371; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-body); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); +} + +:root { + --radius: 0.625rem; + --background: var(--lego-cream); + --foreground: var(--lego-black); + --card: var(--lego-white); + --card-foreground: var(--lego-black); + --popover: var(--lego-white); + --popover-foreground: var(--lego-black); + --primary: var(--lego-blue); + --primary-foreground: var(--lego-white); + --secondary: var(--lego-gray-100); + --secondary-foreground: var(--lego-black); + --muted: var(--lego-gray-100); + --muted-foreground: var(--lego-gray-500); + --accent: var(--lego-yellow); + --accent-foreground: var(--lego-black); + --destructive: var(--lego-red); + --border: var(--lego-gray-200); + --input: var(--lego-gray-200); + --ring: var(--lego-blue); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground antialiased; + font-family: var(--font-body); + } + h1, h2, h3, h4, h5, h6 { + font-family: var(--font-display); + } +} + +/* ============================================ + LEGO 3D BRICK EFFECTS + ============================================ */ + +/* Primary CTA Button - Yellow Brick */ +.brick-button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.875rem 2rem; + font-family: var(--font-display); + font-weight: 800; + font-size: 0.9375rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--lego-black); + background: linear-gradient(180deg, var(--lego-yellow-light) 0%, var(--lego-yellow) 50%, var(--lego-yellow-dark) 100%); + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 4px 0 0 #B89B00, + 0 6px 8px -2px rgba(0, 0, 0, 0.2), + inset 0 1px 0 0 rgba(255, 255, 255, 0.4); +} + +.brick-button:hover { + transform: translateY(-2px); + box-shadow: + 0 6px 0 0 #B89B00, + 0 10px 16px -4px rgba(0, 0, 0, 0.25), + inset 0 1px 0 0 rgba(255, 255, 255, 0.4); +} + +.brick-button:active { + transform: translateY(2px); + box-shadow: + 0 2px 0 0 #B89B00, + 0 3px 4px -1px rgba(0, 0, 0, 0.15), + inset 0 1px 0 0 rgba(255, 255, 255, 0.4); +} + +.brick-button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Red Brick Button */ +.brick-button-red { + color: white; + background: linear-gradient(180deg, #FF1A40 0%, var(--lego-red) 50%, var(--lego-red-dark) 100%); + box-shadow: + 0 4px 0 0 #8C0015, + 0 6px 8px -2px rgba(0, 0, 0, 0.2), + inset 0 1px 0 0 rgba(255, 255, 255, 0.3); +} + +.brick-button-red:hover { + box-shadow: + 0 6px 0 0 #8C0015, + 0 10px 16px -4px rgba(0, 0, 0, 0.25), + inset 0 1px 0 0 rgba(255, 255, 255, 0.3); +} + +/* Blue Brick Button */ +.brick-button-blue { + color: white; + background: linear-gradient(180deg, var(--lego-blue-light) 0%, var(--lego-blue) 50%, var(--lego-blue-dark) 100%); + box-shadow: + 0 4px 0 0 #002D66, + 0 6px 8px -2px rgba(0, 0, 0, 0.2), + inset 0 1px 0 0 rgba(255, 255, 255, 0.25); +} + +.brick-button-blue:hover { + box-shadow: + 0 6px 0 0 #002D66, + 0 10px 16px -4px rgba(0, 0, 0, 0.25), + inset 0 1px 0 0 rgba(255, 255, 255, 0.25); +} + +/* ============================================ + CARD STYLES - LEGO BOX AESTHETIC + ============================================ */ + +.brick-card { + position: relative; + background: var(--lego-white); + border-radius: 12px; + overflow: hidden; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + 0 4px 12px rgba(0, 0, 0, 0.04); +} + +.brick-card:hover { + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.1), + 0 8px 24px rgba(0, 0, 0, 0.06); +} + +/* Card with colored top border */ +.brick-card-accent { + border-top: 4px solid var(--lego-blue); +} + +.brick-card-accent-yellow { + border-top: 4px solid var(--lego-yellow); +} + +.brick-card-accent-red { + border-top: 4px solid var(--lego-red); +} + +.brick-card-accent-green { + border-top: 4px solid var(--lego-green); +} + +/* Interactive card */ +.brick-card-interactive { + cursor: pointer; +} + +.brick-card-interactive:hover { + transform: translateY(-2px); +} + +/* ============================================ + BRICK STUD DECORATIONS + ============================================ */ + +.brick-stud { + width: 12px; + height: 12px; + border-radius: 50%; + background: linear-gradient(145deg, rgba(255,255,255,0.5) 0%, transparent 60%); + box-shadow: + inset 0 -2px 3px rgba(0, 0, 0, 0.15), + 0 1px 1px rgba(0, 0, 0, 0.1); +} + +.brick-stud-yellow { + background: linear-gradient(145deg, var(--lego-yellow-light) 0%, var(--lego-yellow) 100%); + box-shadow: + inset 0 -2px 3px rgba(0, 0, 0, 0.2), + 0 1px 2px rgba(0, 0, 0, 0.15); +} + +.brick-stud-red { + background: linear-gradient(145deg, #FF4D6A 0%, var(--lego-red) 100%); +} + +.brick-stud-blue { + background: linear-gradient(145deg, var(--lego-blue-light) 0%, var(--lego-blue) 100%); +} + +/* Stud row decoration */ +.brick-studs-row { + display: flex; + gap: 8px; +} + +/* ============================================ + INPUT STYLES + ============================================ */ + +.brick-input { + width: 100%; + padding: 0.875rem 1rem; + font-family: var(--font-body); + font-size: 1rem; + font-weight: 500; + color: var(--lego-black); + background: var(--lego-white); + border: 2px solid var(--lego-gray-200); + border-radius: 8px; + transition: all 0.2s ease; +} + +.brick-input::placeholder { + color: var(--lego-gray-400); + font-weight: 400; +} + +.brick-input:hover { + border-color: var(--lego-gray-300); +} + +.brick-input:focus { + outline: none; + border-color: var(--lego-blue); + box-shadow: 0 0 0 3px rgba(0, 85, 191, 0.15); +} + +.brick-input:disabled { + background: var(--lego-gray-100); + cursor: not-allowed; +} + +/* ============================================ + ANIMATIONS + ============================================ */ + +@keyframes brick-stack { + 0%, 100% { transform: translateY(0); } + 25% { transform: translateY(-6px); } + 50% { transform: translateY(-2px); } + 75% { transform: translateY(-8px); } +} + +@keyframes brick-click { + 0% { transform: scale(1) translateY(0); } + 50% { transform: scale(0.95) translateY(2px); } + 100% { transform: scale(1) translateY(0); } +} + +@keyframes glow-yellow { + 0%, 100% { box-shadow: 0 0 20px rgba(255, 207, 0, 0.3); } + 50% { box-shadow: 0 0 35px rgba(255, 207, 0, 0.6); } +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +/* Animation utilities */ +.animate-brick-stack { + animation: brick-stack 1s ease-in-out infinite; +} + +.animate-brick-click { + animation: brick-click 0.3s ease-out; +} + +.animate-glow-yellow { + animation: glow-yellow 2s ease-in-out infinite; +} + +.animate-fade-up { + animation: fade-up 0.4s ease-out forwards; +} + +.animate-scale-in { + animation: scale-in 0.3s ease-out forwards; +} + +/* Staggered animations */ +.stagger-1 { animation-delay: 0.05s; } +.stagger-2 { animation-delay: 0.1s; } +.stagger-3 { animation-delay: 0.15s; } +.stagger-4 { animation-delay: 0.2s; } +.stagger-5 { animation-delay: 0.25s; } + +/* ============================================ + STATUS INDICATORS + ============================================ */ + +.status-searching { + border-left: 4px solid var(--lego-orange); +} + +.status-complete { + border-left: 4px solid var(--lego-green); +} + +.status-error { + border-left: 4px solid var(--lego-red); +} + +.status-idle { + border-left: 4px solid var(--lego-gray-300); +} + +.status-stock-found { + border-left: 4px solid var(--lego-yellow); + animation: glow-yellow 2s ease-in-out infinite; +} + +/* ============================================ + PROGRESS BAR + ============================================ */ + +.brick-progress { + height: 8px; + background: var(--lego-gray-200); + border-radius: 4px; + overflow: hidden; +} + +.brick-progress-bar { + height: 100%; + background: linear-gradient(90deg, var(--lego-yellow) 0%, var(--lego-yellow-light) 50%, var(--lego-yellow) 100%); + background-size: 200% 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +.brick-progress-bar.loading { + animation: shimmer 1.5s infinite; +} + +/* ============================================ + DECORATIVE ELEMENTS + ============================================ */ + +/* Subtle brick pattern for backgrounds */ +.brick-pattern-subtle { + background-image: + radial-gradient(circle at 20px 20px, rgba(0, 0, 0, 0.02) 2px, transparent 2px); + background-size: 40px 40px; +} + +/* Colored accent stripe */ +.accent-stripe { + position: relative; +} + +.accent-stripe::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, + var(--lego-red) 0%, var(--lego-red) 20%, + var(--lego-yellow) 20%, var(--lego-yellow) 40%, + var(--lego-blue) 40%, var(--lego-blue) 60%, + var(--lego-green) 60%, var(--lego-green) 80%, + var(--lego-orange) 80%, var(--lego-orange) 100% + ); +} + +/* ============================================ + TABLE STYLES + ============================================ */ + +.brick-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.brick-table th { + padding: 0.875rem 1rem; + font-family: var(--font-display); + font-weight: 700; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--lego-gray-500); + background: var(--lego-gray-100); + border-bottom: 2px solid var(--lego-gray-200); + text-align: left; +} + +.brick-table th:first-child { + border-top-left-radius: 8px; +} + +.brick-table th:last-child { + border-top-right-radius: 8px; +} + +.brick-table td { + padding: 1rem; + border-bottom: 1px solid var(--lego-gray-200); + font-size: 0.9375rem; +} + +.brick-table tr:last-child td { + border-bottom: none; +} + +.brick-table tr:last-child td:first-child { + border-bottom-left-radius: 8px; +} + +.brick-table tr:last-child td:last-child { + border-bottom-right-radius: 8px; +} + +.brick-table tbody tr { + transition: background-color 0.15s ease; +} + +.brick-table tbody tr:hover { + background: var(--lego-gray-100); +} + +/* Out of stock row */ +.brick-table tr.out-of-stock { + opacity: 0.5; +} + +.brick-table tr.out-of-stock:hover { + opacity: 0.7; +} + +/* ============================================ + TYPOGRAPHY UTILITIES + ============================================ */ + +.text-display { + font-family: var(--font-display); +} + +.text-body { + font-family: var(--font-body); +} + +/* ============================================ + BADGE STYLES + ============================================ */ + +.brick-badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.625rem; + font-family: var(--font-display); + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + border-radius: 4px; +} + +.brick-badge-green { + color: #065F46; + background: #D1FAE5; +} + +.brick-badge-red { + color: #991B1B; + background: #FEE2E2; +} + +.brick-badge-yellow { + color: #92400E; + background: #FEF3C7; +} + +.brick-badge-blue { + color: #1E40AF; + background: #DBEAFE; +} + +.brick-badge-orange { + color: #9A3412; + background: #FFEDD5; +} + +/* ============================================ + LOADING SKELETON + ============================================ */ + +.brick-skeleton { + background: linear-gradient(90deg, + var(--lego-gray-200) 0%, + var(--lego-gray-100) 50%, + var(--lego-gray-200) 100% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 4px; +} + +/* ============================================ + LEGO BRICK RETAILER CARDS + ============================================ */ + +/* Base Lego Brick Structure */ +.lego-brick-blue, +.lego-brick-red, +.lego-brick-yellow, +.lego-brick-green, +.lego-brick-orange, +.lego-brick-gray { + position: relative; + border-radius: 8px; + overflow: visible; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.lego-brick-blue:hover, +.lego-brick-red:hover, +.lego-brick-yellow:hover, +.lego-brick-green:hover, +.lego-brick-orange:hover, +.lego-brick-gray:hover { + transform: translateY(-4px); +} + +/* Blue Brick */ +.lego-brick-blue { + background: linear-gradient(180deg, var(--lego-blue-light) 0%, var(--lego-blue) 50%, var(--lego-blue-dark) 100%); + box-shadow: + 0 6px 0 0 #002D66, + 0 8px 16px -4px rgba(0, 0, 0, 0.3), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-blue:hover { + box-shadow: + 0 8px 0 0 #002D66, + 0 12px 24px -4px rgba(0, 0, 0, 0.35), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-blue .lego-stud-3d { + background: linear-gradient(145deg, #3399FF 0%, var(--lego-blue) 100%); + box-shadow: + inset 0 -3px 4px rgba(0, 0, 0, 0.3), + 0 2px 3px rgba(0, 0, 0, 0.2); +} + +/* Red Brick */ +.lego-brick-red { + background: linear-gradient(180deg, #FF4D6A 0%, var(--lego-red) 50%, var(--lego-red-dark) 100%); + box-shadow: + 0 6px 0 0 #8C0015, + 0 8px 16px -4px rgba(0, 0, 0, 0.3), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-red:hover { + box-shadow: + 0 8px 0 0 #8C0015, + 0 12px 24px -4px rgba(0, 0, 0, 0.35), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-red .lego-stud-3d { + background: linear-gradient(145deg, #FF6680 0%, var(--lego-red) 100%); + box-shadow: + inset 0 -3px 4px rgba(0, 0, 0, 0.3), + 0 2px 3px rgba(0, 0, 0, 0.2); +} + +/* Yellow Brick */ +.lego-brick-yellow { + background: linear-gradient(180deg, var(--lego-yellow-light) 0%, var(--lego-yellow) 50%, var(--lego-yellow-dark) 100%); + box-shadow: + 0 6px 0 0 #B89B00, + 0 8px 16px -4px rgba(0, 0, 0, 0.25), + inset 0 1px 0 0 rgba(255, 255, 255, 0.4); + animation: glow-yellow 2s ease-in-out infinite; +} + +.lego-brick-yellow:hover { + box-shadow: + 0 8px 0 0 #B89B00, + 0 12px 24px -4px rgba(0, 0, 0, 0.3), + inset 0 1px 0 0 rgba(255, 255, 255, 0.4); +} + +.lego-brick-yellow .lego-stud-3d { + background: linear-gradient(145deg, #FFEE99 0%, var(--lego-yellow) 100%); + box-shadow: + inset 0 -3px 4px rgba(0, 0, 0, 0.2), + 0 2px 3px rgba(0, 0, 0, 0.15); +} + +.lego-brick-yellow .lego-brick-body { + color: var(--lego-black); +} + +.lego-brick-yellow .lego-brick-body span, +.lego-brick-yellow .lego-brick-body p { + color: var(--lego-black); +} + +/* Green Brick */ +.lego-brick-green { + background: linear-gradient(180deg, #00A33D 0%, var(--lego-green) 50%, #006B23 100%); + box-shadow: + 0 6px 0 0 #004D16, + 0 8px 16px -4px rgba(0, 0, 0, 0.3), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-green:hover { + box-shadow: + 0 8px 0 0 #004D16, + 0 12px 24px -4px rgba(0, 0, 0, 0.35), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-green .lego-stud-3d { + background: linear-gradient(145deg, #00C44A 0%, var(--lego-green) 100%); + box-shadow: + inset 0 -3px 4px rgba(0, 0, 0, 0.3), + 0 2px 3px rgba(0, 0, 0, 0.2); +} + +/* Orange Brick */ +.lego-brick-orange { + background: linear-gradient(180deg, #FF7033 0%, var(--lego-orange) 50%, #CC4000 100%); + box-shadow: + 0 6px 0 0 #993000, + 0 8px 16px -4px rgba(0, 0, 0, 0.3), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-orange:hover { + box-shadow: + 0 8px 0 0 #993000, + 0 12px 24px -4px rgba(0, 0, 0, 0.35), + inset 0 1px 0 0 rgba(255, 255, 255, 0.2); +} + +.lego-brick-orange .lego-stud-3d { + background: linear-gradient(145deg, #FF8C5A 0%, var(--lego-orange) 100%); + box-shadow: + inset 0 -3px 4px rgba(0, 0, 0, 0.3), + 0 2px 3px rgba(0, 0, 0, 0.2); +} + +/* Gray Brick */ +.lego-brick-gray { + background: linear-gradient(180deg, var(--lego-gray-300) 0%, var(--lego-gray-400) 50%, var(--lego-gray-500) 100%); + box-shadow: + 0 6px 0 0 #5A5A58, + 0 8px 16px -4px rgba(0, 0, 0, 0.25), + inset 0 1px 0 0 rgba(255, 255, 255, 0.3); +} + +.lego-brick-gray:hover { + box-shadow: + 0 8px 0 0 #5A5A58, + 0 12px 24px -4px rgba(0, 0, 0, 0.3), + inset 0 1px 0 0 rgba(255, 255, 255, 0.3); +} + +.lego-brick-gray .lego-stud-3d { + background: linear-gradient(145deg, var(--lego-gray-200) 0%, var(--lego-gray-400) 100%); + box-shadow: + inset 0 -3px 4px rgba(0, 0, 0, 0.2), + 0 2px 3px rgba(0, 0, 0, 0.15); +} + +/* Lego Studs Container */ +.lego-studs { + display: flex; + justify-content: center; + gap: 10px; + padding: 8px 0; + position: relative; + z-index: 1; +} + +/* 3D Stud Effect */ +.lego-stud-3d { + width: 18px; + height: 18px; + border-radius: 50%; + position: relative; +} + +/* Lego Brick Body */ +.lego-brick-body { + padding: 0 16px 16px 16px; + color: white; +} + +/* Browser Preview Container */ +.lego-browser-preview { + width: 100%; + height: 140px; + border-radius: 6px; + overflow: hidden; + background: var(--lego-white); + border: 3px solid rgba(0, 0, 0, 0.15); + position: relative; +} + +.lego-browser-preview iframe { + width: 100%; + height: 100%; + border: none; + background: white; +} + +/* Browser preview loading state */ +.lego-browser-preview::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: shimmer 1.5s infinite; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; +} + +.lego-brick-orange .lego-browser-preview::before { + opacity: 1; +} + +/* Brick Card Entrance Animation */ +.lego-brick-card { + animation: brick-entrance 0.4s ease-out forwards; + opacity: 1; +} + +@keyframes brick-entrance { + 0% { + opacity: 0; + transform: translateY(20px) scale(0.95); + } + 60% { + transform: translateY(-4px) scale(1.02); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Success Glow Effect for In-Stock Items */ +.lego-brick-success { + animation: success-glow 2s ease-in-out infinite; +} + +@keyframes success-glow { + 0%, 100% { + filter: drop-shadow(0 0 8px rgba(0, 133, 43, 0.4)); + } + 50% { + filter: drop-shadow(0 0 20px rgba(0, 133, 43, 0.7)); + } +} + +/* Yellow brick (celebrating) glow override */ +.lego-brick-yellow.lego-brick-success { + animation: yellow-celebrate 1.5s ease-in-out infinite; +} + +@keyframes yellow-celebrate { + 0%, 100% { + filter: drop-shadow(0 0 10px rgba(255, 207, 0, 0.5)); + transform: scale(1); + } + 50% { + filter: drop-shadow(0 0 25px rgba(255, 207, 0, 0.9)); + transform: scale(1.02); + } +} + +/* Green brick (found) enhanced glow */ +.lego-brick-green.lego-brick-success { + position: relative; +} + +.lego-brick-green.lego-brick-success::after { + content: ''; + position: absolute; + inset: -2px; + border-radius: 10px; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + pointer-events: none; + animation: shine-sweep 3s ease-in-out infinite; +} + +@keyframes shine-sweep { + 0% { + opacity: 0; + transform: translateX(-100%); + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateX(100%); + } +} diff --git a/lego-hunter/app/layout.tsx b/lego-hunter/app/layout.tsx new file mode 100644 index 0000000..da55156 --- /dev/null +++ b/lego-hunter/app/layout.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import { Nunito, Plus_Jakarta_Sans } from "next/font/google"; +import "./globals.css"; + +const nunito = Nunito({ + variable: "--font-display", + subsets: ["latin"], + weight: ["400", "600", "700", "800", "900"], +}); + +const plusJakarta = Plus_Jakarta_Sans({ + variable: "--font-body", + subsets: ["latin"], + weight: ["400", "500", "600", "700"], +}); + +export const metadata: Metadata = { + title: "Lego Restock Hunter | Find In-Stock Lego Sets", + description: "Search 15 toy retailers simultaneously to find sold-out Lego sets that have been restocked. Never miss a Lego restock again!", + keywords: ["lego", "restock", "toys", "finder", "in stock", "sold out"], +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/lego-hunter/app/page.tsx b/lego-hunter/app/page.tsx new file mode 100644 index 0000000..d3266db --- /dev/null +++ b/lego-hunter/app/page.tsx @@ -0,0 +1,688 @@ +'use client' + +import { useState, useCallback } from 'react' +import { Search, Loader2, Zap, Store, Trophy, ExternalLink, Package, PackageX, AlertCircle, Eye, EyeOff } from 'lucide-react' +import { triggerLegoConfetti, triggerVictoryConfetti } from '@/components/lego-confetti' +import { DEFAULT_RETAILERS } from '@/lib/retailers' +import type { + Retailer, + RetailerStatus, + ProductData, + DealAnalysis, + SSEEvent +} from '@/types' + +export default function LegoFinderPage() { + const [legoSetName, setLegoSetName] = useState('') + const [maxBudget, setMaxBudget] = useState('') + const [isSearching, setIsSearching] = useState(false) + const [isGeneratingUrls, setIsGeneratingUrls] = useState(false) + const [retailers, setRetailers] = useState>({}) + const [results, setResults] = useState([]) + const [bestDeal, setBestDeal] = useState(null) + const [error, setError] = useState(null) + const [showAgents, setShowAgents] = useState(true) + + const initializeRetailers = useCallback((retailerList: Retailer[]) => { + const initial: Record = {} + retailerList.forEach(r => { + initial[r.name] = { name: r.name, status: 'idle', steps: [] } + }) + setRetailers(initial) + }, []) + + const handleSSEEvent = useCallback((event: SSEEvent) => { + switch (event.type) { + case 'retailer_start': + setRetailers(prev => ({ + ...prev, + [event.retailer!]: { + ...prev[event.retailer!], + status: 'searching', + streamingUrl: event.streamingUrl || prev[event.retailer!]?.streamingUrl + } + })) + break + case 'retailer_step': + setRetailers(prev => ({ + ...prev, + [event.retailer!]: { + ...prev[event.retailer!], + steps: [...(prev[event.retailer!]?.steps || []).slice(-10), event.step!] + } + })) + break + case 'retailer_stock_found': + triggerLegoConfetti() + setRetailers(prev => ({ + ...prev, + [event.retailer!]: { ...prev[event.retailer!], stockFound: true } + })) + break + case 'retailer_complete': + setRetailers(prev => ({ + ...prev, + [event.retailer!]: { ...prev[event.retailer!], status: 'complete', data: event.data } + })) + if (event.data) setResults(prev => [...prev, event.data!]) + break + case 'retailer_error': + setRetailers(prev => ({ + ...prev, + [event.retailer!]: { ...prev[event.retailer!], status: 'error', error: event.error } + })) + break + case 'analysis_complete': + setBestDeal(event.bestDeal || null) + setIsSearching(false) + if (event.bestDeal && event.bestDeal.bestRetailer !== 'None') { + triggerVictoryConfetti() + } + break + case 'error': + setError(event.error || 'An error occurred') + setIsSearching(false) + break + } + }, []) + + const handleSearch = async () => { + if (!legoSetName.trim()) { + setError('Please enter a Lego set name or number') + return + } + setError(null) + setResults([]) + setBestDeal(null) + setIsSearching(true) + setIsGeneratingUrls(true) + + try { + const urlResponse = await fetch('/api/generate-urls', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ legoSetName: legoSetName.trim() }) + }) + if (!urlResponse.ok) throw new Error('Failed to generate retailer URLs') + const { retailers: generatedRetailers } = await urlResponse.json() + setIsGeneratingUrls(false) + initializeRetailers(generatedRetailers) + + const searchResponse = await fetch('/api/search-lego', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + legoSetName: legoSetName.trim(), + maxBudget: parseFloat(maxBudget) || 1000, + retailers: generatedRetailers + }) + }) + if (!searchResponse.ok) throw new Error('Failed to start search') + + const reader = searchResponse.body?.getReader() + if (!reader) throw new Error('No response stream') + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() ?? '' + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + handleSSEEvent(JSON.parse(line.slice(6))) + } catch (e) { + console.warn('Failed to parse SSE event:', e) + } + } + } + } + } catch (err) { + console.error('Search error:', err) + setError(err instanceof Error ? err.message : 'Search failed') + setIsSearching(false) + setIsGeneratingUrls(false) + } + } + + const getRetailerLogo = (name: string) => DEFAULT_RETAILERS.find(r => r.name === name)?.logo || '🏪' + const retailerList = Object.values(retailers) + const completedCount = retailerList.filter(r => r.status === 'complete' || r.status === 'error').length + const inStockCount = results.filter(r => r.inStock).length + const totalCount = retailerList.length + const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0 + + return ( +

+ {/* Colored accent stripe at top */} +
+ + {/* Hero Section */} +
+
+
+ {/* Brick stud decoration */} +
+
+
+
+
+ +

+ Lego Restock + Hunter +

+ +

+ Search 15 retailers simultaneously to find sold-out Lego sets. + Powered by AI to find you the best deal. +

+
+
+
+ + {/* Search Section */} +
+
+
+
+ {/* Search inputs */} +
+
+ + setLegoSetName(e.target.value)} + placeholder="e.g., 75192 Millennium Falcon" + className="brick-input" + disabled={isSearching} + onKeyDown={e => e.key === 'Enter' && handleSearch()} + /> +
+
+ +
+ $ + setMaxBudget(e.target.value)} + placeholder="1000" + className="brick-input pl-8" + disabled={isSearching} + /> +
+
+
+ + {/* Search button */} +
+ + + {!isSearching && ( +

+ + Searches 15 retailers in parallel +

+ )} +
+ + {/* Progress bar */} + {isSearching && ( +
+
+ + {isGeneratingUrls ? 'Generating search URLs with AI...' : `Checking ${completedCount} of ${totalCount} retailers`} + + {!isGeneratingUrls && ( + {Math.round(progress)}% + )} +
+
+
+
+ {inStockCount > 0 && ( +

+ + {inStockCount} in stock found! +

+ )} +
+ )} + + {/* Error message */} + {error && ( +
+ +

{error}

+
+ )} +
+
+
+
+ + {/* Best Deal Section */} + {bestDeal && bestDeal.bestRetailer !== 'None' && ( +
+
+
+
+
+ +
+
+

+ Best Deal Found +

+

+ {bestDeal.bestRetailer} +

+
+
+ +

+ {bestDeal.reason} +

+ +
+
+

Total Cost

+

{bestDeal.totalCost}

+
+ {bestDeal.savings && bestDeal.savings !== 'N/A' && ( +
+ {bestDeal.savings} +
+ )} +
+ + {results.find(r => r.retailer === bestDeal.bestRetailer)?.productUrl && ( + r.retailer === bestDeal.bestRetailer)?.productUrl} + target="_blank" + rel="noopener noreferrer" + className="brick-button mt-6 inline-flex text-lg px-8 py-4 animate-pulse hover:animate-none" + > + + 🛒 Buy Now - Get It Before It's Gone! + + )} +
+
+
+ )} + + {/* No Stock Found */} + {bestDeal && bestDeal.bestRetailer === 'None' && ( +
+
+
+
+ +
+

+ No Stock Found +

+

+ {bestDeal.reason} +

+
+
+
+ )} + + {/* Retailer Grid */} + {retailerList.length > 0 && ( +
+
+
+
+ +

+ Retailer Status +

+ + ({completedCount}/{totalCount} complete) + +
+ +
+ + {showAgents && ( +
+ {retailerList.map((r, i) => ( + + ))} +
+ )} +
+
+ )} + + {/* Results Table */} + {results.length > 0 && !isSearching && ( +
+
+

+ All Results +

+ +
+
+ )} + + {/* Empty State */} + {!isSearching && retailerList.length === 0 && ( +
+
+
+ {['red', 'yellow', 'blue'].map((color, i) => ( +
+ ))} +
+

+ Ready to Hunt +

+

+ Enter a Lego set name or number above to search across 15 retailers simultaneously. +

+
+
+ )} + + {/* Footer */} +
+
+
+
+
+
+
+
+ Lego Restock Hunter +
+

+ Powered by Mino AI + Gemini. Not affiliated with LEGO Group. +

+
+
+
+ ) +} + +/* Retailer Status Card Component - Lego Brick Style */ +function RetailerStatusCard({ retailer, logo, delay }: { retailer: RetailerStatus; logo: string; delay: number }) { + // Determine brick color based on status - prioritize complete status over stockFound + const getBrickColor = () => { + // First check if search is complete + if (retailer.status === 'complete') { + return retailer.data?.inStock ? 'lego-brick-green' : 'lego-brick-gray' + } + // Error state + if (retailer.status === 'error') { + return 'lego-brick-red' + } + // Searching states + if (retailer.status === 'searching') { + // Celebration moment when stock is found but not yet complete + return retailer.stockFound ? 'lego-brick-yellow' : 'lego-brick-orange' + } + // Idle state + return 'lego-brick-blue' + } + + // Check if this is a "success" card (in stock and complete) + const isInStock = retailer.status === 'complete' && retailer.data?.inStock + + // Determine if card should have celebration glow + const shouldGlow = retailer.stockFound || isInStock + + return ( +
+ {/* Lego Studs */} +
+
+
+
+
+
+ + {/* Brick Body */} +
+ {/* Header */} +
+
+ {logo} + {retailer.name} +
+ +
+ + {/* Browser Preview */} +
+ {retailer.streamingUrl ? ( +