diff --git a/src/app/page.tsx b/src/app/page.tsx index aa9d8f4..848a977 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,63 @@ -export default function Home() { - return Preis-Tracker +import Link from 'next/link' +import { desc, eq, sql } from 'drizzle-orm' +import { db, products, priceSnapshots } from '@/lib/db' +import { ProductCard } from '@/components/ProductCard' + +export const dynamic = 'force-dynamic' + +export default async function Home() { + const rows = await db.execute<{ + id: string; url: string; shop: string; name: string; image_url: string | null; + last_price: string | null; min_price: string | null; + }>(sql` + select p.id, p.url, p.shop, p.name, p.image_url, + (select price from price_snapshots s where s.product_id = p.id and s.price is not null order by scraped_at desc limit 1) as last_price, + (select min(price) from price_snapshots s where s.product_id = p.id and s.price is not null) as min_price + from products p where p.enabled = true order by p.created_at desc + `) + + const sparklines = new Map>() + for (const r of rows) { + const snaps = await db.select({ price: priceSnapshots.price, scrapedAt: priceSnapshots.scrapedAt }) + .from(priceSnapshots) + .where(eq(priceSnapshots.productId, r.id)) + .orderBy(desc(priceSnapshots.scrapedAt)) + .limit(30) + sparklines.set(r.id, snaps + .filter((s) => s.price !== null) + .reverse() + .map((s) => ({ price: Number(s.price), t: s.scrapedAt.toISOString() }))) + } + + return ( + + + Preis-Tracker + + + Produkt + Logout + + + {rows.length === 0 ? ( + + Noch keine Produkte. Erstes hinzufügen. + + ) : ( + + {rows.map((r) => ( + + ))} + + )} + + ) } diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx new file mode 100644 index 0000000..53e21e7 --- /dev/null +++ b/src/components/ProductCard.tsx @@ -0,0 +1,37 @@ +import Link from 'next/link' +import { Sparkline } from './Sparkline' + +interface Props { + id: string + name: string + shop: string + imageUrl: string | null + lastPrice: string | null + minPrice: string | null + sparkline: Array<{ price: number; t: string }> +} + +export function ProductCard(p: Props) { + const last = p.lastPrice ? Number(p.lastPrice) : null + const min = p.minPrice ? Number(p.minPrice) : null + const deltaFromMin = last !== null && min !== null ? (last - min).toFixed(2) : null + + return ( + + + {p.imageUrl && } + + {p.shop} + {p.name} + + {last !== null ? `${last.toFixed(2)} €` : '—'} + {deltaFromMin !== null && ( + +{deltaFromMin} € vom Tief + )} + + + + + + ) +} diff --git a/src/components/Sparkline.tsx b/src/components/Sparkline.tsx new file mode 100644 index 0000000..6ee6ddc --- /dev/null +++ b/src/components/Sparkline.tsx @@ -0,0 +1,14 @@ +'use client' +import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts' + +export function Sparkline({ data }: { data: Array<{ price: number; t: string }> }) { + if (data.length === 0) return keine Daten + return ( + + + + + + + ) +}