From 49d269f6d25d2b432554da175b2cd54312411c02 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Wed, 13 May 2026 11:29:06 +0200 Subject: [PATCH] feat(ui): visual redesign of storefront Replace placeholder homepage with a Zava-branded design: - Add app/globals.css design system (CSS variables, light/dark modes, typography, buttons, product cards, header/footer) - Add app/layout.tsx with sticky header, nav, and 4-column footer - Rebuild app/page.tsx with hero, feature strip, and product grid - Add error/empty states for the products listing - Update /<description> metadata The previous code referenced Tailwind utility classes, but Tailwind is not installed in this repo (those classes were no-ops). This PR commits to hand-written CSS rather than introducing a new build dependency. No new dependencies, no new HTTP handlers, no new DB queries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/globals.css | 581 ++++++++++++++++++++++++++++++++++++++++++++++++ app/layout.tsx | 109 +++++++++ app/page.tsx | 131 ++++++++++- 3 files changed, 809 insertions(+), 12 deletions(-) create mode 100644 app/globals.css create mode 100644 app/layout.tsx diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..d198a43 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,581 @@ +/* Zava storefront — global styles */ + +:root { + --color-bg: #fafaf7; + --color-surface: #ffffff; + --color-surface-alt: #f4f1ea; + --color-text: #1a1a1a; + --color-text-muted: #5b5b5b; + --color-border: #e6e2d8; + --color-accent: #c2410c; /* zava terracotta */ + --color-accent-hover: #9a330a; + --color-accent-soft: #fde8d8; + --color-success: #15803d; + + --radius-sm: 6px; + --radius-md: 12px; + --radius-lg: 20px; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.08); + + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + --font-display: "Playfair Display", Georgia, "Times New Roman", serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace; + + --container-max: 1200px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #14130f; + --color-surface: #1c1b17; + --color-surface-alt: #23211b; + --color-text: #f5f2ea; + --color-text-muted: #a8a499; + --color-border: #2f2c25; + --color-accent: #f97316; + --color-accent-hover: #fb923c; + --color-accent-soft: #3a1d0a; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.6); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + color: var(--color-accent); + text-decoration: none; + transition: color 0.15s ease; +} +a:hover { + color: var(--color-accent-hover); +} + +img { + max-width: 100%; + display: block; +} + +.container { + width: 100%; + max-width: var(--container-max); + margin: 0 auto; + padding: 0 24px; +} + +/* ---------- Header ---------- */ + +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: rgba(250, 250, 247, 0.85); + backdrop-filter: saturate(180%) blur(14px); + -webkit-backdrop-filter: saturate(180%) blur(14px); + border-bottom: 1px solid var(--color-border); +} + +@media (prefers-color-scheme: dark) { + .site-header { + background: rgba(20, 19, 15, 0.85); + } +} + +.site-header__inner { + display: flex; + align-items: center; + justify-content: space-between; + height: 64px; + gap: 24px; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--color-text); +} +.brand:hover { + color: var(--color-text); +} + +.brand__mark { + width: 32px; + height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, var(--color-accent), #f59e0b); + display: grid; + place-items: center; + color: #fff; + font-family: var(--font-sans); + font-weight: 800; + font-size: 1rem; + box-shadow: var(--shadow-sm); +} + +.nav { + display: flex; + gap: 28px; + font-size: 0.95rem; +} +.nav a { + color: var(--color-text-muted); + font-weight: 500; +} +.nav a:hover { + color: var(--color-text); +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.icon-btn { + display: inline-grid; + place-items: center; + width: 40px; + height: 40px; + border-radius: 999px; + background: transparent; + border: 1px solid transparent; + color: var(--color-text); + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; +} +.icon-btn:hover { + background: var(--color-surface-alt); + border-color: var(--color-border); +} + +@media (max-width: 720px) { + .nav { + display: none; + } +} + +/* ---------- Hero ---------- */ + +.hero { + position: relative; + padding: 96px 0 80px; + overflow: hidden; + background: + radial-gradient(circle at 20% 20%, var(--color-accent-soft), transparent 55%), + radial-gradient(circle at 80% 60%, rgba(245, 158, 11, 0.18), transparent 60%), + var(--color-bg); +} + +.hero__inner { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 56px; + align-items: center; +} + +@media (max-width: 900px) { + .hero { + padding: 64px 0 56px; + } + .hero__inner { + grid-template-columns: 1fr; + gap: 32px; + } +} + +.hero__eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.8rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-accent); + background: var(--color-accent-soft); + padding: 6px 12px; + border-radius: 999px; + margin-bottom: 20px; +} + +.hero__title { + font-family: var(--font-display); + font-size: clamp(2.5rem, 5.5vw, 4rem); + line-height: 1.05; + letter-spacing: -0.02em; + margin: 0 0 20px; + font-weight: 700; +} + +.hero__title em { + font-style: italic; + color: var(--color-accent); +} + +.hero__subtitle { + font-size: 1.15rem; + color: var(--color-text-muted); + margin: 0 0 32px; + max-width: 56ch; +} + +.hero__cta { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 22px; + font-size: 0.95rem; + font-weight: 600; + border-radius: 999px; + border: 1px solid transparent; + cursor: pointer; + transition: transform 0.1s ease, background 0.15s ease, color 0.15s ease, + border-color 0.15s ease, box-shadow 0.15s ease; + text-decoration: none; +} +.btn:active { + transform: translateY(1px); +} + +.btn--primary { + background: var(--color-accent); + color: #fff; + box-shadow: var(--shadow-sm); +} +.btn--primary:hover { + background: var(--color-accent-hover); + color: #fff; + box-shadow: var(--shadow-md); +} + +.btn--ghost { + background: transparent; + color: var(--color-text); + border-color: var(--color-border); +} +.btn--ghost:hover { + background: var(--color-surface-alt); + color: var(--color-text); +} + +.hero__visual { + position: relative; + aspect-ratio: 4 / 5; + border-radius: var(--radius-lg); + background: + linear-gradient(135deg, #f59e0b 0%, var(--color-accent) 100%); + box-shadow: var(--shadow-lg); + overflow: hidden; +} +.hero__visual::after { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.35), transparent 50%), + radial-gradient(circle at 70% 80%, rgba(0, 0, 0, 0.25), transparent 60%); +} +.hero__visual-tag { + position: absolute; + bottom: 24px; + left: 24px; + right: 24px; + z-index: 1; + color: #fff; + font-family: var(--font-display); + font-size: 1.5rem; + line-height: 1.2; + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); +} + +/* ---------- Section ---------- */ + +.section { + padding: 72px 0; +} + +.section__head { + display: flex; + align-items: end; + justify-content: space-between; + gap: 24px; + margin-bottom: 36px; + flex-wrap: wrap; +} + +.section__title { + font-family: var(--font-display); + font-size: clamp(1.75rem, 3vw, 2.5rem); + font-weight: 700; + letter-spacing: -0.01em; + margin: 0; +} + +.section__subtitle { + color: var(--color-text-muted); + margin: 6px 0 0; + font-size: 1rem; +} + +.section__link { + font-weight: 600; + font-size: 0.95rem; +} + +/* ---------- Product grid ---------- */ + +.product-grid { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 24px; +} + +.product-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; + display: flex; + flex-direction: column; + transition: transform 0.18s ease, box-shadow 0.18s ease, + border-color 0.18s ease; +} +.product-card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-md); + border-color: transparent; +} + +.product-card__media { + aspect-ratio: 4 / 3; + background: var(--color-surface-alt); + position: relative; + overflow: hidden; + display: grid; + place-items: center; +} +.product-card__media::before { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 30% 30%, rgba(194, 65, 12, 0.18), transparent 55%), + radial-gradient(circle at 75% 70%, rgba(245, 158, 11, 0.18), transparent 60%); +} +.product-card__initial { + position: relative; + font-family: var(--font-display); + font-size: 3rem; + font-weight: 700; + color: var(--color-accent); + opacity: 0.55; +} + +.product-card__body { + padding: 18px 20px 20px; + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; +} + +.product-card__name { + margin: 0; + font-size: 1.05rem; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--color-text); +} + +.product-card__desc { + margin: 0; + color: var(--color-text-muted); + font-size: 0.9rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.product-card__footer { + margin-top: auto; + padding-top: 14px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.product-card__price { + font-family: var(--font-mono); + font-size: 1rem; + font-weight: 600; + color: var(--color-text); +} + +.product-card__add { + font-size: 0.85rem; + font-weight: 600; + color: var(--color-accent); + background: transparent; + border: 1px solid var(--color-border); + padding: 8px 14px; + border-radius: 999px; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} +.product-card__add:hover { + background: var(--color-accent); + color: #fff; + border-color: var(--color-accent); +} + +/* ---------- Empty state ---------- */ + +.empty-state { + text-align: center; + padding: 64px 24px; + background: var(--color-surface); + border: 1px dashed var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text-muted); +} + +/* ---------- Feature strip ---------- */ + +.features { + background: var(--color-surface-alt); + border-top: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); +} +.features__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 24px; + padding: 36px 0; +} +.feature { + display: flex; + gap: 14px; + align-items: flex-start; +} +.feature__icon { + flex: 0 0 auto; + width: 36px; + height: 36px; + display: grid; + place-items: center; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 10px; + color: var(--color-accent); + font-size: 1.1rem; +} +.feature__title { + font-weight: 600; + font-size: 0.95rem; + margin: 0 0 2px; +} +.feature__desc { + font-size: 0.85rem; + color: var(--color-text-muted); + margin: 0; +} + +/* ---------- Footer ---------- */ + +.site-footer { + border-top: 1px solid var(--color-border); + background: var(--color-surface); + padding: 48px 0 32px; + margin-top: 48px; +} + +.site-footer__inner { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr; + gap: 32px; + margin-bottom: 32px; +} +@media (max-width: 800px) { + .site-footer__inner { + grid-template-columns: 1fr 1fr; + } +} + +.site-footer h4 { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--color-text-muted); + margin: 0 0 12px; +} +.site-footer ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} +.site-footer a { + color: var(--color-text); +} +.site-footer a:hover { + color: var(--color-accent); +} + +.site-footer__tagline { + color: var(--color-text-muted); + margin: 8px 0 0; + font-size: 0.9rem; + max-width: 32ch; +} + +.site-footer__bottom { + border-top: 1px solid var(--color-border); + padding-top: 20px; + display: flex; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; + font-size: 0.85rem; + color: var(--color-text-muted); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..8d11b68 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,109 @@ +import "./globals.css"; +import type { ReactNode } from "react"; + +export const metadata = { + title: "Zava — Crafted goods for everyday rituals", + description: + "Zava is a curated storefront for thoughtfully designed home goods, apparel, and accessories.", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + <html lang="en"> + <body> + <header className="site-header"> + <div className="container site-header__inner"> + <a href="/" className="brand" aria-label="Zava home"> + <span className="brand__mark" aria-hidden="true">Z</span> + <span>Zava</span> + </a> + <nav className="nav" aria-label="Primary"> + <a href="/">Shop</a> + <a href="/">New arrivals</a> + <a href="/">Collections</a> + <a href="/">Journal</a> + <a href="/">About</a> + </nav> + <div className="header-actions"> + <button className="icon-btn" type="button" aria-label="Search"> + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" + stroke="currentColor" strokeWidth="2" strokeLinecap="round" + strokeLinejoin="round" aria-hidden="true"> + <circle cx="11" cy="11" r="7" /> + <path d="m20 20-3.5-3.5" /> + </svg> + </button> + <button className="icon-btn" type="button" aria-label="Account"> + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" + stroke="currentColor" strokeWidth="2" strokeLinecap="round" + strokeLinejoin="round" aria-hidden="true"> + <circle cx="12" cy="8" r="4" /> + <path d="M4 21a8 8 0 0 1 16 0" /> + </svg> + </button> + <button className="icon-btn" type="button" aria-label="Cart"> + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" + stroke="currentColor" strokeWidth="2" strokeLinecap="round" + strokeLinejoin="round" aria-hidden="true"> + <path d="M5 7h14l-1.5 11a2 2 0 0 1-2 1.7H8.5a2 2 0 0 1-2-1.7L5 7Z" /> + <path d="M9 7V5a3 3 0 0 1 6 0v2" /> + </svg> + </button> + </div> + </div> + </header> + + {children} + + <footer className="site-footer"> + <div className="container"> + <div className="site-footer__inner"> + <div> + <a href="/" className="brand" aria-label="Zava home"> + <span className="brand__mark" aria-hidden="true">Z</span> + <span>Zava</span> + </a> + <p className="site-footer__tagline"> + Thoughtfully designed goods for the rituals that make a house + a home. + </p> + </div> + <div> + <h4>Shop</h4> + <ul> + <li><a href="/">New arrivals</a></li> + <li><a href="/">Best sellers</a></li> + <li><a href="/">Collections</a></li> + <li><a href="/">Gift cards</a></li> + </ul> + </div> + <div> + <h4>Company</h4> + <ul> + <li><a href="/">About</a></li> + <li><a href="/">Journal</a></li> + <li><a href="/">Sustainability</a></li> + <li><a href="/">Careers</a></li> + </ul> + </div> + <div> + <h4>Help</h4> + <ul> + <li><a href="/">Shipping & returns</a></li> + <li><a href="/">Contact</a></li> + <li><a href="/">FAQ</a></li> + </ul> + </div> + </div> + <div className="site-footer__bottom"> + <span>© {new Date().getFullYear()} Zava. All rights reserved.</span> + <span> + <a href="/">Privacy</a> · <a href="/">Terms</a> + </span> + </div> + </div> + </footer> + </body> + </html> + ); +} diff --git a/app/page.tsx b/app/page.tsx index 2ba07c4..d132e04 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,19 +1,126 @@ import { db } from "@/lib/db"; export default async function HomePage() { - const products = await db.listProducts({ limit: 12 }); + let products: Awaited<ReturnType<typeof db.listProducts>> = []; + let dbError = false; + try { + products = await db.listProducts({ limit: 12 }); + } catch { + dbError = true; + } + return ( - <main className="container mx-auto p-8"> - <h1 className="text-3xl font-bold mb-6">Zava — Featured products</h1> - <ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> - {products.map((p) => ( - <li key={p.id} className="border rounded p-4"> - <h2 className="font-semibold">{p.name}</h2> - <p className="text-sm text-gray-600">{p.description}</p> - <p className="mt-2 font-mono">${(p.priceCents / 100).toFixed(2)}</p> - </li> - ))} - </ul> + <main> + <section className="hero"> + <div className="container hero__inner"> + <div> + <span className="hero__eyebrow">New season · Spring 2026</span> + <h1 className="hero__title"> + Crafted goods for <em>everyday</em> rituals. + </h1> + <p className="hero__subtitle"> + Discover small-batch homeware, apparel, and accessories made by + independent makers — designed to be used, loved, and passed on. + </p> + <div className="hero__cta"> + <a href="#featured" className="btn btn--primary"> + Shop the collection + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" + stroke="currentColor" strokeWidth="2" strokeLinecap="round" + strokeLinejoin="round" aria-hidden="true"> + <path d="M5 12h14" /> + <path d="m13 5 7 7-7 7" /> + </svg> + </a> + <a href="/" className="btn btn--ghost">Our story</a> + </div> + </div> + <div className="hero__visual" aria-hidden="true"> + <div className="hero__visual-tag"> + Made slowly,<br />built to last. + </div> + </div> + </div> + </section> + + <section className="features" aria-label="Highlights"> + <div className="container features__grid"> + <div className="feature"> + <div className="feature__icon" aria-hidden="true">✦</div> + <div> + <p className="feature__title">Small-batch</p> + <p className="feature__desc">From makers we know by name.</p> + </div> + </div> + <div className="feature"> + <div className="feature__icon" aria-hidden="true">↺</div> + <div> + <p className="feature__title">Free 30-day returns</p> + <p className="feature__desc">Take your time deciding.</p> + </div> + </div> + <div className="feature"> + <div className="feature__icon" aria-hidden="true">⚑</div> + <div> + <p className="feature__title">Carbon-neutral shipping</p> + <p className="feature__desc">On every order, everywhere.</p> + </div> + </div> + <div className="feature"> + <div className="feature__icon" aria-hidden="true">♡</div> + <div> + <p className="feature__title">Lifetime repairs</p> + <p className="feature__desc">We fix what we sell.</p> + </div> + </div> + </div> + </section> + + <section id="featured" className="section"> + <div className="container"> + <div className="section__head"> + <div> + <h2 className="section__title">Featured products</h2> + <p className="section__subtitle"> + A handpicked selection from this season's collection. + </p> + </div> + <a href="/" className="section__link">View all →</a> + </div> + + {dbError ? ( + <div className="empty-state"> + We couldn't load products right now. Please check back shortly. + </div> + ) : products.length === 0 ? ( + <div className="empty-state">No products available yet.</div> + ) : ( + <ul className="product-grid"> + {products.map((p) => ( + <li key={p.id} className="product-card"> + <div className="product-card__media" aria-hidden="true"> + <span className="product-card__initial"> + {p.name?.charAt(0).toUpperCase() ?? "Z"} + </span> + </div> + <div className="product-card__body"> + <h3 className="product-card__name">{p.name}</h3> + <p className="product-card__desc">{p.description}</p> + <div className="product-card__footer"> + <span className="product-card__price"> + ${(p.priceCents / 100).toFixed(2)} + </span> + <button className="product-card__add" type="button"> + Add to cart + </button> + </div> + </div> + </li> + ))} + </ul> + )} + </div> + </section> </main> ); }