feat(ui): implement InstaChef design system
Some checks failed
Build & Push Docker Image / test-and-build (push) Failing after 38s
Some checks failed
Build & Push Docker Image / test-and-build (push) Failing after 38s
- Replace Tailwind with IC CSS design tokens (purple/pink/orange brand gradient, Lilita One / DM Sans / JetBrains Mono fonts, light+dark theme via data-theme) - Add all SVG icon components (ic/Bell, BellOff, Check, Chevron, Clipboard, Close, Download, External, Filter, Link, Plus, Retry, Search, Settings, Share, Spark, Trash, PhasePrepping, PhaseSimmering, PhasePlating) - Add shared primitives: Chip, RecipeThumb (deterministic gradient swatch), CookingPot (animated SVG), PhaseTrack, SectionHead - Add TopBar with LIVE indicator and notification bell - Add CookingHero: animated hero card for in-progress items - Add TimelineRow: queue list row with status badges - Add EmptyState: gradient hero + dismissible How it works card - Add RecipeSheet: bottom-sheet detail overlay with phase progress - Add AddUrlScreen: full-page URL input with clipboard paste - Add NotificationsScreen: push toggle + SSE status - Rewrite +page.svelte: screen router (home/addurl/notifs) + RecipeSheet; preserves all SSE, retry, remove, filter, auto-subscribe logic - Rewrite share/+page.svelte: uses AddUrlScreen shell, preserves Share Target logic and auto-process on URL param - Rewrite InstallPrompt.svelte: InstallSheet bottom-sheet design, all PWA logic intact - Update manifest.json theme_color to #FFF8F5 - 282 unit tests passing (unchanged) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,13 +2,11 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import UrlInputSection from './components/UrlInputSection.svelte';
|
||||
import AddUrlScreen from '../components/AddUrlScreen.svelte';
|
||||
|
||||
let status = $state('idle');
|
||||
let logs = $state<string[]>([]);
|
||||
let status = $state<'idle' | 'enqueuing' | 'success' | 'error'>('idle');
|
||||
|
||||
// URL param parsing for Share Target
|
||||
// Instagram typically shares text that contains the URL, so we might need to parse it out
|
||||
let sharedText = $derived($page.url.searchParams.get('text') || '');
|
||||
let sharedUrl = $derived($page.url.searchParams.get('url') || '');
|
||||
|
||||
@@ -17,32 +15,27 @@
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
let targetUrl = $derived(sharedUrl || extractUrl(sharedText));
|
||||
let targetUrl = $derived(sharedUrl || extractUrl(sharedText) || '');
|
||||
|
||||
// Track if we've already auto-processed to prevent duplicate processing
|
||||
let hasAutoProcessed = $state(false);
|
||||
|
||||
// Auto-process URL if provided via share target
|
||||
// Use onMount instead of $effect for side effects (SvelteKit best practice)
|
||||
onMount(() => {
|
||||
if (targetUrl && status === 'idle' && !hasAutoProcessed) {
|
||||
hasAutoProcessed = true;
|
||||
process();
|
||||
process(targetUrl);
|
||||
}
|
||||
});
|
||||
|
||||
async function process(url?: string) {
|
||||
const urlToProcess = url || targetUrl;
|
||||
if (!urlToProcess) return;
|
||||
|
||||
async function process(url: string) {
|
||||
if (!url) return;
|
||||
status = 'enqueuing';
|
||||
logs = [...logs, '🚀 Enqueuing extraction from: ' + urlToProcess];
|
||||
|
||||
try {
|
||||
// Enqueue URL for background processing
|
||||
const response = await fetch('/api/queue', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ url: urlToProcess }),
|
||||
body: JSON.stringify({ url }),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
@@ -52,88 +45,100 @@
|
||||
}
|
||||
|
||||
const queueItem = await response.json();
|
||||
logs = [...logs, `✅ URL enqueued successfully with ID: ${queueItem.id}`];
|
||||
logs = [...logs, '🔄 Redirecting to queue dashboard...'];
|
||||
status = 'success';
|
||||
|
||||
// Small delay to show the success message
|
||||
setTimeout(() => {
|
||||
// Redirect to homepage (queue dashboard) with the queue item ID highlighted
|
||||
goto(`/?highlight=${queueItem.id}`);
|
||||
}, 1500);
|
||||
|
||||
}, 800);
|
||||
} catch (e) {
|
||||
status = 'error';
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||
logs = [...logs, `❌ Error: ${errorMessage}`];
|
||||
console.error('Failed to enqueue:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function retry() {
|
||||
status = 'idle';
|
||||
logs = [...logs, 'Retrying...'];
|
||||
process();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Share to InstaRecipe</title>
|
||||
<meta name="description" content="Share Instagram recipes for extraction" />
|
||||
<title>Add Recipe — InstaChef</title>
|
||||
<meta name="description" content="Share Instagram recipes to InstaChef" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto p-6 max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2 text-center">Share to InstaRecipe</h1>
|
||||
<p class="text-gray-600 text-center">
|
||||
{#if targetUrl}
|
||||
Processing your shared recipe...
|
||||
{#if status === 'enqueuing' || status === 'success'}
|
||||
<!-- Transitional feedback while enqueuing -->
|
||||
<div class="enqueue-screen">
|
||||
<div class="enqueue-card">
|
||||
{#if status === 'enqueuing'}
|
||||
<div class="spinner"></div>
|
||||
<div class="enqueue-title">Adding to queue…</div>
|
||||
{:else}
|
||||
Paste an Instagram recipe URL to extract it
|
||||
<div class="enqueue-check">✓</div>
|
||||
<div class="enqueue-title">Added! Redirecting…</div>
|
||||
{/if}
|
||||
</p>
|
||||
<div class="enqueue-url">{targetUrl}</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<AddUrlScreen
|
||||
initialUrl={targetUrl}
|
||||
onBack={() => goto('/')}
|
||||
onSubmit={process}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !targetUrl}
|
||||
<UrlInputSection onProcess={process} />
|
||||
{:else}
|
||||
<!-- Status indicator for shared URLs -->
|
||||
<div class="max-w-2xl mx-auto mb-8">
|
||||
<div class="bg-white p-6 rounded-lg shadow-md border">
|
||||
<h3 class="font-semibold mb-2">Processing URL:</h3>
|
||||
<p class="text-sm text-gray-600 mb-4 break-all">{targetUrl}</p>
|
||||
|
||||
{#if status === 'enqueuing'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
||||
<span class="text-blue-600">Enqueuing for processing...</span>
|
||||
</div>
|
||||
{:else if status === 'error'}
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<span class="text-red-600">❌ Error occurred</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={retry}
|
||||
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
{:else}
|
||||
<div class="text-green-600">✅ Ready to process</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Log viewer for feedback -->
|
||||
{#if logs.length > 0}
|
||||
<div class="max-w-2xl mx-auto mt-8">
|
||||
<div class="bg-gray-50 p-4 rounded-lg border">
|
||||
<h3 class="font-semibold mb-2">Process Log:</h3>
|
||||
<div class="space-y-1 text-sm">
|
||||
{#each logs as log}
|
||||
<div class="text-gray-700">{log}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<style>
|
||||
.enqueue-screen {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.enqueue-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 28px;
|
||||
padding: 40px 32px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.spinner {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--border);
|
||||
border-top-color: var(--pink);
|
||||
animation: spin 0.75s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.enqueue-check {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: var(--status-success);
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.enqueue-title {
|
||||
font-family: 'Lilita One', system-ui, sans-serif;
|
||||
font-size: 22px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.enqueue-url {
|
||||
font-size: 12px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--muted);
|
||||
word-break: break-all;
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user