FieberPrints is a Berlin-based online store selling DIY punk shirts and linocut prints — and a portfolio piece engineered to demonstrate full-stack ownership. I built the entire thing: a Next.js frontend with a modular CSS design system, a Supabase backend handling auth, product data, and orders, a custom cart with checkout flow, and a CSS variable architecture strict enough that a single token change repaints the entire UI.
Every layer of the stack was chosen to serve the product, not the résumé.
| Framework | Next.js 15 Per-page CSS Modules with a shared globals layer. The App Router gives server components for the gallery and product pages, keeping client-side JS minimal — only the cart, auth flows, and interactive overlays run on the client. |
| Backend | Supabase Handles authentication (email/password + session persistence), the artworks table (product data, image URLs, sizes, pricing), and the orders table. Row-level security policies ensure users can only read their own order history. |
| Styling | Modular CSS + CSS Variables A strict token system: --color-background, --color-surface, --color-secondary (the pink), --font-display (Bebas Neue), --font-body (Space Grotesk). Every component imports from its own .module.css. No Tailwind, no CSS-in-JS — the constraint was intentional. |
| State | React Context + useState Cart state lives in a context provider wrapping the layout, so the nav badge, cart drawer, and checkout page all share one source of truth without an external store. Auth state is derived directly from Supabase's onAuthStateChange listener. |
| Payment Engine | Stripe Stripe payment management system. |
The constraint wasn't technical. It was visual — and it was harder.
The hardest constraint on this project wasn't the Supabase integration or the checkout flow — it was the design system. The aesthetic had strict, non-negotiable rules: dark backgrounds throughout, pink (#fd00d4) as punctuation only, zero border-radius everywhere, Bebas Neue for display text, Space Grotesk for body copy. The challenge was that CSS defaults constantly fight this — browsers apply their own button styles, input focus rings, link colours, and background defaults. Every component had to be explicitly overridden, and any slip meant a white card or a rounded corner breaking the identity.
The most recurring bug: var(--color-primary) was set to #ffffff (white text on dark backgrounds), but several components used it as a background colour — rendering white buttons with white text. Invisible. Caught late, fixed systematically across every button variant in the codebase.
I built the token system so that surface colours and text colours are explicitly separated —--color-surface for card backgrounds, --color-surface-alt for inputs, and --color-primary reserved strictly for text. Buttons always use--color-secondary (pink). This made the intent machine-readable: if something renders wrong, the token name tells you why.
The ticker animation required a specific architectural fix: a single-strip approach withtranslateX(-50%) produced a blank gap mid-loop whenever content width didn't divide evenly into the viewport. The solution was two identical strips sitting side-by-side in a flex row, each animating at translateX(-100%) — the second strip is always immediately adjacent to the first, so there's no gap possible, regardless of content length.
/* Two strips. Each moves its own full width left. No gap. */
.ticker {
display: flex; /* strips sit side by side */
overflow: hidden;
width: 100%;
}
.tickerInner {
min-width: 100%; /* always fills viewport */
animation: tickerScroll 22s linear infinite;
}
@keyframes tickerScroll {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}Hindsight is the best code reviewer.
The modular CSS approach was the right call for maintainability, but it created a coordination problem: when the homepage imports from both page.module.css andGallery.module.css, a class name collision between the two files causes silent, hard-to-trace layout bugs. Next time I'd enforce a stricter naming convention (BEM-style prefixes per component) from day one rather than retrofitting it during the design system pass.
I'd also move cart state to Zustand earlier. React Context works for simple cart logic, but as the checkout flow grew to include address modals, order history, and profile management, the context value re-rendered more subtly dependent components than necessary. A Zustand store with derived selectors would have kept that contained without much added complexity.
Finally: image handling. The product photos have inconsistent backgrounds — white, yellow, blue — because they're real photographs of real shirts. The fix was an aspect-ratio lock (aspect-ratio: 3/4, object-fit: cover) combined with a 1px inset box-shadow that frames each photo against the dark site background, making the inconsistency read as intentional rather than accidental. It works, but a consistent photography brief from the start would have been cleaner.