diff --git a/.aiideas b/.aiideas index 904ea96..ccb5919 100644 --- a/.aiideas +++ b/.aiideas @@ -1,103 +1,28 @@ -Rolle: Du bist ein UI - Designer mit Fokus auf "Modern Minimalist" Design(Stilrichtung: Linear, Teenage Engineering, Vercel).Ziel: Redesign der Whisky - App "DramLog".Wir verabschieden uns vom klassischen "Luxus-Look"(Gold / Serifen) und nutzen einen "Industrial Dark" Stil. +Act as a Senior TypeScript/Next.js Developer. -Design Rules & Tokens: +I need a robust client-side image processing utility (an "Image Agent") to optimize user uploads before sending them to an LLM or Supabase. - Color Palette(Tailwind): +**Task:** +Create a utility file `src/utils/image-processing.ts`. +This file should export a function `processImageForAI` that uses the library `browser-image-compression`. -Bg - App: bg - zinc - 950(Ein sehr dunkles, warmes Grau, kein hartes Schwarz). +**Requirements:** +1. **Input:** The function takes a raw `File` object (from an HTML input). +2. **Processing Logic:** + - Resize the image to a maximum of **1024x1024** pixels (maintain aspect ratio). + - Convert the image to **WebP** format. + - Limit the file size to approx **0.4MB**. + - Enable `useWebWorker: true` to prevent UI freezing. +3. **Output:** The function must return a Promise that resolves to an object with this interface: + ```typescript + interface ProcessedImage { + file: File; // The compressed WebP file (ready for Supabase storage) + base64: string; // The Base64 string (ready for LLM API calls) + originalFile: File; // Pass through the original file + } + ``` +4. **Helper:** Include a helper function to convert the resulting Blob/File to a Base64 string correctly. +5. **Edge Cases:** Handle errors gracefully (try/catch) and ensure the returned `file` has the correct `.webp` extension and mime type. - Bg - Card: bg - zinc - 900(Deutlich abgesetzt vom Hintergrund). - - Text - Primary: text - zinc - 50(Fast Weiß, hoher Kontrast). - - Text - Secondary: text - zinc - 400(Mittelgrau für Labels). - - Accent: text - orange - 500(Ein sattes, mattes Orange für Highlights) und bg - orange - 600 für Primary Buttons.Keine Verläufe / Gradients, sondern flache("flat") Farben. - - Typography: - - Nutze Inter oder DM Sans für alles. - - Headlines: font - bold tracking - tight(Enger Buchstabenabstand, wirkt kompakt und modern). - - Labels: uppercase text - xs tracking - widest font - semibold(Technischer Look). - - Component: Whisky Card(Clean Split): - - Kein Text mehr über dem Bild! - -Top: Bild(Aspect Ratio 4: 3 oder 16: 9), rounded - t - xl. - - Bottom: Info - Block(p - 4 bg - zinc - 900 rounded - b - xl). - - Inhalt: Whisky Name(Bold, White), darunter Destillerie(Orange, Small). - - Tags: Kleine "Pills" mit bg - zinc - 800 text - zinc - 300. - -Component: Dashboard Stats: - -Minimalistisch.Nur die Zahl(riesig, z.B.text - 4xl font - bold text - white) und darunter das Label(text - zinc - 500). - - Keine Boxen, keine Rahmen. "Data Ink Ratio" optimieren. - - Component: Floating Navigation(The Capsule): - - Statt einer durchgehenden Leiste unten, nutze eine "Floating Capsule". - - Eine abgerundete "Insel"(rounded - full) die ca. 20px über dem unteren Bildschirmrand schwebt. - - Breite: ca. 90 % des Screens oder max - w - md. - - Farbe: bg - zinc - 800 / 90 backdrop - blur - md border border - zinc - 700. - - Scan Button: In der Mitte der Kapsel, Kreis in bg - orange - 600(Flat, kein Schatten), weißes Icon. - - Code - Snippet für die "Industrial Capsule Navigation": -JavaScript - -import { Home, Grid, Scan, User } from 'lucide-react'; - -export const NavigationCapsule = () => { - return ( -
-
- - {/* Left Item */ } - < button className = "p-3 text-zinc-400 hover:text-white transition-colors" > - - - - {/* Left Item */ } - - - {/* PRIMARY ACTION - The "Industrial Button" */ } - - - {/* Right Item */ } - - - {/* Right Item (Settings/More) */ } - - -
-
- ); -}; - -Aufgabe: Setze die bestehenden Views(Dashboard und Liste) mit diesen neuen "Industrial Dark" Regeln um.Sorge für klare Kontraste durch zinc - 950 vs zinc - 900 Flächen. -Was dieser Look ändert: - -Lesbarkeit: Durch den zinc - 900 Hintergrund der Karten hebt sich der weiße Text extrem gut ab.Kein "Text auf unruhigem Foto" - Problem mehr. - - Modernität: Das "Pill" - Menü(Capsule) sieht aus wie bei modernen iOS Apps(Dynamic Island Ästhetik). - - Ehrlichkeit: Es versucht nicht, "altes Geld"(Gold / Serifen) zu imitieren, sondern wirkt wie ein modernes Werkzeug für Enthusiasten. \ No newline at end of file +**Step 1:** Give me the `npm install` command to add the necessary library. +**Step 2:** Write the complete `src/utils/image-processing.ts` code with proper JSDoc comments. \ No newline at end of file diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 9f33ed9..3c65212 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.47.10", "@tanstack/react-query": "^5.62.7", + "browser-image-compression": "^2.0.2", "canvas-confetti": "^1.9.3", "dexie": "^4.2.1", "dexie-react-hooks": "^4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec78466..9745f10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@tanstack/react-query': specifier: ^5.62.7 version: 5.90.12(react@19.2.3) + browser-image-compression: + specifier: ^2.0.2 + version: 2.0.2 canvas-confetti: specifier: ^1.9.3 version: 1.9.4 @@ -1295,6 +1298,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-image-compression@2.0.2: + resolution: {integrity: sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2854,6 +2860,9 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true + uzip@0.20201231.0: + resolution: {integrity: sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==} + victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} @@ -4106,6 +4115,10 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-image-compression@2.0.2: + dependencies: + uzip: 0.20201231.0 + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.9 @@ -5945,6 +5958,8 @@ snapshots: uuid@13.0.0: {} + uzip@0.20201231.0: {} + victory-vendor@37.3.6: dependencies: '@types/d3-array': 3.2.2 diff --git a/src/app/bottles/[id]/page.tsx b/src/app/bottles/[id]/page.tsx index d8fd562..2f6f3f4 100644 --- a/src/app/bottles/[id]/page.tsx +++ b/src/app/bottles/[id]/page.tsx @@ -41,7 +41,7 @@ export default function BottlePage() { if (!bottleId) return null; return ( -
+
diff --git a/src/app/globals.css b/src/app/globals.css index aef6c8e..971b5b2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -25,6 +25,18 @@ body { font-feature-settings: "cv02", "cv03", "cv04", "cv11"; } +/* Global Input Text Fix */ +input, +textarea, +select { + @apply bg-zinc-950 text-white border-zinc-800 focus:ring-1 focus:ring-orange-600 outline-none transition-all; +} + +input::placeholder, +textarea::placeholder { + @apply text-zinc-600; +} + h1, h2, h3, diff --git a/src/app/page.tsx b/src/app/page.tsx index 41cf1b6..dbf7b00 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,7 +12,7 @@ import LanguageSwitcher from "@/components/LanguageSwitcher"; import OfflineIndicator from "@/components/OfflineIndicator"; import { useI18n } from "@/i18n/I18nContext"; import { useSession } from "@/context/SessionContext"; -import { Sparkles, X } from "lucide-react"; +import { Sparkles, X, Loader2 } from "lucide-react"; import { BottomNavigation } from '@/components/BottomNavigation'; import ScanAndTasteFlow from '@/components/ScanAndTasteFlow'; @@ -25,10 +25,15 @@ export default function Home() { const { t } = useI18n(); const { activeSession } = useSession(); const [isFlowOpen, setIsFlowOpen] = useState(false); - const [capturedImage, setCapturedImage] = useState(null); + const [capturedFile, setCapturedFile] = useState(null); + const [hasMounted, setHasMounted] = useState(false); - const handleImageSelected = (base64: string) => { - setCapturedImage(base64); + useEffect(() => { + setHasMounted(true); + }, []); + + const handleImageSelected = (file: File) => { + setCapturedFile(file); setIsFlowOpen(true); }; @@ -149,6 +154,14 @@ export default function Home() { await supabase.auth.signOut(); }; + if (!hasMounted) { + return ( +
+ +
+ ); + } + if (!user) { return (
@@ -257,7 +270,7 @@ export default function Home() { setIsFlowOpen(false)} - base64Image={capturedImage} + imageFile={capturedFile} />
); diff --git a/src/app/sessions/[id]/page.tsx b/src/app/sessions/[id]/page.tsx index 433ec99..fb65de2 100644 --- a/src/app/sessions/[id]/page.tsx +++ b/src/app/sessions/[id]/page.tsx @@ -188,29 +188,29 @@ export default function SessionDetailPage() { if (isLoading) { return ( -
- +
+
); } if (!session) { return ( -
-

Session nicht gefunden

- Zurück zum Start +
+

Session nicht gefunden

+ Zurück zum Start
); } return ( -
+
{/* Back Button */}
Alle Sessions @@ -219,7 +219,7 @@ export default function SessionDetailPage() {
{/* Hero */} -
+
{/* Visual Eyecatcher: Background Glow */} {tastings.length > 0 && tastings[0].bottles.image_url && (
@@ -239,7 +239,7 @@ export default function SessionDetailPage() { {/* Visual Eyecatcher: Bottle Preview */} {tastings.length > 0 && tastings[0].bottles.image_url && (
-
+
{tastings[0].bottles.name}
-
+
LATEST
@@ -255,7 +255,7 @@ export default function SessionDetailPage() {
-
+
Tasting Session
@@ -263,23 +263,23 @@ export default function SessionDetailPage() { Abgeschlossen )}
-

+

{session.name}

- - + + {new Date(session.scheduled_at).toLocaleDateString('de-DE')} {participants.length > 0 && ( -
- +
+ p.buddies.name)} limit={5} />
)} {tastings.length > 0 && ( - - + + {tastings.length} {tastings.length === 1 ? 'Whisky' : 'Whiskys'} )} @@ -292,7 +292,7 @@ export default function SessionDetailPage() { activeSession?.id !== session.id ? ( @@ -324,9 +324,9 @@ export default function SessionDetailPage() {
{/* Sidebar: Participants */}