Improve Directus integration and homepage dynamic events

- Add auth token to asset URLs for proper image access
- Map Directus artwork fields (name->title, price_gbp/usd->price, notes->description)
- Add currency field (GBP/USD) based on available price
- Fetch upcoming events dynamically on homepage
- Prefer artworks with images for featured display
- Add proper error handling for artwork lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-04 22:02:17 +00:00
parent 5a477e31a2
commit 0e13f60d14
7 changed files with 130 additions and 89 deletions

View File

@ -231,7 +231,7 @@ export default function CheckoutPage() {
{item.artwork.medium && (
<p className="text-xs text-gray-500 mt-1">{item.artwork.medium}</p>
)}
<p className="text-sm font-medium mt-2">£{item.price.toLocaleString()}</p>
<p className="text-sm font-medium mt-2">{item.artwork.currency === 'GBP' ? '£' : '$'}{item.price.toLocaleString()}</p>
<button
onClick={() => removeItem(item.id)}
className="text-xs text-gray-500 underline mt-2 hover:no-underline"
@ -246,7 +246,7 @@ export default function CheckoutPage() {
<div className="border-t border-gray-200 mt-6 pt-6 space-y-2">
<div className="flex justify-between text-sm">
<span>Subtotal</span>
<span>£{subtotal.toLocaleString()}</span>
<span>${subtotal.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm text-gray-500">
<span>Shipping</span>
@ -257,7 +257,7 @@ export default function CheckoutPage() {
<div className="border-t border-gray-200 mt-6 pt-6">
<div className="flex justify-between text-lg font-medium">
<span>Total</span>
<span>£{subtotal.toLocaleString()}</span>
<span>${subtotal.toLocaleString()}</span>
</div>
<p className="mt-1 text-xs text-gray-500">
+ shipping (calculated at next step)

View File

@ -12,13 +12,9 @@ export const revalidate = 60;
async function getEventsList(): Promise<{ upcoming: Event[]; past: Event[] }> {
try {
const events = await getEvents({ status: 'published' });
const now = new Date();
const upcoming = events.filter(
(e) => !e.end_date || new Date(e.end_date) >= now
);
const past = events.filter((e) => e.end_date && new Date(e.end_date) < now);
const allEvents = await getEvents();
const upcoming = allEvents.filter((e) => e.status === 'published');
const past = allEvents.filter((e) => e.status === 'past');
return { upcoming, past };
} catch (error) {

View File

@ -1,6 +1,6 @@
import Link from 'next/link';
import Image from 'next/image';
import { getArtworks, Artwork } from '@/lib/directus';
import { getArtworks, getEvents, Artwork, Event } from '@/lib/directus';
import { ArtworkCard } from '@/components/artwork-card';
import { WisdomWordsCarousel } from '@/components/wisdom-words-carousel';
@ -8,16 +8,41 @@ export const revalidate = 60;
async function getFeaturedArtworks(): Promise<Artwork[]> {
try {
const artworks = await getArtworks({ status: 'published', limit: 6 });
return artworks;
// Fetch more than needed and prefer artworks with images
const artworks = await getArtworks({ status: 'published', limit: 50 });
const withImages = artworks.filter((a) => a.image);
// Return up to 6 artworks that have images, or fall back to whatever is available
return withImages.length >= 6 ? withImages.slice(0, 6) : artworks.slice(0, 6);
} catch (error) {
console.error('Error fetching artworks:', error);
return [];
}
}
async function getUpcomingEvents(): Promise<Event[]> {
try {
const events = await getEvents({ status: 'published', limit: 4 });
return events;
} catch (error) {
console.error('Error fetching events:', error);
return [];
}
}
function formatEventDate(start?: string, end?: string): string {
if (!start) return '';
const s = new Date(start);
const opts: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' };
if (!end) return s.toLocaleDateString('en-GB', opts);
const e = new Date(end);
return `${s.toLocaleDateString('en-GB', opts)} - ${e.toLocaleDateString('en-GB', opts)}`;
}
export default async function HomePage() {
const artworks = await getFeaturedArtworks();
const [artworks, upcomingEvents] = await Promise.all([
getFeaturedArtworks(),
getUpcomingEvents(),
]);
// Wisdom words gallery - all quote images from original site
const wisdomImages = [
@ -305,41 +330,27 @@ export default async function HomePage() {
</div>
<div className="space-y-8">
{/* Event 1 */}
<div className="border-b border-gray-200 pb-8">
<p className="text-sm text-gray-500 uppercase tracking-wider mb-2">Feb 2, 2026</p>
<h3 className="font-serif text-xl text-[#222]">Secret Screening</h3>
<p className="mt-2 text-gray-600">
An exclusive preview event for the creative community.
</p>
<Link href="/events" className="mt-3 inline-block text-sm text-[#222] underline underline-offset-4 hover:no-underline">
Learn More
</Link>
</div>
{/* Event 2 */}
<div className="border-b border-gray-200 pb-8">
<p className="text-sm text-gray-500 uppercase tracking-wider mb-2">Apr 4-11, 2026</p>
<h3 className="font-serif text-xl text-[#222]">UnEarthing Templer Way at Birdwood House</h3>
<p className="mt-2 text-gray-600">
A week-long creative retreat exploring art and nature.
</p>
<Link href="/events" className="mt-3 inline-block text-sm text-[#222] underline underline-offset-4 hover:no-underline">
Learn More
</Link>
</div>
{/* Event 3 */}
<div className="pb-8">
<p className="text-sm text-gray-500 uppercase tracking-wider mb-2">Apr 16-23, 2026</p>
<h3 className="font-serif text-xl text-[#222]">UNEARTHING TEMPLER WAY EXHIBITION</h3>
<p className="mt-2 text-gray-600">
Exhibition showcasing works created during the retreat.
{upcomingEvents.length > 0 ? (
upcomingEvents.map((event, index) => (
<div key={event.id} className={index < upcomingEvents.length - 1 ? 'border-b border-gray-200 pb-8' : 'pb-8'}>
<p className="text-sm text-gray-500 uppercase tracking-wider mb-2">
{formatEventDate(event.start_date, event.end_date)}
</p>
<h3 className="font-serif text-xl text-[#222]">{event.title}</h3>
{event.location && (
<p className="mt-1 text-sm text-gray-500">{event.location}</p>
)}
{event.description && (
<p className="mt-2 text-gray-600 line-clamp-2">{event.description}</p>
)}
<Link href="/events" className="mt-3 inline-block text-sm text-[#222] underline underline-offset-4 hover:no-underline">
Learn More
</Link>
</div>
))
) : (
<p className="text-center text-gray-500 py-8">No upcoming events at the moment.</p>
)}
</div>
<div className="mt-12 text-center">

View File

@ -164,7 +164,7 @@ export default function ArtworkDetailPage() {
{isSold ? (
<p className="text-xl font-medium text-gray-400">Sold</p>
) : artwork.price ? (
<p className="text-2xl font-medium">£{artwork.price.toLocaleString()}</p>
<p className="text-2xl font-medium">{artwork.currency === 'GBP' ? '£' : '$'}{artwork.price.toLocaleString()}</p>
) : (
<p className="text-lg text-gray-600">Price on request</p>
)}
@ -279,7 +279,7 @@ export default function ArtworkDetailPage() {
<div className="mt-4 text-center">
<h3 className="font-serif text-lg">{work.title}</h3>
{work.price && (
<p className="mt-1 text-sm">£{work.price.toLocaleString()}</p>
<p className="mt-1 text-sm">{work.currency === 'GBP' ? '£' : '$'}{work.price.toLocaleString()}</p>
)}
</div>
</Link>

View File

@ -85,7 +85,9 @@ export function ArtworkCard({
{isSold ? (
<p className="mt-2 text-sm font-medium text-gray-400">Sold</p>
) : artwork.price ? (
<p className="mt-2 text-sm font-medium">£{artwork.price.toLocaleString()}</p>
<p className="mt-2 text-sm font-medium">
{artwork.currency === 'GBP' ? '£' : '$'}{artwork.price.toLocaleString()}
</p>
) : (
<p className="mt-2 text-sm text-gray-500">Price on request</p>
)}

View File

@ -84,7 +84,7 @@ export function CartDrawer() {
)}
</div>
<p className="text-sm font-medium">
£{item.price.toLocaleString()}
{item.artwork.currency === 'GBP' ? '£' : '$'}{item.price.toLocaleString()}
</p>
</div>
@ -110,7 +110,7 @@ export function CartDrawer() {
<div className="border-t border-gray-200 px-6 py-4">
<div className="flex justify-between text-base font-medium">
<span>Subtotal</span>
<span>£{subtotal.toLocaleString()}</span>
<span>${subtotal.toLocaleString()}</span>
</div>
<p className="mt-1 text-xs text-gray-500">
Shipping and taxes calculated at checkout

View File

@ -11,6 +11,7 @@ export interface Artwork {
medium?: string;
dimensions?: string;
price?: number;
currency?: 'GBP' | 'USD';
description?: string;
image?: string | DirectusFile;
gallery?: string[];
@ -153,23 +154,45 @@ export const directus = createDirectus<any>(directusUrl)
.with(staticToken(apiToken))
.with(rest());
// Helper to get asset URL
// Helper to get asset URL (includes auth token since Directus assets require authentication)
export function getAssetUrl(fileId: string | DirectusFile | undefined, options?: { width?: number; height?: number; quality?: number; format?: 'webp' | 'jpg' | 'png' }): string {
if (!fileId) return '/placeholder.jpg';
const id = typeof fileId === 'string' ? fileId : fileId.id;
let url = `${directusUrl}/assets/${id}`;
const params = new URLSearchParams();
// Auth token required for asset access
if (apiToken) params.append('access_token', apiToken);
if (options) {
const params = new URLSearchParams();
if (options.width) params.append('width', options.width.toString());
if (options.height) params.append('height', options.height.toString());
if (options.quality) params.append('quality', options.quality.toString());
if (options.format) params.append('format', options.format);
if (params.toString()) url += `?${params.toString()}`;
}
return url;
return `${directusUrl}/assets/${id}?${params.toString()}`;
}
// Map Directus artwork fields to frontend Artwork interface
// Directus has: name, price_gbp, price_usd, notes, creation_date
// Frontend expects: title, price, description, slug, year
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function mapArtwork(raw: any): Artwork {
const gbp = parseFloat(raw.price_gbp);
const usd = parseFloat(raw.price_usd);
const hasGbp = !isNaN(gbp) && gbp > 0;
const hasUsd = !isNaN(usd) && usd > 0;
return {
...raw,
title: raw.name || raw.title || '',
price: hasGbp ? gbp : hasUsd ? usd : (raw.price || 0),
currency: hasGbp ? 'GBP' : 'USD',
description: raw.notes || raw.description || '',
slug: raw.slug || String(raw.id),
year: raw.creation_date ? new Date(raw.creation_date).getFullYear() : raw.year,
};
}
// API functions with explicit return types
@ -188,30 +211,33 @@ export async function getArtworks(options?: {
filter: Object.keys(filter).length > 0 ? filter : undefined,
limit: options?.limit || -1,
sort: ['-date_created'],
fields: options?.fields || ['*', { series: ['id', 'name', 'slug'] }],
fields: options?.fields || ['*'],
})
);
return result as Artwork[];
return (result as Artwork[]).map(mapArtwork);
}
export async function getArtwork(idOrSlug: string): Promise<Artwork> {
// Try to find by slug first, then by ID
// Try by ID first (since Directus artworks use numeric IDs as slugs)
try {
const result = await directus.request(
readItem('artworks', idOrSlug, {
fields: ['*'],
})
);
return mapArtwork(result);
} catch {
// Fall back to slug search
const bySlug = await directus.request(
readItems('artworks', {
filter: { slug: { _eq: idOrSlug } },
limit: 1,
fields: ['*', { series: ['*'] }],
fields: ['*'],
})
);
if (bySlug.length > 0) return bySlug[0] as Artwork;
const result = await directus.request(
readItem('artworks', idOrSlug, {
fields: ['*', { series: ['*'] }],
})
);
return result as Artwork;
if (bySlug.length > 0) return mapArtwork(bySlug[0]);
throw new Error(`Artwork not found: ${idOrSlug}`);
}
}
export async function getSeries(options?: { limit?: number }): Promise<Series[]> {
@ -244,6 +270,7 @@ export async function getSeriesItem(idOrSlug: string): Promise<Series> {
}
export async function getEvents(options?: { status?: string; limit?: number }): Promise<Event[]> {
try {
const filter: Record<string, unknown> = {};
if (options?.status) filter.status = { _eq: options.status };
@ -256,6 +283,11 @@ export async function getEvents(options?: { status?: string; limit?: number }):
})
);
return result as Event[];
} catch {
// Events collection may not exist yet
console.warn('Failed to fetch events - collection may not exist yet');
return [];
}
}
export async function getPages(): Promise<Page[]> {