diff --git a/app/api/products/route.ts b/app/api/products/route.ts new file mode 100644 index 0000000..cbd4a14 --- /dev/null +++ b/app/api/products/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { getProducts } from "../../lib/payload"; + +// Same-origin proxy for client components (CartContent, RelatedProducts) +// that need the full catalog reactively — Payload's own API is public-read +// and CORS wouldn't be a blocker, but going through the app's own origin +// avoids depending on that, reuses Next.js's fetch cache from getProducts() +// (no extra round trip to Payload beyond the first request within the 60s +// revalidate window), and keeps the Payload URL itself as a server-only +// implementation detail the client never talks to directly. +export async function GET() { + const products = await getProducts(); + return NextResponse.json(products); +} diff --git a/app/cart/components/CartContent.tsx b/app/cart/components/CartContent.tsx index 7507abb..4a93e69 100644 --- a/app/cart/components/CartContent.tsx +++ b/app/cart/components/CartContent.tsx @@ -4,7 +4,8 @@ import { useState } from "react"; import Link from "next/link"; import Image from "next/image"; import { useCart, removeFromCart, setQuantity } from "../../lib/cart"; -import { PRODUCTS, formatPrice } from "../../lib/products"; +import { useProducts } from "../../lib/products"; +import { formatPrice } from "../../lib/format"; import { SHIPPING_COST, FREE_SHIPPING_THRESHOLD } from "../../lib/shipping"; import { Reveal } from "../../components/Reveal"; import { VersandModal } from "../../components/VersandModal"; @@ -13,8 +14,16 @@ import { FreeShippingBanner } from "./FreeShippingBanner"; export function CartContent() { const [versandOpen, setVersandOpen] = useState(false); const cart = useCart(); + const products = useProducts(); + // While the /api/products fetch is still pending, treat a non-empty + // cart as "loading" rather than "empty" — the old hardcoded PRODUCTS + // lookup was synchronous, so this distinction didn't exist before; + // without it, a returning shopper with items already in their cart + // would briefly see the empty-cart message flash before their real + // cart content gets a chance to render. + const productsLoading = products.length === 0 && cart.length > 0; const items = cart - .map((entry) => ({ entry, product: PRODUCTS[entry.id] })) + .map((entry) => ({ entry, product: products.find((p) => p.id === entry.id) })) .filter((row): row is { entry: typeof cart[number]; product: NonNullable<(typeof row)["product"]> } => Boolean(row.product)); const subtotal = items.reduce((sum, { entry, product }) => sum + entry.qty * product.price, 0); @@ -37,11 +46,15 @@ export function CartContent() { Warenkorb

- {items.length > 0 ? "Schön, dass du da bist." : "Dein Warenkorb ist noch leer."} + {productsLoading + ? "Einen Moment…" + : items.length > 0 + ? "Schön, dass du da bist." + : "Dein Warenkorb ist noch leer."}

- {items.length === 0 ? ( + {productsLoading ? null : items.length === 0 ? (

Schau dir unsere Produkte an und finde, was zu dir passt. diff --git a/app/cart/components/FreeShippingBanner.tsx b/app/cart/components/FreeShippingBanner.tsx index 5cbd8c0..2e7d8e3 100644 --- a/app/cart/components/FreeShippingBanner.tsx +++ b/app/cart/components/FreeShippingBanner.tsx @@ -1,8 +1,9 @@ "use client"; import { useEffect, useState } from "react"; +import { AnimatePresence, motion } from "motion/react"; import { FREE_SHIPPING_THRESHOLD } from "../../lib/shipping"; -import { formatPrice } from "../../lib/products"; +import { formatPrice } from "../../lib/format"; const SUCCESS_VISIBLE_MS = 2500; @@ -49,36 +50,45 @@ export function FreeShippingBanner({ subtotal }: { subtotal: number }) { return () => clearTimeout(t); }, [phase]); - if (phase === "hidden") return null; - const remaining = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal); const progressPct = Math.min(100, (subtotal / FREE_SHIPPING_THRESHOLD) * 100); return ( -

-

- {phase === "success" - ? "Kostenloser Versand freigeschaltet ✓" - : `Noch ${formatPrice(remaining)} bis zum kostenlosen Versand!`} -

-
-
+ {phase !== "hidden" && ( + -
-
+ > +

+ {phase === "success" + ? "Kostenloser Versand freigeschaltet ✓" + : `Noch ${formatPrice(remaining)} bis zum kostenlosen Versand!`} +

+
+
+
+ + )} + ); } diff --git a/app/cart/components/RelatedProducts.tsx b/app/cart/components/RelatedProducts.tsx index 2c8f4c2..de1d1d1 100644 --- a/app/cart/components/RelatedProducts.tsx +++ b/app/cart/components/RelatedProducts.tsx @@ -1,54 +1,61 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import Image from "next/image"; -import { PRODUCTS, RELATED_PRODUCT_IDS, formatPrice } from "../../lib/products"; +import { useProducts } from "../../lib/products"; +import { formatPrice } from "../../lib/format"; import { Reveal } from "../../components/Reveal"; import { AddToCartInlineButton, FEEDBACK_MS } from "../../components/AddToCartInlineButton"; import { useCart } from "../../lib/cart"; -const ALL_PRODUCT_IDS = Object.keys(PRODUCTS); const DISPLAY_COUNT = 3; -function pickRandom(excludeIds: string[], count: number): string[] { - const pool = ALL_PRODUCT_IDS.filter((id) => !excludeIds.includes(id)); +function pickRandom(allIds: string[], excludeIds: string[], count: number): string[] { + const pool = allIds.filter((id) => !excludeIds.includes(id)); const shuffled = [...pool].sort(() => Math.random() - 0.5); return shuffled.slice(0, count); } export function RelatedProducts() { const cart = useCart(); + const products = useProducts(); const hasItems = cart.length > 0; const cartKey = cart .map((i) => i.id) .sort() .join(","); + // useMemo, not a plain .map() — .map() would return a new array + // reference on every render regardless of whether `products` itself + // changed, which would make the effect below re-run (and re-pick) every + // single render if `productIds` were listed as its dependency. + const productIds = useMemo(() => products.map((p) => p.id), [products]); - // Math.random() can't run during the render that has to match the SSR - // pass (that would be a hydration mismatch — server and client would - // pick different products) — so the first paint always shows the fixed - // RELATED_PRODUCT_IDS fallback, and a client-only effect swaps in the - // real, cart-aware random pick right after mount. - const [displayIds, setDisplayIds] = useState(RELATED_PRODUCT_IDS); + // Starts empty — the catalog itself is now fetched (useProducts()), so + // there's nothing to pick a random set from until that resolves. The + // effect below picks as soon as products arrive, same idea as + // CartContent's productsLoading guard. + const [displayIds, setDisplayIds] = useState([]); - // First run (mount/hydration): pick the initial random set. After that, - // don't reshuffle everything on every cart change — one of these cards' - // own AddToCartInlineButton adds to the cart without navigating away, - // and a full reshuffle would yank the other two cards out from under - // the user mid-browse. Instead, only swap out whichever displayed card - // just became "already in the cart" (no longer relevant to recommend) - // and top that one slot back up — the rest stay exactly where they were. + // First run once products have loaded: pick the initial random set. + // After that, don't reshuffle everything on every cart change — one of + // these cards' own AddToCartInlineButton adds to the cart without + // navigating away, and a full reshuffle would yank the other two cards + // out from under the user mid-browse. Instead, only swap out whichever + // displayed card just became "already in the cart" (no longer relevant + // to recommend) and top that one slot back up — the rest stay exactly + // where they were. const pickedRef = useRef(false); const swapTimeoutRef = useRef | undefined>(undefined); useEffect(() => () => clearTimeout(swapTimeoutRef.current), []); useEffect(() => { + if (productIds.length === 0) return; const cartIds = cartKey ? cartKey.split(",") : []; if (!pickedRef.current) { pickedRef.current = true; - setDisplayIds(pickRandom(cartIds, DISPLAY_COUNT)); + setDisplayIds(pickRandom(productIds, cartIds, DISPLAY_COUNT)); return; } @@ -63,26 +70,30 @@ export function RelatedProducts() { const missing = DISPLAY_COUNT - stillRelevant.length; if (missing <= 0) return stillRelevant; - let replacements = pickRandom([...cartIds, ...stillRelevant], missing); + let replacements = pickRandom(productIds, [...cartIds, ...stillRelevant], missing); - // The catalog only has 5 products — once the cart holds 3+ - // distinct ones, "3 recommendations that aren't already in the - // cart" becomes mathematically impossible (5 - 3 in cart leaves - // only 2 to show). Falling back to re-suggesting something - // already in the cart (a normal "grab another one" pattern) beats - // silently shrinking the grid below DISPLAY_COUNT. + // The catalog only has a handful of products — once the cart + // holds enough distinct ones, "N recommendations that aren't + // already in the cart" can become impossible. Falling back to + // re-suggesting something already in the cart (a normal "grab + // another one" pattern) beats silently shrinking the grid below + // DISPLAY_COUNT. if (replacements.length < missing) { const stillMissing = missing - replacements.length; - const fallback = pickRandom([...stillRelevant, ...replacements], stillMissing); + const fallback = pickRandom(productIds, [...stillRelevant, ...replacements], stillMissing); replacements = [...replacements, ...fallback]; } return [...stillRelevant, ...replacements]; }); }, FEEDBACK_MS); - }, [cartKey]); + }, [cartKey, productIds]); - if (displayIds.length === 0) return null; + const displayProducts = displayIds + .map((id) => products.find((p) => p.id === id)) + .filter((p): p is NonNullable => Boolean(p)); + + if (displayProducts.length === 0) return null; return (
@@ -114,35 +125,32 @@ export function RelatedProducts() { scroll-reveal nicety on a list that mutates; a static grid renders correctly with no animation risk. */}
- {displayIds.map((id) => { - const product = PRODUCTS[id]; - return ( -
-
- {product.name} -
-
-

- {product.name} -

-

{formatPrice(product.price)}

- -
+ {displayProducts.map((product) => ( +
+
+ {product.name}
- ); - })} +
+

+ {product.name} +

+

{formatPrice(product.price)}

+ +
+
+ ))}
); diff --git a/app/components/AddToCartButton.tsx b/app/components/AddToCartButton.tsx index 8bcb1f9..6e986d6 100644 --- a/app/components/AddToCartButton.tsx +++ b/app/components/AddToCartButton.tsx @@ -54,7 +54,22 @@ export function AddToCartButton({ onClick={handleClick} className={`${base} ${stateClasses}`} > - {added ? "Hinzugefügt ✓" : label} + {/* CSS-grid text-stack, not just swapping the button's text node + directly — this button is inline-flex/content-sized (no w-full), + so "Hinzugefügt ✓" being shorter than most labels made the whole + button visibly shrink while showing the success state. Stacking + both possible texts in the same grid cell (both invisible ones + still contribute to sizing) reserves width for whichever is + wider, so the button's box never changes size either way. */} + + + + {added ? "Hinzugefügt ✓" : label} + ); } diff --git a/app/components/AddToCartInlineButton.tsx b/app/components/AddToCartInlineButton.tsx index 3ffa63d..d1850fb 100644 --- a/app/components/AddToCartInlineButton.tsx +++ b/app/components/AddToCartInlineButton.tsx @@ -48,7 +48,7 @@ export function AddToCartInlineButton({ // border-border/border-success both being present would silently race on // CSS source order instead of one cleanly winning. const stateClasses = added - ? "border-success bg-success-subtle scale-[1.02]" + ? "border-success bg-success-subtle" : "border-border hover:border-brand"; return ( diff --git a/app/components/ProductSpotlight.tsx b/app/components/ProductSpotlight.tsx index 4a8bc8f..de48d31 100644 --- a/app/components/ProductSpotlight.tsx +++ b/app/components/ProductSpotlight.tsx @@ -2,6 +2,8 @@ import Image from "next/image"; import Link from "next/link"; import { AddToCartButton } from "./AddToCartButton"; import { Reveal } from "./Reveal"; +import { getProductBySlug } from "../lib/payload"; +import { formatPrice } from "../lib/format"; /** * Product teaser for ToDo-Karten, placed after the Werkzeuge section (not @@ -13,15 +15,22 @@ import { Reveal } from "./Reveal"; * than dropped at the very top before the page has earned any trust. * Not derived from a Figma frame (page-home never had this section) — * a deliberate, code-only addition, styled to match the /todo-cards - * pricing panel it's a teaser for. + * pricing panel it's a teaser for. Price/photo come from Payload (same + * "todo-karten" product the shop/cart use) rather than being duplicated + * here as a hardcoded literal, so they can never silently drift apart — + * the marketing headline/copy below stays hand-written, since it's + * deliberately punchier than the plain catalog description. */ -export function ProductSpotlight() { +export async function ProductSpotlight() { + const product = await getProductBySlug("todo-karten"); + if (!product) return null; + return (
ToDo-Karten Set
-

12,90 €

+

{formatPrice(product.price)}

inkl. MwSt. zzgl. Versand

diff --git a/app/components/TrustRow.tsx b/app/components/TrustRow.tsx index f00a68a..40503ac 100644 --- a/app/components/TrustRow.tsx +++ b/app/components/TrustRow.tsx @@ -1,5 +1,5 @@ import { FREE_SHIPPING_THRESHOLD, TOTAL_DAYS_DE } from "../lib/shipping"; -import { formatPrice } from "../lib/products"; +import { formatPrice } from "../lib/format"; const items = [ { diff --git a/app/lib/format.ts b/app/lib/format.ts new file mode 100644 index 0000000..23a7928 --- /dev/null +++ b/app/lib/format.ts @@ -0,0 +1,11 @@ +// Plain utility, no client-only behavior — kept out of lib/products.ts +// (which is "use client" for its useProducts() hook) specifically so +// Server Components can still call it directly. Any export from a +// "use client" module becomes a client-only reference as far as Next.js's +// RSC boundary is concerned, even a pure function with zero hooks — a +// Server Component importing formatPrice from products.ts fails at +// runtime with "Attempted to call formatPrice() from the server but +// formatPrice is on the client." +export function formatPrice(value: number): string { + return `${value.toFixed(2).replace(".", ",")} €`; +} diff --git a/app/lib/payload.ts b/app/lib/payload.ts index 9806120..4239e0c 100644 --- a/app/lib/payload.ts +++ b/app/lib/payload.ts @@ -55,3 +55,60 @@ export async function getBlogPosts(limit = 3): Promise { : null, })); } + +// Frontend-facing shape — `id` is Payload's `slug` field, not its numeric +// row id. Cart items are stored in localStorage keyed by this string (see +// lib/cart.ts), so slugs were chosen in the Products collection to match +// the ids the old hardcoded catalog used ("todo-karten" etc.) — switching +// to numeric ids here would silently orphan every existing shopper's cart. +export type Product = { + id: string; + name: string; + description: string; + price: number; + image: string; + href: string | null; +}; + +type PayloadProduct = { + id: number; + name: string; + slug: string; + description: string | null; + price: number; + image: { url: string } | number | null; + detailHref: string | null; +}; + +export async function getProducts(): Promise { + const params = new URLSearchParams({ + "where[tenant.slug][equals]": TENANT_SLUG, + sort: "sortOrder", + depth: "2", + limit: "100", + }); + + const res = await fetch(`${PAYLOAD_URL}/api/products?${params}`, { + next: { revalidate: 60 }, + }); + if (!res.ok) { + console.error(`getProducts: Payload returned ${res.status} ${res.statusText}`); + return []; + } + + const data: { docs?: PayloadProduct[] } = await res.json(); + const docs = Array.isArray(data.docs) ? data.docs : []; + return docs.map((product) => ({ + id: product.slug, + name: product.name, + description: product.description ?? "", + price: product.price, + image: typeof product.image === "object" && product.image ? product.image.url : "", + href: product.detailHref || null, + })); +} + +export async function getProductBySlug(slug: string): Promise { + const products = await getProducts(); + return products.find((p) => p.id === slug) ?? null; +} diff --git a/app/lib/products.ts b/app/lib/products.ts index 0dabb4e..d79066e 100644 --- a/app/lib/products.ts +++ b/app/lib/products.ts @@ -1,65 +1,49 @@ -// Small in-code product catalog — this project has no commerce backend, -// so cart items (stored in localStorage as {id, qty} pairs, see cart.ts) -// need somewhere to look up name/price/photo/description by id. Only -// "todo-karten" has a real detail page (/todo-cards) right now; the -// other four exist because they're shown in Figma's page-cart "Passt -// perfekt dazu" row and are addable to the cart from there, same as the -// real product — they just don't have their own detail pages built yet -// (matching the project's established pattern of cross-linking to -// not-yet-built routes rather than leaving buttons disconnected). -export type Product = { - id: string; - name: string; - description: string; - price: number; - image: string; - href?: string; -}; +"use client"; -export const PRODUCTS: Record = { - "todo-karten": { - id: "todo-karten", - name: "ToDo-Karten – Set", - description: "50 ToDo-Karten für mehr Fokus und Klarheit im Alltag.", - price: 12.9, - image: "/product-todo-karten.png", - href: "/todo-cards", - }, - "notizbuch-klarheit": { - id: "notizbuch-klarheit", - name: "Notizbuch – Klarheit", - description: "Dein Begleiter für Gedanken, Notizen und neue Perspektiven.", - price: 9.9, - image: "/product-notizbuch-klarheit.png", - }, - wochenplaner: { - id: "wochenplaner", - name: "Wochenplaner – Überblick", - description: "Behalte deine Woche im Blick und setze klare Prioritäten.", - price: 14.9, - image: "/product-wochenplaner.png", - }, - "notizbuch-fokus": { - id: "notizbuch-fokus", - name: "Notizbuch – Fokus", - description: "Für mehr Konzentration und einen klaren Kopf im Alltag.", - price: 9.9, - image: "/product-notizbuch-fokus.png", - }, - zielkarten: { - id: "zielkarten", - name: "Zielkarten – Set", - description: "Definiere deine Ziele und behalte sie fest im Blick.", - price: 11.9, - image: "/product-zielkarten.png", - }, -}; +import { useEffect, useState } from "react"; +import type { Product } from "./payload"; -export const RELATED_PRODUCT_IDS = ["wochenplaner", "notizbuch-fokus", "zielkarten"]; +export type { Product }; -// Shop overview grid order — mirrors page-shop-overview in Figma. -export const SHOP_PRODUCT_IDS = ["todo-karten", "wochenplaner", "notizbuch-fokus", "zielkarten"]; +// Products now live in Payload's "products" collection (tenant +// einfach-produktiv), not a hardcoded catalog — see lib/payload.ts's +// getProducts() for the server-side fetch. Client components (CartContent, +// RelatedProducts) can't call that directly the way a Server Component +// can, so this hook fetches the same-origin /api/products proxy instead, +// with a tiny module-level cache so /cart's two consumers (CartContent + +// RelatedProducts) share one request instead of firing it twice. +let cache: Product[] | null = null; +let inflight: Promise | null = null; -export function formatPrice(value: number): string { - return `${value.toFixed(2).replace(".", ",")} €`; +async function fetchProducts(): Promise { + if (cache) return cache; + if (!inflight) { + inflight = fetch("/api/products") + .then((res) => (res.ok ? res.json() : [])) + .then((data: Product[]) => { + cache = data; + return data; + }) + .catch(() => []); + } + return inflight; +} + +// Starts empty (SSR-safe — matches useCart()'s pattern of a safe default +// that fills in after a client-only effect, see lib/cart.ts) and updates +// once the fetch resolves. +export function useProducts(): Product[] { + const [products, setProducts] = useState(cache ?? []); + + useEffect(() => { + let cancelled = false; + fetchProducts().then((data) => { + if (!cancelled) setProducts(data); + }); + return () => { + cancelled = true; + }; + }, []); + + return products; } diff --git a/app/shop/components/ProductGrid.tsx b/app/shop/components/ProductGrid.tsx index bbcce05..1749bf4 100644 --- a/app/shop/components/ProductGrid.tsx +++ b/app/shop/components/ProductGrid.tsx @@ -1,61 +1,73 @@ import Link from "next/link"; import Image from "next/image"; -import { PRODUCTS, SHOP_PRODUCT_IDS, formatPrice } from "../../lib/products"; +import { getProducts } from "../../lib/payload"; +import { formatPrice } from "../../lib/format"; import { RevealGroup, RevealItem } from "../../components/Reveal"; import { AddToCartInlineButton } from "../../components/AddToCartInlineButton"; -export function ProductGrid() { +// Server Component — fetches straight from Payload (getProducts(), ISR +// cached 60s) rather than going through the client-side useProducts() +// hook /cart's components need; this grid doesn't react to cart state, so +// there's no reason to pay for a client fetch when a server one already +// gives faster first paint and no loading flash. +// "notizbuch-klarheit" is deliberately excluded from the shop grid — it +// has no card in Figma's page-shop-overview (only 4 products do), even +// though it's a real product in Payload. Still shown elsewhere as a +// cross-sell (RelatedProducts on /cart). +const SHOP_GRID_EXCLUDE_IDS = ["notizbuch-klarheit"]; + +export async function ProductGrid() { + const products = (await getProducts()).filter((p) => !SHOP_GRID_EXCLUDE_IDS.includes(p.id)); + return (
- {SHOP_PRODUCT_IDS.map((id) => { - const product = PRODUCTS[id]; - return ( - -
- {product.name} -
-
-

( + +

+ {product.name} +
+
+

+ {product.name} +

+

+ {formatPrice(product.price)} + inkl. MwSt. +

+ {product.href && ( + - {product.name} -

-

{formatPrice(product.price)}

- {product.href && ( - - Mehr erfahren - - - )} + Mehr erfahren + + + )} - {/* flex-1 spacer — pins every card's button to the same Y - regardless of the "Mehr erfahren" line only the - todo-karten card has (see figma-to-nextjs skill's - Tools.tsx equal-height lesson). */} -
+ {/* flex-1 spacer — pins every card's button to the same Y + regardless of the "Mehr erfahren" line only products with + a detail page have (see figma-to-nextjs skill's Tools.tsx + equal-height lesson). */} +
- -
- - ); - })} + +
+ + ))} - -

Alle Preise inkl. MwSt., zzgl. Versandkosten.

); } diff --git a/app/todo-cards/components/Pricing.tsx b/app/todo-cards/components/Pricing.tsx index f9fe32b..e2d5d97 100644 --- a/app/todo-cards/components/Pricing.tsx +++ b/app/todo-cards/components/Pricing.tsx @@ -2,6 +2,8 @@ import Image from "next/image"; import { AddToCartButton } from "../../components/AddToCartButton"; import { Reveal } from "../../components/Reveal"; import { TOTAL_DAYS_DE } from "../../lib/shipping"; +import { getProductBySlug } from "../../lib/payload"; +import { formatPrice } from "../../lib/format"; const bullets = [ "50 ToDo-Karten", @@ -11,7 +13,14 @@ const bullets = [ "Nachhaltig produziert in Deutschland", ]; -export function Pricing() { +// Price/photo come from Payload (same "todo-karten" product the shop/cart +// use), not a hardcoded literal — see ProductSpotlight.tsx for the same +// reasoning; the bullet list stays hand-written since it's spec detail, +// not something the Products collection models. +export async function Pricing() { + const product = await getProductBySlug("todo-karten"); + if (!product) return null; + return (
{/* Three flex children (photo, info, price/CTA) competing for space @@ -20,7 +29,7 @@ export function Pricing() {
ToDo-Karten Set
-

12,90 €

+

{formatPrice(product.price)}

inkl. MwSt. zzgl. Versand

diff --git a/app/versand/components/VersandSections.tsx b/app/versand/components/VersandSections.tsx index 699e7fe..f54df24 100644 --- a/app/versand/components/VersandSections.tsx +++ b/app/versand/components/VersandSections.tsx @@ -6,7 +6,7 @@ import { TRANSIT_DAYS_DE, TOTAL_DAYS_DE, } from "../../lib/shipping"; -import { formatPrice } from "../../lib/products"; +import { formatPrice } from "../../lib/format"; // Single source of truth for both the full /versand page and the cart's // quick-reference VersandModal — same section ids/titles/copy either way, diff --git a/public/product-notizbuch-fokus.png b/public/product-notizbuch-fokus.png deleted file mode 100644 index d882ef1..0000000 Binary files a/public/product-notizbuch-fokus.png and /dev/null differ diff --git a/public/product-notizbuch-klarheit.png b/public/product-notizbuch-klarheit.png deleted file mode 100644 index aa79e53..0000000 Binary files a/public/product-notizbuch-klarheit.png and /dev/null differ diff --git a/public/product-todo-karten.png b/public/product-todo-karten.png deleted file mode 100644 index dd538bc..0000000 Binary files a/public/product-todo-karten.png and /dev/null differ diff --git a/public/product-wochenplaner.png b/public/product-wochenplaner.png deleted file mode 100644 index 200cec6..0000000 Binary files a/public/product-wochenplaner.png and /dev/null differ diff --git a/public/product-zielkarten.png b/public/product-zielkarten.png deleted file mode 100644 index 0bd4332..0000000 Binary files a/public/product-zielkarten.png and /dev/null differ