Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 88 additions & 61 deletions apps/storefront/app/artworks/[slug]/passport/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Badge, buttonVariants, Container, Eyebrow, Heading, Text } from '@tms/ui';
import { ArrowRight, ShieldCheck } from 'lucide-react';
import { Badge, Container } from '@tms/ui';
import { ShieldCheck } from 'lucide-react';
import type { Metadata } from 'next';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { PassportShare } from '@/components/artwork/passport-share';
import { PillLink } from '@/components/site/pill-link';
import { dataProvider } from '@/lib/data';

interface Params {
Expand Down Expand Up @@ -35,9 +36,19 @@ export async function generateMetadata({ params }: Params): Promise<Metadata> {

function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div className="flex flex-col gap-1 border-t border-line py-3 first:border-t-0 first:pt-0 sm:flex-row sm:gap-3">
<dt className="w-40 shrink-0 text-muted">{label}</dt>
<dd className={mono ? 'font-mono text-ink' : 'text-ink-2'}>{value}</dd>
<div className="flex flex-col gap-1 border-t border-line py-3 first:border-t-0 first:pt-0 sm:flex-row sm:items-baseline sm:gap-6">
<dt className="w-40 shrink-0 font-display text-xs font-semibold uppercase tracking-[0.12em] text-muted">
{label}
</dt>
<dd
className={
mono
? 'font-mono text-sm tracking-wide text-ink'
: 'text-sm font-medium text-ink sm:text-base'
}
>
{value}
</dd>
</div>
);
}
Expand All @@ -48,46 +59,57 @@ export default async function ArtworkPassportPage({ params }: Params) {
if (!passport) notFound();

return (
<Container className="py-10">
<nav aria-label="Breadcrumb" className="text-xs uppercase tracking-[0.08em] text-muted">
<Link href="/artworks" className="rounded-sm hover:text-ink">
<Container className="py-10 sm:py-14">
<nav
aria-label="Breadcrumb"
className="font-display text-xs font-semibold uppercase tracking-[0.12em] text-muted"
>
<Link
href="/artworks"
className="rounded-sm outline-none hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
Artworks
</Link>
<span aria-hidden> / </span>
<Link href={`/artworks/${passport.artworkSlug}`} className="rounded-sm hover:text-ink">
<Link
href={`/artworks/${passport.artworkSlug}`}
className="rounded-sm outline-none hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
{passport.title}
</Link>
<span aria-hidden> / </span>
<span className="text-ink-2">Passport</span>
<span className="text-ink">Passport</span>
</nav>

<div className="mt-6 max-w-3xl">
<div className="flex items-center gap-2">
<ShieldCheck className="size-5 text-accent" aria-hidden />
<Eyebrow className="m-0">Artwork Passport</Eyebrow>
<header className="mt-8 max-w-3xl">
<div className="flex items-center gap-2 text-muted">
<ShieldCheck className="size-4" aria-hidden />
<p className="font-display text-xs font-semibold uppercase tracking-[0.2em]">
Artwork passport
</p>
</div>
<Heading as={1} size="display-lg" className="mt-2">
<h1 className="mt-3 font-display text-4xl font-bold uppercase leading-[0.95] tracking-tight text-ink sm:text-5xl">
{passport.title}
</Heading>
<Text size="lg" tone="secondary" className="mt-3">
</h1>
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
A record of authenticity and provenance for this artwork, issued by {passport.issuedBy}.
</Text>
</div>
</p>
</header>

<div className="mt-8 grid gap-8 lg:grid-cols-[1.4fr_1fr]">
<div className="mt-12 grid gap-12 lg:grid-cols-[1.4fr_1fr] lg:gap-16">
{/* Certificate */}
<section
aria-labelledby="certificate-title"
className="rounded-[var(--radius-lg)] border border-line bg-surface p-6"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<Heading id="certificate-title" as={2} size="md">
<section aria-labelledby="certificate-title">
<div className="flex flex-wrap items-end justify-between gap-3 border-b border-line pb-4">
<h2
id="certificate-title"
className="font-display text-sm font-bold uppercase tracking-wide text-ink"
>
Certificate of authenticity
</Heading>
</h2>
<Badge tone={passport.editionSize ? 'warning' : 'neutral'}>{passport.edition}</Badge>
</div>

<dl className="mt-5 text-sm">
<dl className="mt-2 text-sm">
<Field label="Version ID" value={passport.versionId} mono />
<Field label="Collection" value={passport.collection} />
<Field label="Edition" value={passport.edition} />
Expand All @@ -98,7 +120,7 @@ export default async function ArtworkPassportPage({ params }: Params) {
<Field label="Issued by" value={passport.issuedBy} />
</dl>

<p className="mt-5 rounded-[var(--radius-md)] border border-line bg-canvas-2 p-3 text-xs text-muted">
<p className="mt-6 text-xs leading-relaxed text-muted">
The version ID is derived from this artwork’s content and stays fixed for this release —
any change to the piece issues a new version. A per-piece serial ledger and a
tamper-evident record are server-authoritative once the catalogue backend lands
Expand All @@ -107,72 +129,77 @@ export default async function ArtworkPassportPage({ params }: Params) {
</section>

{/* Ownership + share */}
<aside className="space-y-6">
<section
aria-labelledby="ownership-title"
className="rounded-[var(--radius-lg)] border border-line bg-surface p-6"
>
<Heading id="ownership-title" as={2} size="md">
<aside className="space-y-10">
<section aria-labelledby="ownership-title">
<h2
id="ownership-title"
className="border-b border-line pb-4 font-display text-sm font-bold uppercase tracking-wide text-ink"
>
Ownership record
</Heading>
<Text tone="secondary" className="mt-2 text-sm">
</h2>
<p className="mt-4 text-sm leading-relaxed text-ink-2">
No owner is on record yet. Ownership is registered to the buyer and the serial is
assigned when a piece is purchased.
</Text>
</p>
<p className="mt-3 text-xs text-muted">
Placeholder — the ownership ledger is a backend feature (TMS-FBR-001). Nothing is
stored on this device.
</p>
</section>

<section
aria-labelledby="share-title"
className="rounded-[var(--radius-lg)] border border-line bg-surface p-6"
>
<Heading id="share-title" as={2} size="md">
<section aria-labelledby="share-title">
<h2
id="share-title"
className="border-b border-line pb-4 font-display text-sm font-bold uppercase tracking-wide text-ink"
>
Share this passport
</Heading>
<Text tone="secondary" className="mt-2 text-sm">
</h2>
<p className="mt-4 text-sm leading-relaxed text-ink-2">
Share the public authenticity link for {passport.title}.
</Text>
<div className="mt-3">
</p>
<div className="mt-4">
<PassportShare title={passport.title} />
</div>
</section>
</aside>
</div>

{/* Provenance timeline */}
<section aria-labelledby="provenance-title" className="mt-10 max-w-3xl">
<Heading id="provenance-title" as={2} size="md">
<section aria-labelledby="provenance-title" className="mt-16 max-w-3xl">
<h2
id="provenance-title"
className="border-b border-line pb-4 font-display text-sm font-bold uppercase tracking-wide text-ink"
>
Provenance
</Heading>
<ol className="mt-4 space-y-4 border-l border-line pl-6">
</h2>
<ol className="mt-6 space-y-6 border-l border-line pl-6">
{passport.provenance.map((event, index) => (
<li key={`${event.label}-${index}`} className="relative">
<span
aria-hidden
className="absolute -left-[1.6rem] top-1.5 size-2.5 rounded-full border border-line bg-accent"
className="absolute -left-[1.6rem] top-1.5 size-2.5 rounded-full bg-neutral-950"
/>
<div className="flex flex-wrap items-baseline justify-between gap-x-3">
<p className="font-medium text-ink">{event.label}</p>
<p className="text-xs text-muted">{event.date}</p>
<p className="font-display text-sm font-bold uppercase tracking-wide text-ink">
{event.label}
</p>
<p className="font-display text-xs font-semibold uppercase tracking-[0.12em] text-muted">
{event.date}
</p>
</div>
<p className="mt-0.5 text-sm text-ink-2">{event.detail}</p>
<p className="mt-1 text-sm leading-relaxed text-ink-2">{event.detail}</p>
</li>
))}
</ol>
</section>

<div className="mt-10 flex flex-wrap gap-3">
<Link href={`/artworks/${passport.artworkSlug}`} className={buttonVariants({ size: 'lg' })}>
Back to artwork
</Link>
<div className="mt-14 flex flex-wrap items-center gap-4">
<PillLink href={`/artworks/${passport.artworkSlug}`}>Back to artwork</PillLink>
<Link
href="/design-studio"
className={buttonVariants({ size: 'lg', variant: 'secondary' })}
className="text-xs font-medium uppercase tracking-[0.08em] text-muted underline-offset-4 outline-none hover:text-ink hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
Design with this artwork <ArrowRight className="size-4" aria-hidden />
Design with this artwork
</Link>
</div>
</Container>
Expand Down
9 changes: 6 additions & 3 deletions apps/storefront/components/artwork/passport-share.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import { Button } from '@tms/ui';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';

Expand Down Expand Up @@ -38,14 +37,18 @@ export function PassportShare({ title }: { title: string }) {

return (
<div>
<Button type="button" variant="secondary" onClick={handleShare}>
<button
type="button"
onClick={handleShare}
className="inline-flex h-11 items-center justify-center gap-2 rounded-full border border-line-2 bg-canvas px-5 text-xs font-semibold uppercase tracking-[0.08em] text-ink outline-none hover:bg-canvas-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
{copied ? (
<Check className="size-4" aria-hidden />
) : (
<Share2 className="size-4" aria-hidden />
)}
{copied ? 'Link copied' : 'Share passport'}
</Button>
</button>
<p role="status" aria-live="polite" className="mt-2 min-h-[1.25rem] text-xs text-muted">
{status}
</p>
Expand Down
47 changes: 27 additions & 20 deletions apps/storefront/components/cart/cart-drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client';

import { EmptyState, IconButton } from '@tms/ui';
import { IconButton } from '@tms/ui';
import { ShoppingBag, X } from 'lucide-react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { type MouseEvent, useCallback, useEffect, useRef } from 'react';
import { PillLink } from '@/components/site/pill-link';
import { CartLineList } from './cart-line-list';
import { CartSummary } from './cart-summary';
import { useCart } from './cart-provider';
Expand Down Expand Up @@ -69,9 +70,9 @@ export function CartDrawer() {
onClick={onDialogClick}
className="tms-slideover m-0 ml-auto h-dvh max-h-none w-[min(28rem,92vw)] max-w-none bg-canvas p-0 text-ink open:flex open:flex-col"
>
<div className="flex h-16 shrink-0 items-center justify-between border-b border-line px-5">
<h2 className="font-display text-sm font-semibold tracking-tight">
Your bag{count > 0 ? ` (${count})` : ''}
<div className="flex h-16 shrink-0 items-center justify-between border-b border-line bg-canvas/90 px-5 backdrop-blur">
<h2 className="font-display text-sm font-bold uppercase tracking-[0.12em] text-ink">
Your bag{count > 0 ? ` · ${count}` : ''}
</h2>
<IconButton
label="Close bag"
Expand All @@ -81,21 +82,27 @@ export function CartDrawer() {
</div>

{cart.lines.length === 0 ? (
<div className="flex flex-1 items-center justify-center p-6">
<EmptyState
icon={<ShoppingBag className="size-6" aria-hidden />}
title="Your bag is empty"
description="Explore the gallery or design your own piece in the studio."
action={
<Link
href="/design-studio"
onClick={closeCart}
className="inline-flex h-11 items-center justify-center rounded-md bg-accent px-5 text-sm font-medium text-on-accent outline-none hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
Open the Design Studio
</Link>
}
/>
<div className="flex flex-1 flex-col items-center justify-center gap-6 px-6 text-center">
<ShoppingBag className="size-8 text-ink" aria-hidden />
<div className="max-w-xs">
<p className="font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted">
Shopping bag
</p>
<h3 className="mt-3 font-display text-3xl font-bold uppercase leading-[0.95] tracking-tight text-ink">
Empty for now
</h3>
<p className="mt-4 text-sm leading-relaxed text-muted">
Explore the gallery or design your own piece in the studio.
</p>
</div>
<PillLink href="/design-studio">Design studio</PillLink>
<Link
href="/artworks"
onClick={closeCart}
className="text-xs font-medium uppercase tracking-[0.08em] text-muted underline-offset-4 outline-none hover:text-ink hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
Browse artworks
</Link>
</div>
) : (
<>
Expand All @@ -107,7 +114,7 @@ export function CartDrawer() {
<Link
href="/cart"
onClick={closeCart}
className="mt-3 block rounded-sm text-center text-sm text-muted underline-offset-2 outline-none hover:text-ink hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
className="mt-3 block rounded-sm text-center text-xs font-medium uppercase tracking-[0.08em] text-muted underline-offset-4 outline-none hover:text-ink hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
View full bag
</Link>
Expand Down
2 changes: 1 addition & 1 deletion apps/storefront/components/cart/cart-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function CartSummary({ onCheckout }: { onCheckout?: () => void }) {
onClick={onCheckout}
disabled={hasIssues}
aria-describedby={hasIssues ? `${inputId}-blocked` : undefined}
className="inline-flex h-12 w-full items-center justify-center rounded-md bg-accent text-sm font-medium text-on-accent outline-none hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:bg-disabled disabled:text-disabled-ink disabled:hover:brightness-100"
className="inline-flex h-12 w-full items-center justify-center rounded-full bg-neutral-950 text-sm font-semibold uppercase tracking-[0.08em] text-white outline-none hover:bg-neutral-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:bg-disabled disabled:text-disabled-ink disabled:hover:bg-disabled"
>
Checkout
</button>
Expand Down
Loading
Loading