feat(ui): implement InstaChef design system
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:
Giancarmine Salucci
2026-05-12 22:02:47 +02:00
parent 0b9f598c7d
commit 573cf49ac5
39 changed files with 2982 additions and 569 deletions

View File

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