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
6 changes: 0 additions & 6 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
# Service role key is only used server-side (API routes / webhooks)
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key

# ─── Cloudinary ──────────────────────────────────────────────────────────────
# Found in: Cloudinary Dashboard → Settings → API Keys
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret

# ─── Stripe ─────────────────────────────────────────────────────────────────
# Found in: Stripe Dashboard → Developers → API keys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
branches:
- '**'

jobs:
biome:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: latest

- name: Run Biome
run: biome check
6 changes: 5 additions & 1 deletion app/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,11 @@ export default async function ArtworkDetailPage(props: { params: Promise<{ slug:
{details.map(({ label, value, accent, bold }) => (
<div key={label} className="flex items-center justify-between gap-4">
<span className="font-sans text-[13px] text-text-tertiary">{label}</span>
<span className={`text-right font-sans text-[13px] ${bold ? "font-semibold" : ""} ${accent ? "text-accent" : "text-text-primary"}`}>{value}</span>
<span
className={`text-right font-sans text-[13px] ${bold ? "font-semibold" : ""} ${accent ? "text-accent" : "text-text-primary"}`}
>
{value}
</span>
</div>
))}
{collection && (
Expand Down
5 changes: 2 additions & 3 deletions app/api/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export async function POST(request: Request) {
emailItems = [{ title: artwork.title, price: artwork.price, type: "print", quantity }];
}

// Case 2: cart checkout (metadata.items JSON)
// Case 2: cart checkout (metadata.items JSON)
} else if (meta.items) {
const cartItems: { id: string; qty: number; type: "original" | "print" }[] = JSON.parse(meta.items);
const ids = cartItems.map(i => i.id);
Expand All @@ -156,7 +156,7 @@ export async function POST(request: Request) {
quantity: item.qty,
}));

// Case 3: original artwork direct purchase (existing metadata.artworkIds)
// Case 3: original artwork direct purchase (existing metadata.artworkIds)
} else if (meta.artworkIds) {
const artworkIds = meta.artworkIds.split(",").filter(Boolean);

Expand Down Expand Up @@ -197,7 +197,6 @@ export async function POST(request: Request) {
type: "original" as const,
quantity: 1,
}));

} else {
console.error("Webhook: unrecognised metadata", { sessionId: session.id, meta });
return Response.json({ error: "Missing metadata" }, { status: 400 });
Expand Down
34 changes: 13 additions & 21 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
@import "tailwindcss";

@variant dark
(
&:where(.dark, .dark *));

@theme {
--color-bg-main: #FAF8F5;
--color-bg-warm: #F0ECE6;
--color-bg-deep: #1A1917;
--color-bg-stone: #E8E3DB;
--color-accent: #260614;
--color-divider: #DDD8D0;
--color-divider-dark: #2A2926;
--color-text-primary: #1A1917;
--color-text-secondary: #6B6660;
--color-text-tertiary: #6E6A68;
--color-text-light: #FAF8F5;
--color-text-light-muted: #A8A29C;
--color-bg-main: #faf8f5;
--color-bg-warm: #f0ece6;
--color-bg-deep: #1a1917;
--color-bg-stone: #e8e3db;
--color-accent: #260614;
--color-divider: #ddd8d0;
--color-divider-dark: #2a2926;
--color-text-primary: #1a1917;
--color-text-secondary: #6b6660;
--color-text-tertiary: #6e6a68;
--color-text-light: #faf8f5;
--color-text-light-muted: #a8a29c;

--font-sans: "Funnel Sans", ui-sans-serif, system-ui, sans-serif;

Expand All @@ -33,7 +29,6 @@
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

}

body {
Expand All @@ -58,8 +53,5 @@ button {
}

@utility input-line {
@apply w-full border-0 border-b border-divider bg-transparent
pb-3 pt-1 text-text-primary placeholder-text-tertiary
text-sm font-sans outline-hidden transition-colors
focus:border-text-secondary resize-none;
@apply w-full border-0 border-b border-divider bg-transparent pb-3 pt-1 text-text-primary placeholder-text-tertiary text-sm font-sans outline-hidden transition-colors focus:border-text-secondary resize-none;
}
35 changes: 9 additions & 26 deletions drizzle/migrations/meta/20260403115657_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,8 @@
"tableFrom": "artworks",
"tableTo": "collections",
"schemaTo": "public",
"columnsFrom": [
"collection_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["collection_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
Expand All @@ -225,9 +221,7 @@
"name": "artworks: public read",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"public"
],
"to": ["public"],
"using": "true"
}
},
Expand Down Expand Up @@ -391,23 +385,16 @@
"tableFrom": "orders",
"tableTo": "artworks",
"schemaTo": "public",
"columnsFrom": [
"artwork_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["artwork_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"orders_session_artwork_unique": {
"columns": [
"artwork_id",
"stripe_session_id"
],
"columns": ["artwork_id", "stripe_session_id"],
"nullsNotDistinct": false,
"name": "orders_session_artwork_unique"
}
Expand Down Expand Up @@ -520,9 +507,7 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {
"collections_slug_key": {
"columns": [
"slug"
],
"columns": ["slug"],
"nullsNotDistinct": false,
"name": "collections_slug_key"
}
Expand All @@ -533,9 +518,7 @@
"name": "Public read",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"public"
],
"to": ["public"],
"using": "true"
}
},
Expand Down Expand Up @@ -566,4 +549,4 @@
}
}
}
}
}
2 changes: 1 addition & 1 deletion drizzle/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
"breakpoints": true
}
]
}
}
7 changes: 4 additions & 3 deletions features/artwork/artwork-info-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ const returnsItem = {

export default function ArtworkInfoSection({ type }: { type?: string | null }) {
const typeItem = type === "print" ? printItem : originalItem;
const items = type === "print"
? [...sharedItems, typeItem, signedEditionItem, returnsItem]
: [...sharedItems, typeItem, returnsItem];
const items =
type === "print"
? [...sharedItems, typeItem, signedEditionItem, returnsItem]
: [...sharedItems, typeItem, returnsItem];

return (
<div className="flex flex-col gap-7">
Expand Down
2 changes: 1 addition & 1 deletion features/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"use client";
import Link from "next/link";

interface BreadcrumbItem {
label: string;
href?: string;
}

export default function Breadcrumb({ items }: { items: BreadcrumbItem[] }) {

function handleClick(href: string) {
const hashMatch = href.match(/^\/#(.+)$/);
if (hashMatch) {
Expand Down
4 changes: 3 additions & 1 deletion features/cart/cart-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ export default function CartDrawer() {
{item.type === "print" && item.quantity > 1 && (
<p className="font-sans text-[12px] text-text-tertiary">Qty: {item.quantity}</p>
)}
<p className="font-medium font-sans text-[15px] text-text-primary">{formatPrice(item.price * item.quantity)}</p>
<p className="font-medium font-sans text-[15px] text-text-primary">
{formatPrice(item.price * item.quantity)}
</p>
</div>
<button
onClick={() => removeItem(item.id)}
Expand Down
4 changes: 1 addition & 3 deletions features/cart/cart-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ export default function CartProvider({ children }: { children: React.ReactNode }
if (item.type === "print") {
const existing = prev.find(i => i.id === item.id);
if (existing) {
return prev.map(i =>
i.id === item.id ? { ...i, quantity: Math.min(10, i.quantity + item.quantity) } : i,
);
return prev.map(i => (i.id === item.id ? { ...i, quantity: Math.min(10, i.quantity + item.quantity) } : i));
}
return [...prev, item];
}
Expand Down
4 changes: 2 additions & 2 deletions features/collection/collection-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export async function getCollections() {
return Promise.all(
collections.map(async collection => ({
...collection,
coverImageUrl: collection.coverImageUrl ? (await resolveImages(collection.coverImageUrl))[0] ?? null : null,
heroImage: collection.heroImage ? (await resolveImages(collection.heroImage))[0] ?? null : null,
coverImageUrl: collection.coverImageUrl ? ((await resolveImages(collection.coverImageUrl))[0] ?? null) : null,
heroImage: collection.heroImage ? ((await resolveImages(collection.heroImage))[0] ?? null) : null,
})),
);
}
27 changes: 12 additions & 15 deletions features/collection/featured-collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,25 @@ export default async function FeaturedCollection() {
const collections = await getCollections();
const collection = collections[0];

if (!collection) return null;
if (!collection) {
return null;
}

const image = collection.heroImage || collection.coverImageUrl;

return (
<section id="collections" className="relative w-full overflow-hidden" style={{ height: "clamp(480px, 75vh, 800px)" }}>
{image && (
<Image
src={image}
alt={collection.name}
fill
className="object-cover"
sizes="100vw"
quality={75}
/>
)}
<section
id="collections"
className="relative w-full overflow-hidden"
style={{ height: "clamp(480px, 75vh, 800px)" }}
>
{image && <Image src={image} alt={collection.name} fill className="object-cover" sizes="100vw" quality={75} />}

<div className="absolute inset-0 bg-gradient-to-r from-bg-deep/85 via-bg-deep/50 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-bg-deep/40 via-transparent to-transparent" />

<div className="absolute inset-0 flex items-end pb-12 px-5 md:items-center md:pb-0 md:px-12">
<div className="flex flex-col gap-5 max-w-xl">
<div className="absolute inset-0 flex items-end px-5 pb-12 md:items-center md:px-12 md:pb-0">
<div className="flex max-w-xl flex-col gap-5">
<span className="font-sans text-[11px] uppercase tracking-[2.5px]" style={{ color: "rgba(255,255,255,0.5)" }}>
Collection
</span>
Expand Down Expand Up @@ -59,7 +56,7 @@ export default async function FeaturedCollection() {

<Link
href={`/collections/${collection.slug}`}
className="mt-1 w-fit font-normal font-sans text-[13px] tracking-[0.5px] border px-5 py-2.5 transition-all hover:bg-white/10"
className="mt-1 w-fit border px-5 py-2.5 font-normal font-sans text-[13px] tracking-[0.5px] transition-all hover:bg-white/10"
style={{ color: "rgba(255,255,255,0.85)", borderColor: "rgba(255,255,255,0.25)" }}
>
Explore collection →
Expand Down
11 changes: 6 additions & 5 deletions features/email/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ export async function sendOrderConfirmation({

const hasOriginals = items.some(i => i.type === "original");
const hasPrints = items.some(i => i.type === "print");
const followUpCopy = hasOriginals && !hasPrints
? "I will be in touch shortly to arrange packaging and shipping for your original painting."
: hasPrints && !hasOriginals
? "Your print will be carefully packed and shipped within 2–5 business days. I'll be in touch within 48 hours with your shipping confirmation."
: "I'll now prepare your pieces with care and will be in touch shortly to confirm the shipping details.";
const followUpCopy =
hasOriginals && !hasPrints
? "I will be in touch shortly to arrange packaging and shipping for your original painting."
: hasPrints && !hasOriginals
? "Your print will be carefully packed and shipped within 2–5 business days. I'll be in touch within 48 hours with your shipping confirmation."
: "I'll now prepare your pieces with care and will be in touch shortly to confirm the shipping details.";

const itemRows = items
.map(
Expand Down
7 changes: 6 additions & 1 deletion features/legal-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export default function LegalPage({ content, metaTitleDe, metaTitleEn }) {

<div className="flex min-h-screen flex-col bg-bg-main">
<nav className="flex items-center gap-2 px-5 py-4 md:px-12">
<Link href="/" className="font-sans text-[12px] text-text-tertiary transition-colors hover:text-text-secondary">Home</Link>
<Link
href="/"
className="font-sans text-[12px] text-text-tertiary transition-colors hover:text-text-secondary"
>
Home
</Link>
<span className="font-sans text-[12px] text-text-tertiary">/</span>
<span className="font-sans text-[12px] text-text-secondary">{lang === "de" ? metaTitleDe : metaTitleEn}</span>
</nav>
Expand Down
10 changes: 8 additions & 2 deletions features/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,20 @@ export default function Navigation() {
{links.map(({ label, sectionId }) => (
<button
key={label}
onClick={() => { handleNavClick(sectionId); setMobileOpen(false); }}
onClick={() => {
handleNavClick(sectionId);
setMobileOpen(false);
}}
className="text-left font-normal font-sans text-[15px] text-text-secondary tracking-[0.5px] transition-colors hover:text-text-primary"
>
{label}
</button>
))}
<button
onClick={() => { handleNavClick("contact"); setMobileOpen(false); }}
onClick={() => {
handleNavClick("contact");
setMobileOpen(false);
}}
className="text-left font-normal font-sans text-[15px] text-accent tracking-[0.5px] transition-opacity hover:opacity-80"
>
Inquire
Expand Down
4 changes: 3 additions & 1 deletion features/prints/prints-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default async function PrintsSection() {
<div className="flex flex-col gap-8 px-5 py-15 pb-20 md:gap-10 md:px-12">
<div className="flex flex-col gap-1.5">
<div className="flex w-full items-center justify-between">
<span className="font-normal font-sans text-[16px] text-text-tertiary uppercase tracking-wide3">Limited Prints</span>
<span className="font-normal font-sans text-[16px] text-text-tertiary uppercase tracking-wide3">
Limited Prints
</span>
<span className="font-normal font-sans text-[13px] text-text-tertiary tracking-[0.5px]">
{prints.length} print{prints.length !== 1 ? "s" : ""}
</span>
Expand Down
Loading
Loading