Initial commit: Katheryn frontend (Directus inventory display)

This commit is contained in:
Jeff Emmett 2026-02-11 03:04:32 +01:00
commit 450a1444a0
111 changed files with 11028 additions and 0 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
node_modules
.next
.git
.gitignore
*.md
.env*.local
Dockerfile
docker-compose*.yml
.dockerignore

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

51
Dockerfile Normal file
View File

@ -0,0 +1,51 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source
COPY . .
# Build args
ARG DIRECTUS_API_TOKEN
# Set environment for build
ENV NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
ENV DIRECTUS_API_TOKEN=${DIRECTUS_API_TOKEN}
ENV NEXT_TELEMETRY_DISABLED=1
# Build the application
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built assets
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Set ownership
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

23
docker-compose.yml Normal file
View File

@ -0,0 +1,23 @@
services:
katheryn-frontend:
build:
context: .
dockerfile: Dockerfile
args:
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
container_name: katheryn-frontend
restart: unless-stopped
environment:
- NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
labels:
- "traefik.enable=true"
- "traefik.http.routers.katheryn-staging.rule=Host(`katheryn-staging.jeffemmett.com`)"
- "traefik.http.routers.katheryn-staging.entrypoints=web"
- "traefik.http.services.katheryn-staging.loadbalancer.server.port=3000"
networks:
- traefik-public
networks:
traefik-public:
external: true

18
eslint.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

35
next.config.ts Normal file
View File

@ -0,0 +1,35 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'katheryn-cms.jeffemmett.com',
pathname: '/assets/**',
},
{
protocol: 'http',
hostname: 'localhost',
port: '8055',
pathname: '/assets/**',
},
],
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Cache-Control',
value: 'no-store, must-revalidate',
},
],
},
];
},
};
export default nextConfig;

6551
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@directus/sdk": "^21.0.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

1
public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

View File

@ -0,0 +1,65 @@
#!/bin/bash
# Portrait/Bio
curl -L -o katheryn-portrait.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1554298674802-URS0LU6GR1ASLE3RPUS7/Katheryn+Trenshaw+tree+blossom2-17.jpg"
# Logo
curl -L -o logo.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1590862269439-4NF5POV2BKQF3WCHN9JR/KatherynTrenshawLogoMid.png"
curl -L -o favicon.ico "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1590780333148-K7Q2YNVHYRO1CTQVPZQT/KatherynTrenshawLogo.ico"
# Media Banner
curl -L -o media-banner.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526503000769-8Q3WLYWRWQO5NGX0L38Y/MediaBanner-1BW.png"
# Store/Artwork images
curl -L -o corvids-communion.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/8ca71a57-a72f-4194-96f4-8b9adbd106ce/Corvids+Communion_painting_Katheryn_Trenshaw_25.jpg"
curl -L -o wabi-sabi-triptych.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/ce2a6aaa-6b8c-48cc-af24-dcf25e7ffb16/Wabi+Sabi+triptych+by+Katheryn+Trenshaw.jpeg"
curl -L -o iyos-book-hands.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/183d501e-b2f3-483b-b68f-601693bce4f6/IYOS+book+in+hands.JPG"
curl -L -o white-block.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/b853328b-6e84-44cb-8a70-b9385f38787f/white+block+2.jpg"
# In Your Own Skin page
curl -L -o lizzie-endorsement.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/d6a1fe37-df88-435d-b214-5b356db00313/Lizzie+Hubbard+endorsement+IYOS.png"
curl -L -o depression-portrait.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/6a419225-7251-4854-80d1-3fc3d103138e/depression+portrait.png"
curl -L -o iyos-image.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1692360551122-LVZHF0RDJYSBMHVIYIEA/image-asset.jpeg"
# Breaking the Silence
curl -L -o you-stole-my-voice.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526499653167-IFU1TON8DCMTQF8TP9P2/You+Stole+My+Voice.jpg"
# In Your Own Skin book cover
curl -L -o iyos-book-cover.jpg "https://images-eu.ssl-images-amazon.com/images/I/51q58fdCgbL.jpg"
# Instagram/recent images
curl -L -o instagram-1.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1757865974225-4S9ONVEZKRE77KQJ14AC/image-asset.jpeg"
curl -L -o instagram-2.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1753737585109-VMVABFLVXRCQTK1RXARH/image-asset.jpeg"
curl -L -o instagram-3.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1752744371025-WPBYH0DLXKX6LCHFHNMU/image-asset.jpeg"
curl -L -o instagram-4.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1751319026546-AUAV6M0N864HPP92MKBO/image-asset.jpeg"
curl -L -o instagram-5.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1750068353597-VYM3FR1ZSHXXZ0U2O4NR/image-asset.jpeg"
echo "All images downloaded!"
ls -la
# Main gallery paintings from homepage carousel
curl -L -o choose-love-choose-life.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1605636339005-KXP5FWW72I8KV6KNEPU8/%3Cuntitled%3EChoose+Love%2C+Choose+Life.jpg"
curl -L -o at-the-stillpoint.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1605636369170-918IHIE2QG2X8559QZI9/%3Cuntitled%3EKT1050--At_the_Stillpoint.jpg"
curl -L -o ecstasy-of-belonging.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1605636398212-Z28TYI1OMR1O6LDO1B9R/%3Cuntitled%3EKT1283--Ecstasy+of+Belonging.jpg"
curl -L -o births-blessing-way.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1605636452434-S4AS1X4N6FW242BNPYSH/%3Cuntitled%3EKT1033--Births+Blessing+Way.jpg"
curl -L -o surrender-to-oneself.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1605636498028-JAL4SJQUVVYUUPN7CI37/%3Cuntitled%3EKT1292--Surrender+to+Oneself.jpg"
curl -L -o spiralling-into-starlight.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1605636538275-DBY1CPYS9K1IXM6D0FBK/%3Cuntitled%3ESpiralling+into+Starlight.jpg"
curl -L -o edge-of-light-darkness.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1605636596896-RRAZH1D54L1G90383EZV/%3Cuntitled%3EAt+the+Edge+of+Light+You+Must+Step+into+Darkness.jpg"
# Additional portrait/studio shots
curl -L -o katheryn-studio.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1535812716548-IZ9JMMH605VBAMYHL3G3/20180614Katheryn--18.jpg"
curl -L -o painting-1.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526495397710-0NUF7WC6WV48DFXMWCCN/IMG_0495-3.jpg"
curl -L -o painting-2.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526827919490-KYAEVPI426U0MY2EBVZF/KUUFE9163.jpg"
curl -L -o painting-3.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526827939578-H0SWUDV4KXB5NYMAIKBJ/HBRCE6064.jpg"
curl -L -o painting-4.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526828103311-3P7OS3VMBO2JV3ZGN74S/SXESE7185.jpg"
# Wisdom words/quote images
curl -L -o quote-alan-watts.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526834505816-XUBUYPBT2H8ZNI8PD7L5/PPCCE-Quote+--+AlanWatts+-+The+Only+way+to+make+sense-V1.jpg"
curl -L -o quote-oliver.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526304968884-4DVBOBXFZAW3WSM8YHRU/Attention_Oliver_Quote.jpg"
curl -L -o quote-light.jpg "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526305005354-D8F1WE37ZFSU5YK1TPI3/Light_Quote.jpg"
curl -L -o quote-rumi-light.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526825385941-W54CZMP8QOAL8MZ3AKML/IYOS_Quote_rumi_LIght_enters_woundtif.png"
curl -L -o quote-gilbert-duty.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1526825383872-GY9H2BT1OW03I3919VS4/IYOS_Quote_Gilbert_Duty_Find_Beauty.png"
curl -L -o quote-thich-nhat-hanh.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1669924011534-PRFRJDA0PL61YGIQO87Y/Abdessamad+insta+Thich+Nhat+Hahn+quote.png"
curl -L -o quote-james-baldwin.png "https://images.squarespace-cdn.com/content/v1/5aef40c1cc8feda235a99bb6/1669924170079-NNG7Z6UD1GP48KEIZS6Z/james+baldwin+quote+on+photo+texture.png"
echo "Additional gallery images downloaded!"

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

BIN
public/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

BIN
public/images/logo-kt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

BIN
public/images/recent-1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

BIN
public/images/recent-2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

BIN
public/images/recent-3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
public/images/store-1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/images/store-2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

BIN
public/images/store-3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

BIN
public/images/store-4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
public/images/wisdom-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

BIN
public/images/wisdom-10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
public/images/wisdom-11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
public/images/wisdom-12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
public/images/wisdom-13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
public/images/wisdom-14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
public/images/wisdom-15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

BIN
public/images/wisdom-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

BIN
public/images/wisdom-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

BIN
public/images/wisdom-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

BIN
public/images/wisdom-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

BIN
public/images/wisdom-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 KiB

BIN
public/images/wisdom-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

BIN
public/images/wisdom-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
public/images/wisdom-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
public/images/wisdom-9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/placeholder.svg Normal file
View File

@ -0,0 +1 @@
<svg width="600" height="600" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#e5e7eb"/><text x="50%" y="50%" font-family="Arial" font-size="24" fill="#9ca3af" text-anchor="middle" dominant-baseline="middle">No Image</text></svg>

After

Width:  |  Height:  |  Size: 256 B

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

120
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,120 @@
import { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import { getPage, Page } from '@/lib/directus';
export const metadata: Metadata = {
title: 'About',
description: 'Learn about Katheryn Trenshaw - artist, teacher, and facilitator',
};
export const revalidate = 60;
async function getAboutPage(): Promise<Page | null> {
try {
return await getPage('about');
} catch (error) {
console.error('Error fetching about page:', error);
return null;
}
}
export default async function AboutPage() {
const page = await getAboutPage();
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h1 className="font-serif text-4xl md:text-5xl">About</h1>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-6xl px-4 py-16">
{page?.content ? (
<div
className="prose prose-lg mx-auto"
dangerouslySetInnerHTML={{ __html: page.content }}
/>
) : (
<div className="grid gap-16 lg:grid-cols-2 items-start">
{/* Image */}
<div className="relative aspect-[3/4] bg-gray-100 overflow-hidden">
<Image
src="/images/katheryn-portrait.jpg"
alt="Katheryn Trenshaw"
fill
className="object-cover"
sizes="(max-width: 1024px) 100vw, 50vw"
priority
/>
</div>
{/* Bio */}
<div className="prose prose-lg">
<h2 className="font-serif text-2xl mt-0">Katheryn Trenshaw</h2>
<p>
Katheryn Trenshaw is an artist, teacher, and facilitator dedicated to
helping others discover their creative voice. Through the Passionate
Presence Center for Creative Expression, she offers transformative
experiences that combine art-making with personal development.
</p>
<p>
Her work explores themes of identity, nature, and the human experience,
inviting viewers to pause and reflect on their own journey. Drawing
inspiration from the natural world and inner landscapes, each piece
tells a unique story.
</p>
<h3 className="font-serif text-xl mt-12">The Art</h3>
<p>
Using a variety of media including oils, acrylics, and mixed media,
Katheryn creates pieces that invite the viewer to look closer and
discover something new with each viewing. Her work has been exhibited
in galleries across the UK and internationally.
</p>
<h3 className="font-serif text-xl mt-12">Teaching &amp; Facilitation</h3>
<p>
Beyond personal artistic practice, Katheryn is passionate about
helping others express themselves creatively. Through programs like
<em> In Your Own Skin</em> and one-to-one sessions, she creates
supportive spaces for creative exploration and self-discovery.
</p>
</div>
</div>
)}
</div>
{/* Media Banner */}
<div className="bg-gray-50 py-12">
<div className="mx-auto max-w-4xl px-4">
<div className="relative h-24 md:h-32">
<Image
src="/images/media-banner.png"
alt="Media coverage"
fill
className="object-contain"
sizes="800px"
/>
</div>
</div>
</div>
{/* CTA */}
<div className="border-t border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<p className="text-lg text-gray-600">
Interested in working together?
</p>
<Link href="/contact" className="mt-6 inline-block btn btn-primary">
Get in Touch
</Link>
</div>
</div>
</div>
);
}

33
src/app/blog/page.tsx Normal file
View File

@ -0,0 +1,33 @@
import { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Blog',
description: 'Thoughts, reflections, and creative insights from Katheryn Trenshaw',
};
export default function BlogPage() {
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h1 className="font-serif text-4xl md:text-5xl">Blog</h1>
<p className="mt-4 text-gray-600">
Thoughts, reflections, and creative insights
</p>
</div>
</div>
{/* Coming soon */}
<div className="mx-auto max-w-7xl px-4 py-20 text-center">
<p className="text-gray-500">
Blog posts coming soon.
</p>
<Link href="/" className="mt-8 inline-block text-sm underline underline-offset-4 hover:no-underline">
Return Home
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,96 @@
import { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
export const metadata: Metadata = {
title: 'Breaking The Silence',
description: 'A creative project exploring voice, expression, and the power of speaking our truth',
};
export default function BreakingTheSilencePage() {
return (
<div className="min-h-screen bg-white">
{/* Hero with artwork */}
<div className="relative min-h-[70vh] flex items-center justify-center bg-gray-900">
<div className="absolute inset-0">
<Image
src="/images/you-stole-my-voice.jpg"
alt="You Stole My Voice - Artwork by Katheryn Trenshaw"
fill
className="object-cover opacity-60"
priority
/>
</div>
<div className="relative text-center px-4 py-20 max-w-4xl mx-auto text-white">
<h1 className="font-serif text-4xl md:text-6xl">Breaking The Silence</h1>
<p className="mt-6 text-lg text-gray-200 max-w-2xl mx-auto">
A creative project exploring voice, expression, and the power of speaking our truth
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-3xl px-4 py-16">
<div className="prose prose-lg mx-auto">
<p>
<em>Breaking The Silence</em> is an ongoing creative project that explores
the themes of voice, expression, and the transformative power of speaking
our truth.
</p>
<p>
Through visual art, written word, and collaborative experiences, this
project invites participants to explore the silences in their own lives
and find new ways to give voice to their stories.
</p>
<h2 className="font-serif">The Art</h2>
<p>
The artwork &ldquo;You Stole My Voice&rdquo; represents the central theme of this
project &ndash; the reclaiming of voice after it has been silenced, suppressed,
or taken away. It speaks to experiences of trauma, oppression, and the
courage required to speak again.
</p>
<h2 className="font-serif">The Project</h2>
<p>
This project takes many forms: workshops, exhibitions, collaborative
art-making, and individual sessions. Each offering creates space for
participants to explore their own relationship with voice and silence.
</p>
</div>
</div>
{/* Artwork showcase */}
<div className="bg-gray-50 py-16">
<div className="mx-auto max-w-4xl px-4">
<div className="relative aspect-[4/3]">
<Image
src="/images/you-stole-my-voice.jpg"
alt="You Stole My Voice - Full artwork"
fill
className="object-contain"
sizes="(max-width: 1024px) 100vw, 800px"
/>
</div>
<p className="mt-4 text-center text-sm text-gray-500 italic">
&ldquo;You Stole My Voice&rdquo; &ndash; Katheryn Trenshaw
</p>
</div>
</div>
{/* CTA */}
<div className="border-t border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h3 className="font-serif text-2xl">Interested in This Project?</h3>
<p className="mt-4 text-gray-600">
Get in touch to learn more about upcoming workshops or collaborations.
</p>
<Link href="/contact" className="mt-8 inline-block btn btn-primary">
Get In Touch
</Link>
</div>
</div>
</div>
);
}

278
src/app/checkout/page.tsx Normal file
View File

@ -0,0 +1,278 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { useCart } from '@/context/cart-context';
import { getAssetUrl } from '@/lib/directus';
export default function CheckoutPage() {
const { items, subtotal, removeItem, itemCount } = useCart();
const [formData, setFormData] = useState({
email: '',
firstName: '',
lastName: '',
address: '',
city: '',
postcode: '',
country: 'United Kingdom',
phone: '',
notes: '',
});
if (items.length === 0) {
return (
<div className="min-h-screen flex flex-col items-center justify-center py-20 pt-32">
<h1 className="font-serif text-2xl">Your cart is empty</h1>
<p className="mt-4 text-gray-600">Add some artwork to continue.</p>
<Link href="/store" className="mt-8 btn btn-primary">
Browse Store
</Link>
</div>
);
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// TODO: Integrate with Zettle payment
alert('Checkout functionality coming soon! For now, please contact us to complete your purchase.');
};
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-8">
<h1 className="font-serif text-3xl">Checkout</h1>
</div>
</div>
<div className="mx-auto max-w-7xl px-4 py-12">
<div className="grid gap-12 lg:grid-cols-2">
{/* Order Form */}
<div>
<h2 className="text-lg font-medium uppercase tracking-wider mb-6">
Contact Information
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm text-gray-600 mb-2">
Email Address
</label>
<input
type="email"
id="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label htmlFor="firstName" className="block text-sm text-gray-600 mb-2">
First Name
</label>
<input
type="text"
id="firstName"
required
value={formData.firstName}
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
<div>
<label htmlFor="lastName" className="block text-sm text-gray-600 mb-2">
Last Name
</label>
<input
type="text"
id="lastName"
required
value={formData.lastName}
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
</div>
<h2 className="text-lg font-medium uppercase tracking-wider mt-12 mb-6">
Shipping Address
</h2>
<div>
<label htmlFor="address" className="block text-sm text-gray-600 mb-2">
Address
</label>
<input
type="text"
id="address"
required
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label htmlFor="city" className="block text-sm text-gray-600 mb-2">
City
</label>
<input
type="text"
id="city"
required
value={formData.city}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
<div>
<label htmlFor="postcode" className="block text-sm text-gray-600 mb-2">
Postcode
</label>
<input
type="text"
id="postcode"
required
value={formData.postcode}
onChange={(e) => setFormData({ ...formData, postcode: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
</div>
<div>
<label htmlFor="country" className="block text-sm text-gray-600 mb-2">
Country
</label>
<select
id="country"
value={formData.country}
onChange={(e) => setFormData({ ...formData, country: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none bg-white"
>
<option>United Kingdom</option>
<option>Ireland</option>
<option>France</option>
<option>Germany</option>
<option>United States</option>
<option>Other</option>
</select>
</div>
<div>
<label htmlFor="phone" className="block text-sm text-gray-600 mb-2">
Phone (optional)
</label>
<input
type="tel"
id="phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
<div>
<label htmlFor="notes" className="block text-sm text-gray-600 mb-2">
Order Notes (optional)
</label>
<textarea
id="notes"
rows={3}
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none resize-none"
placeholder="Any special instructions..."
/>
</div>
<div className="pt-6">
<button
type="submit"
className="w-full btn btn-primary py-4"
>
Proceed to Payment
</button>
<p className="mt-4 text-center text-xs text-gray-500">
Secure payment powered by Zettle
</p>
</div>
</form>
</div>
{/* Order Summary */}
<div className="lg:pl-8">
<div className="bg-gray-50 p-8">
<h2 className="text-lg font-medium uppercase tracking-wider mb-6">
Order Summary ({itemCount} {itemCount === 1 ? 'item' : 'items'})
</h2>
<ul className="divide-y divide-gray-200">
{items.map((item) => (
<li key={item.id} className="py-4 flex gap-4">
<div className="relative h-24 w-24 flex-shrink-0 bg-gray-100">
{item.image && (
<Image
src={getAssetUrl(item.image, { width: 200, quality: 80 })}
alt={item.title}
fill
className="object-cover"
/>
)}
</div>
<div className="flex-1">
<h3 className="text-sm font-medium">{item.title}</h3>
{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>
<button
onClick={() => removeItem(item.id)}
className="text-xs text-gray-500 underline mt-2 hover:no-underline"
>
Remove
</button>
</div>
</li>
))}
</ul>
<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>
</div>
<div className="flex justify-between text-sm text-gray-500">
<span>Shipping</span>
<span>Calculated at next step</span>
</div>
</div>
<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>
</div>
<p className="mt-1 text-xs text-gray-500">
+ shipping (calculated at next step)
</p>
</div>
</div>
<div className="mt-8 text-center">
<Link href="/store" className="text-sm underline underline-offset-2 hover:no-underline">
Continue Shopping
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

231
src/app/contact/page.tsx Normal file
View File

@ -0,0 +1,231 @@
'use client';
import { Suspense, useState } from 'react';
import { useSearchParams } from 'next/navigation';
function ContactForm() {
const searchParams = useSearchParams();
const artworkEnquiry = searchParams.get('artwork');
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
subject: artworkEnquiry ? 'purchase' : '',
message: artworkEnquiry ? `I am interested in the artwork "${artworkEnquiry}".` : '',
});
const [submitted, setSubmitted] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// TODO: Implement form submission (e.g., via API route or email service)
setSubmitted(true);
};
if (submitted) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="text-center px-4 py-20">
<h1 className="font-serif text-3xl">Thank You</h1>
<p className="mt-4 text-gray-600">
Your message has been sent. I&apos;ll be in touch soon.
</p>
<a href="/" className="mt-8 inline-block text-sm underline underline-offset-4 hover:no-underline">
Return Home
</a>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h1 className="font-serif text-4xl md:text-5xl">Contact</h1>
<p className="mt-4 text-gray-600">
I&apos;d love to hear from you
</p>
</div>
</div>
<div className="mx-auto max-w-7xl px-4 py-12">
<div className="grid gap-16 lg:grid-cols-2">
{/* Contact Form */}
<div>
<h2 className="text-lg font-medium uppercase tracking-wider mb-6">
Send a Message
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid gap-6 sm:grid-cols-2">
<div>
<label htmlFor="firstName" className="block text-sm text-gray-600 mb-2">
First Name
</label>
<input
type="text"
id="firstName"
required
value={formData.firstName}
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
<div>
<label htmlFor="lastName" className="block text-sm text-gray-600 mb-2">
Last Name
</label>
<input
type="text"
id="lastName"
required
value={formData.lastName}
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm text-gray-600 mb-2">
Email
</label>
<input
type="email"
id="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm text-gray-600 mb-2">
Subject
</label>
<select
id="subject"
required
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none bg-white"
>
<option value="">Select a subject</option>
<option value="purchase">Artwork Enquiry</option>
<option value="commission">Commission Request</option>
<option value="session">Sessions / Workshops</option>
<option value="iyos">In Your Own Skin</option>
<option value="press">Press / Media</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label htmlFor="message" className="block text-sm text-gray-600 mb-2">
Message
</label>
<textarea
id="message"
rows={6}
required
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none resize-none"
/>
</div>
<button type="submit" className="w-full btn btn-primary py-4">
Send Message
</button>
</form>
</div>
{/* Contact Info */}
<div className="lg:pl-8">
<h2 className="text-lg font-medium uppercase tracking-wider mb-6">
Get in Touch
</h2>
<div className="space-y-8">
<div>
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Email</h3>
<a
href="mailto:post@ktrenshaw.com"
className="mt-2 block text-gray-900 hover:underline"
>
post@ktrenshaw.com
</a>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Studio</h3>
<p className="mt-2 text-gray-900">By appointment only</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Response Time</h3>
<p className="mt-2 text-gray-900">Usually within 2-3 business days</p>
</div>
</div>
{/* Social */}
<div className="mt-12 pt-8 border-t border-gray-200">
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Follow</h3>
<div className="mt-4 flex gap-4">
<a
href="https://instagram.com/katheryn_trenshaw"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900"
>
Instagram
</a>
<a
href="https://www.youtube.com/channel/UC37yzOhJTfAFGxI_0fL-kUA"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900"
>
YouTube
</a>
<a
href="https://www.facebook.com/KatherynTrenshawRadicalWellbeing"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900"
>
Facebook
</a>
</div>
</div>
{/* Commission note */}
<div className="mt-12 p-6 bg-gray-50">
<h3 className="font-serif text-lg">Interested in a Commission?</h3>
<p className="mt-2 text-sm text-gray-600">
Commission work is available for private collectors and businesses.
Each commission is a unique collaboration. Please include details
about your vision, preferred size, and any specific requirements.
</p>
</div>
</div>
</div>
</div>
</div>
);
}
export default function ContactPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="animate-pulse text-gray-400">Loading...</div>
</div>
}>
<ContactForm />
</Suspense>
);
}

157
src/app/events/page.tsx Normal file
View File

@ -0,0 +1,157 @@
import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { getEvents, getAssetUrl, Event } from '@/lib/directus';
export const metadata: Metadata = {
title: 'Events',
description: 'Upcoming exhibitions, workshops, and events with Katheryn Trenshaw',
};
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);
return { upcoming, past };
} catch (error) {
console.error('Error fetching events:', error);
return { upcoming: [], past: [] };
}
}
function formatDateRange(start?: string, end?: string): string {
if (!start) return '';
const startDate = new Date(start);
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
};
if (!end) return startDate.toLocaleDateString('en-GB', options);
const endDate = new Date(end);
if (startDate.getMonth() === endDate.getMonth()) {
return `${startDate.getDate()} - ${endDate.toLocaleDateString('en-GB', options)}`;
}
return `${startDate.toLocaleDateString('en-GB', options)} - ${endDate.toLocaleDateString('en-GB', options)}`;
}
function EventCard({ event }: { event: Event }) {
const imageUrl = getAssetUrl(event.image, { width: 600, quality: 80, format: 'webp' });
return (
<article className="group">
<div className="img-zoom relative aspect-[16/9] bg-gray-100">
{event.image ? (
<Image
src={imageUrl}
alt={event.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
) : (
<div className="flex h-full items-center justify-center text-gray-400">
<span className="text-sm">Event</span>
</div>
)}
</div>
<div className="mt-4">
<p className="text-sm text-gray-500">
{formatDateRange(event.start_date, event.end_date)}
</p>
<h3 className="mt-1 font-serif text-xl">{event.title}</h3>
{event.location && (
<p className="mt-1 text-sm text-gray-600">{event.location}</p>
)}
{event.description && (
<div
className="mt-2 text-sm text-gray-600 line-clamp-2"
dangerouslySetInnerHTML={{ __html: event.description }}
/>
)}
</div>
</article>
);
}
export default async function EventsPage() {
const { upcoming, past } = await getEventsList();
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h1 className="font-serif text-4xl md:text-5xl">Events</h1>
<p className="mt-4 text-gray-600">
Exhibitions, workshops, and creative gatherings
</p>
</div>
</div>
<div className="mx-auto max-w-7xl px-4 py-12">
{/* Upcoming Events */}
<section>
<h2 className="font-serif text-2xl border-b border-gray-200 pb-4">
Upcoming Events
</h2>
{upcoming.length > 0 ? (
<div className="mt-8 grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{upcoming.map((event) => (
<EventCard key={event.id} event={event} />
))}
</div>
) : (
<div className="mt-8 py-12 text-center">
<p className="text-gray-500">
No upcoming events at the moment.
</p>
<p className="mt-2 text-sm text-gray-400">
Subscribe to the newsletter to be notified of new events.
</p>
</div>
)}
</section>
{/* Past Events */}
{past.length > 0 && (
<section className="mt-16">
<h2 className="font-serif text-2xl border-b border-gray-200 pb-4">
Past Events
</h2>
<div className="mt-8 grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{past.map((event) => (
<EventCard key={event.id} event={event} />
))}
</div>
</section>
)}
</div>
{/* CTA */}
<div className="border-t border-gray-200 bg-gray-50">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h3 className="font-serif text-2xl">Want to Host an Event?</h3>
<p className="mt-4 text-gray-600">
Interested in hosting a workshop or exhibition? Get in touch.
</p>
<Link href="/contact" className="mt-6 inline-block btn btn-primary">
Contact Us
</Link>
</div>
</div>
</div>
);
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,204 @@
import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getArtwork, getArtworks, getAssetUrl, Artwork } from '@/lib/directus';
export const revalidate = 60;
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
try {
const artwork = await getArtwork(slug);
if (!artwork) return { title: 'Artwork Not Found' };
return {
title: artwork.title,
description: artwork.description?.replace(/<[^>]*>/g, '').slice(0, 160) || `${artwork.title} by Katheryn Trenshaw`,
openGraph: {
images: artwork.image
? [getAssetUrl(artwork.image, { width: 1200, quality: 85 })]
: undefined,
},
};
} catch {
return { title: 'Artwork Not Found' };
}
}
export async function generateStaticParams() {
try {
const artworks = await getArtworks({ status: 'published' });
return artworks.map((artwork) => ({
slug: String(artwork.slug || artwork.id),
}));
} catch {
return [];
}
}
export default async function ArtworkPage({ params }: Props) {
const { slug } = await params;
let artwork: Artwork;
try {
artwork = await getArtwork(slug);
if (!artwork) notFound();
} catch {
notFound();
}
const imageUrl = getAssetUrl(artwork.image, { width: 1200, quality: 90 });
return (
<div className="min-h-screen py-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Breadcrumb */}
<nav className="mb-8">
<ol className="flex items-center space-x-2 text-sm text-gray-500">
<li>
<Link href="/" className="hover:text-gray-700">
Home
</Link>
</li>
<li>/</li>
<li>
<Link href="/gallery" className="hover:text-gray-700">
Gallery
</Link>
</li>
<li>/</li>
<li className="text-gray-900">{artwork.title}</li>
</ol>
</nav>
<div className="grid gap-12 lg:grid-cols-2">
{/* Image */}
<div className="relative aspect-square overflow-hidden rounded-lg bg-gray-100">
{artwork.image ? (
<Image
src={imageUrl}
alt={artwork.title}
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-contain"
priority
/>
) : (
<div className="flex h-full items-center justify-center text-gray-400">
<span>No image available</span>
</div>
)}
</div>
{/* Details */}
<div>
<h1 className="font-serif text-4xl font-bold text-gray-900">
{artwork.title}
</h1>
{artwork.year && (
<p className="mt-2 text-lg text-gray-500">{artwork.year}</p>
)}
<dl className="mt-8 space-y-4">
{artwork.medium && (
<div>
<dt className="text-sm font-medium text-gray-500">Medium</dt>
<dd className="mt-1 text-gray-900">{artwork.medium}</dd>
</div>
)}
{artwork.dimensions && (
<div>
<dt className="text-sm font-medium text-gray-500">Dimensions</dt>
<dd className="mt-1 text-gray-900">{artwork.dimensions}</dd>
</div>
)}
{typeof artwork.series === 'object' && artwork.series?.name && (
<div>
<dt className="text-sm font-medium text-gray-500">Series</dt>
<dd className="mt-1">
<Link
href={`/gallery/series/${artwork.series.slug || artwork.series.id}`}
className="text-amber-700 hover:text-amber-800"
>
{artwork.series.name}
</Link>
</dd>
</div>
)}
</dl>
{/* Price & Status */}
<div className="mt-8 rounded-lg bg-gray-50 p-6">
{artwork.status === 'sold' ? (
<div>
<span className="inline-block rounded bg-red-100 px-3 py-1 text-sm font-medium text-red-800">
Sold
</span>
<p className="mt-2 text-sm text-gray-600">
This artwork has found a new home. Contact us for similar works.
</p>
</div>
) : artwork.price ? (
<div>
<p className="text-3xl font-bold text-gray-900">
${artwork.price.toLocaleString()}
</p>
<button className="mt-4 w-full rounded-md bg-amber-700 px-6 py-3 font-medium text-white transition hover:bg-amber-800">
Enquire About This Piece
</button>
</div>
) : (
<div>
<p className="text-gray-600">Price on request</p>
<button className="mt-4 w-full rounded-md bg-amber-700 px-6 py-3 font-medium text-white transition hover:bg-amber-800">
Contact for Details
</button>
</div>
)}
</div>
{/* Description */}
{artwork.description && (
<div className="mt-8">
<h2 className="text-lg font-medium text-gray-900">About This Piece</h2>
<div
className="prose prose-gray mt-4"
dangerouslySetInnerHTML={{ __html: artwork.description }}
/>
</div>
)}
</div>
</div>
{/* Back to Gallery */}
<div className="mt-12">
<Link
href="/gallery"
className="inline-flex items-center text-amber-700 hover:text-amber-800"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to Gallery
</Link>
</div>
</div>
</div>
);
}

67
src/app/gallery/page.tsx Normal file
View File

@ -0,0 +1,67 @@
import { Metadata } from 'next';
import { getArtworks, getSeries, Series, Artwork } from '@/lib/directus';
import { ArtworkGrid } from '@/components/artwork-grid';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Gallery',
description: 'Browse the complete gallery of artworks by Katheryn Trenshaw',
};
export const revalidate = 60;
async function getGalleryData(): Promise<{ artworks: Artwork[]; series: Series[] }> {
try {
const [artworks, series] = await Promise.all([
getArtworks({ status: 'published' }),
getSeries(),
]);
return { artworks, series };
} catch (error) {
console.error('Error fetching gallery data:', error);
return { artworks: [], series: [] };
}
}
export default async function GalleryPage() {
const { artworks, series } = await getGalleryData();
return (
<div className="min-h-screen pt-28 py-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center">
<h1 className="font-serif text-4xl font-bold text-gray-900 md:text-5xl">
Gallery
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-gray-600">
Explore the complete collection of original artworks
</p>
</div>
{/* Series Filter (if series exist) */}
{series.length > 0 && (
<div className="mt-8">
<h2 className="text-lg font-medium text-gray-900">Browse by Series</h2>
<div className="mt-4 flex flex-wrap gap-2">
{series.map((s) => (
<Link
key={s.id}
href={`/gallery/series/${s.slug || s.id}`}
className="rounded-full bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-amber-50 hover:text-amber-700"
>
{s.name}
</Link>
))}
</div>
</div>
)}
{/* Artwork Grid */}
<div className="mt-12">
<ArtworkGrid artworks={artworks} columns={3} />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,115 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getSeriesItem, getSeries, Series } from '@/lib/directus';
import { ArtworkGrid } from '@/components/artwork-grid';
export const revalidate = 60;
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
try {
const series = await getSeriesItem(slug);
if (!series) return { title: 'Series Not Found' };
return {
title: series.name,
description: series.description?.replace(/<[^>]*>/g, '').slice(0, 160) || `${series.name} - artwork series by Katheryn Trenshaw`,
};
} catch {
return { title: 'Series Not Found' };
}
}
export async function generateStaticParams() {
try {
const allSeries = await getSeries();
return allSeries.map((s) => ({
slug: String(s.slug || s.id),
}));
} catch {
return [];
}
}
export default async function SeriesPage({ params }: Props) {
const { slug } = await params;
let series: Series;
try {
series = await getSeriesItem(slug);
if (!series) notFound();
} catch {
notFound();
}
const artworks = series.artworks?.filter((a) => a.status === 'published') || [];
return (
<div className="min-h-screen py-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Breadcrumb */}
<nav className="mb-8">
<ol className="flex items-center space-x-2 text-sm text-gray-500">
<li>
<Link href="/" className="hover:text-gray-700">
Home
</Link>
</li>
<li>/</li>
<li>
<Link href="/gallery" className="hover:text-gray-700">
Gallery
</Link>
</li>
<li>/</li>
<li className="text-gray-900">{series.name}</li>
</ol>
</nav>
{/* Header */}
<div className="mb-12">
<h1 className="font-serif text-4xl font-bold text-gray-900 md:text-5xl">
{series.name}
</h1>
{series.description && (
<div
className="prose prose-lg prose-gray mt-4 max-w-3xl"
dangerouslySetInnerHTML={{ __html: series.description }}
/>
)}
</div>
{/* Artworks */}
<ArtworkGrid artworks={artworks} columns={3} />
{/* Back to Gallery */}
<div className="mt-12">
<Link
href="/gallery"
className="inline-flex items-center text-amber-700 hover:text-amber-800"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to Gallery
</Link>
</div>
</div>
</div>
);
}

183
src/app/globals.css Normal file
View File

@ -0,0 +1,183 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #1a1a1a;
--accent: #8b7355;
--muted: #6b6b6b;
--border: #e5e5e5;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans), system-ui, sans-serif;
--font-serif: var(--font-serif), Georgia, serif;
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
line-height: 1.6;
}
/* Typography */
.font-serif {
font-family: var(--font-serif), Georgia, "Times New Roman", serif;
}
.font-sans {
font-family: var(--font-sans), system-ui, -apple-system, sans-serif;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Better image rendering */
img {
image-rendering: -webkit-optimize-contrast;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f5f5f5;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #999;
}
/* Link styles */
a {
transition: opacity 0.2s ease;
}
a:hover {
opacity: 0.7;
}
/* Button base */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.05em;
text-transform: uppercase;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.btn-primary {
background: var(--foreground);
color: var(--background);
border-color: var(--foreground);
}
.btn-primary:hover {
background: transparent;
color: var(--foreground);
}
.btn-secondary {
background: transparent;
color: var(--foreground);
border-color: var(--foreground);
}
.btn-secondary:hover {
background: var(--foreground);
color: var(--background);
}
/* Prose styles for CMS content */
.prose {
max-width: 65ch;
}
.prose h1,
.prose h2,
.prose h3 {
font-family: var(--font-serif);
font-weight: 400;
line-height: 1.3;
margin-top: 2em;
margin-bottom: 0.5em;
}
.prose h1 {
font-size: 2.5rem;
}
.prose h2 {
font-size: 1.875rem;
}
.prose h3 {
font-size: 1.5rem;
}
.prose p {
margin-bottom: 1.5em;
}
.prose a {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Image hover zoom effect */
.img-zoom {
overflow: hidden;
}
.img-zoom img {
transition: transform 0.5s ease;
}
.img-zoom:hover img {
transform: scale(1.05);
}
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.5s ease forwards;
}
/* Sold badge */
.badge-sold {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}

View File

@ -0,0 +1,144 @@
import { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
export const metadata: Metadata = {
title: 'In Your Own Skin',
description: 'A transformative program for creative self-discovery and personal expression',
};
export default function InYourOwnSkinPage() {
return (
<div className="min-h-screen bg-white">
{/* Hero */}
<div className="relative min-h-[60vh] flex items-center justify-center bg-gray-100">
<div className="absolute inset-0">
<Image
src="/images/iyos-image.jpg"
alt="In Your Own Skin"
fill
className="object-cover opacity-30"
priority
/>
</div>
<div className="relative text-center px-4 py-20 max-w-4xl mx-auto">
<h1 className="font-serif text-4xl md:text-6xl">In Your Own Skin</h1>
<p className="mt-6 text-lg text-gray-700 max-w-2xl mx-auto">
A transformative journey of creative self-discovery and personal expression
</p>
</div>
</div>
{/* Book Section */}
<div className="mx-auto max-w-6xl px-4 py-16">
<div className="grid gap-12 lg:grid-cols-2 items-center">
<div className="relative aspect-[3/4] max-w-md mx-auto lg:mx-0">
<Image
src="/images/iyos-book-hands.jpg"
alt="In Your Own Skin Book"
fill
className="object-cover"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
</div>
<div>
<h2 className="font-serif text-3xl">The Book</h2>
<p className="mt-6 text-gray-600 leading-relaxed">
<em>In Your Own Skin</em> is a unique program designed to help you reconnect
with your authentic creative self. Through a combination of art-making,
reflective practices, and guided exploration, you&apos;ll discover new ways
to express who you truly are.
</p>
<p className="mt-4 text-gray-600 leading-relaxed">
This program is not about learning to draw or paint &ldquo;correctly&rdquo; &ndash;
it&apos;s about finding your own visual language and using creativity as a
tool for self-understanding and growth.
</p>
<a
href="https://www.amazon.co.uk/dp/B07KQXNV1S"
target="_blank"
rel="noopener noreferrer"
className="mt-8 inline-block btn btn-primary"
>
Get the Book on Amazon
</a>
</div>
</div>
</div>
{/* Endorsement */}
<div className="bg-gray-50 py-16">
<div className="mx-auto max-w-4xl px-4">
<div className="relative">
<Image
src="/images/lizzie-endorsement.png"
alt="Endorsement from Lizzie Hubbard"
width={800}
height={400}
className="w-full"
/>
</div>
</div>
</div>
{/* Depression Portrait Section */}
<div className="mx-auto max-w-6xl px-4 py-16">
<div className="grid gap-12 lg:grid-cols-2 items-center">
<div className="order-2 lg:order-1">
<h2 className="font-serif text-3xl">Who Is This For?</h2>
<ul className="mt-6 space-y-3 text-gray-600">
<li className="flex items-start gap-3">
<span className="text-gray-400"></span>
Anyone seeking a deeper connection with themselves
</li>
<li className="flex items-start gap-3">
<span className="text-gray-400"></span>
Those who feel creatively blocked or disconnected
</li>
<li className="flex items-start gap-3">
<span className="text-gray-400"></span>
People navigating life transitions or seeking clarity
</li>
<li className="flex items-start gap-3">
<span className="text-gray-400"></span>
Creative souls wanting to explore new ways of expression
</li>
</ul>
</div>
<div className="order-1 lg:order-2 relative aspect-square max-w-md mx-auto lg:mx-0">
<Image
src="/images/depression-portrait.png"
alt="Self Portrait"
fill
className="object-contain"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
</div>
</div>
</div>
{/* CTA */}
<div className="border-t border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h3 className="font-serif text-2xl">Ready to Begin?</h3>
<p className="mt-4 text-gray-600">
Get in touch to learn more about the program or book a session.
</p>
<div className="mt-8 flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/contact" className="btn btn-primary">
Enquire About This Program
</Link>
<a
href="https://www.amazon.co.uk/dp/B07KQXNV1S"
target="_blank"
rel="noopener noreferrer"
className="btn btn-secondary"
>
Buy the Book
</a>
</div>
</div>
</div>
</div>
);
}

54
src/app/layout.tsx Normal file
View File

@ -0,0 +1,54 @@
import type { Metadata } from "next";
import { Cormorant_Garamond, Inter } from "next/font/google";
import "./globals.css";
import { Navigation } from "@/components/navigation";
import { Footer } from "@/components/footer";
import { CartProvider } from "@/context/cart-context";
import { CartDrawer } from "@/components/cart-drawer";
const cormorant = Cormorant_Garamond({
variable: "--font-serif",
subsets: ["latin"],
weight: ["400", "500", "600"],
display: "swap",
});
const inter = Inter({
variable: "--font-sans",
subsets: ["latin"],
display: "swap",
});
export const metadata: Metadata = {
title: {
default: "Katheryn Trenshaw | Artist & Creative Expression",
template: "%s | Katheryn Trenshaw",
},
description: "Passionate Presence Center for Creative Expression. Fine art, workshops, and transformative experiences by Katheryn Trenshaw.",
keywords: ["fine art", "artist", "paintings", "workshops", "creative expression", "Katheryn Trenshaw", "In Your Own Skin"],
authors: [{ name: "Katheryn Trenshaw" }],
openGraph: {
type: "website",
locale: "en_GB",
siteName: "Katheryn Trenshaw",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${cormorant.variable} ${inter.variable}`}>
<body className="flex min-h-screen flex-col bg-white antialiased">
<CartProvider>
<Navigation />
<main className="flex-1">{children}</main>
<Footer />
<CartDrawer />
</CartProvider>
</body>
</html>
);
}

412
src/app/page.tsx Normal file
View File

@ -0,0 +1,412 @@
import Link from 'next/link';
import Image from 'next/image';
import { getArtworks, Artwork } from '@/lib/directus';
import { ArtworkCard } from '@/components/artwork-card';
import { HeroCarousel } from '@/components/hero-carousel';
import { WisdomWordsCarousel } from '@/components/wisdom-words-carousel';
export const revalidate = 60;
async function getFeaturedArtworks(): Promise<Artwork[]> {
try {
const artworks = await getArtworks({ status: 'published', limit: 6 });
return artworks;
} catch (error) {
console.error('Error fetching artworks:', error);
return [];
}
}
export default async function HomePage() {
const artworks = await getFeaturedArtworks();
// Hero carousel paintings - exact images from original site
const heroImages = [
{ src: '/images/choose-love-choose-life.jpg', alt: 'Choose Love, Choose Life' },
{ src: '/images/at-the-stillpoint.jpg', alt: 'At the Stillpoint' },
{ src: '/images/ecstasy-of-belonging.jpg', alt: 'Ecstasy of Belonging' },
{ src: '/images/births-blessing-way.jpg', alt: 'Birth\'s Blessing Way' },
{ src: '/images/surrender-to-oneself.jpg', alt: 'Surrender to Oneself' },
{ src: '/images/spiralling-into-starlight.jpg', alt: 'Spiralling into Starlight' },
{ src: '/images/edge-of-light-darkness.jpg', alt: 'At the Edge of Light You Must Step into Darkness' },
];
// Wisdom words gallery - all quote images from original site
const wisdomImages = [
{ src: '/images/wisdom-1.png', alt: 'Thich Nhat Hanh quote' },
{ src: '/images/wisdom-2.png', alt: 'Call it clan quote' },
{ src: '/images/wisdom-3.png', alt: 'James Baldwin quote' },
{ src: '/images/wisdom-4.png', alt: 'David Whyte quote' },
{ src: '/images/wisdom-5.png', alt: 'Gilbert duty quote' },
{ src: '/images/wisdom-6.png', alt: 'Rumi light quote' },
{ src: '/images/wisdom-7.png', alt: 'Jane Howard quote' },
{ src: '/images/wisdom-8.png', alt: 'Katheryn quote - celebrates' },
{ src: '/images/wisdom-9.png', alt: 'Katheryn quote - story' },
{ src: '/images/wisdom-10.png', alt: 'Katheryn quote - viewers' },
{ src: '/images/wisdom-11.jpg', alt: 'IYOS FB quote' },
{ src: '/images/wisdom-12.jpg', alt: 'Oliver quote' },
{ src: '/images/wisdom-13.png', alt: 'Walt Whitman quote' },
{ src: '/images/wisdom-14.jpg', alt: 'Light quote' },
{ src: '/images/wisdom-15.jpg', alt: 'Alan Watts quote' },
{ src: '/images/wisdom-16.png', alt: 'Katheryn beauty quote' },
];
// Instagram feed images
const instagramImages = [
{ src: '/images/instagram-1.jpg', alt: 'Instagram 1' },
{ src: '/images/instagram-2.jpg', alt: 'Instagram 2' },
{ src: '/images/instagram-3.jpg', alt: 'Instagram 3' },
{ src: '/images/instagram-4.jpg', alt: 'Instagram 4' },
{ src: '/images/instagram-5.jpg', alt: 'Instagram 5' },
{ src: '/images/instagram-6.jpg', alt: 'Instagram 6' },
];
return (
<div>
{/* Hero Carousel - Full screen, 2-second autoplay, fade transition */}
<HeroCarousel images={heroImages} interval={2000} />
{/* Tagline - Passionate Presence Center */}
<section className="py-16 bg-white">
<div className="text-center px-4 max-w-4xl mx-auto">
<h1 className="font-serif text-3xl md:text-4xl lg:text-5xl leading-tight text-[#222]">
Passionate Presence Center
<br />
for Creative Expression
</h1>
</div>
</section>
{/* Testimonial Quote - exact text from original */}
<section className="py-16 bg-white border-t border-gray-100">
<div className="text-center px-4 max-w-3xl mx-auto">
<blockquote className="text-lg md:text-xl text-[#222] italic leading-relaxed">
&ldquo;Working with Katheryn Trenshaw is fun and illuminating. It has served the
unfolding of my deep remembering. Artist, healer, transformer; Katheryn embodies
what she teaches.&rdquo;
</blockquote>
<footer className="mt-6 text-sm text-gray-500 uppercase tracking-widest">
DJ, Counsellor, Maine, USA
</footer>
</div>
</section>
{/* ARTWORK Section */}
<section className="py-20 bg-white" id="artwork">
<div className="mx-auto max-w-5xl px-4">
<div className="text-center mb-12">
<p className="text-xs uppercase tracking-[0.3em] text-gray-500 mb-2">ARTWORK</p>
<h2 className="font-serif text-3xl md:text-4xl text-[#222]">Paintings Store</h2>
</div>
{artworks.length > 0 ? (
<div className="grid gap-8 grid-cols-1 md:grid-cols-2">
{artworks.map((artwork, index) => (
<ArtworkCard
key={artwork.id}
artwork={artwork}
priority={index < 2}
aspectRatio="3:2"
/>
))}
</div>
) : (
/* Static fallback with hero images as artwork */
<div className="grid gap-8 grid-cols-1 md:grid-cols-2">
{heroImages.slice(0, 6).map((img, index) => (
<Link key={index} href="/store" className="group block">
<div className="relative aspect-[3/2] bg-gray-100 overflow-hidden">
<Image
src={img.src}
alt={img.alt}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 50vw"
priority={index < 2}
/>
</div>
<div className="mt-4 text-center">
<h3 className="font-serif text-lg text-[#222]">{img.alt}</h3>
</div>
</Link>
))}
</div>
)}
<div className="mt-16 text-center">
<Link
href="/store"
className="inline-block px-8 py-4 bg-[#222] text-white text-sm uppercase tracking-wider rounded-full hover:bg-[#444] transition-colors"
>
View Store
</Link>
</div>
</div>
</section>
{/* IN YOUR OWN SKIN Section */}
<section className="py-20 bg-gray-50" id="in-your-own-skin">
<div className="mx-auto max-w-6xl px-4">
<div className="grid gap-12 lg:grid-cols-2 items-center">
{/* Image */}
<div className="relative aspect-[4/5] bg-gray-100 overflow-hidden">
<Image
src="/images/iyos-book-hands.jpg"
alt="In Your Own Skin"
fill
className="object-cover"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
</div>
{/* Content */}
<div>
<h2 className="font-serif text-4xl md:text-5xl text-[#222]">In Your Own Skin</h2>
<p className="mt-6 text-gray-600 leading-relaxed">
<strong>In Your Own Skin: Photographic Portraits Revealing Our Secrets</strong> is
an antidote to human separation. Katheryn Trenshaw reveals our most powerful
secrets in photographic autobiographies on skin.
</p>
<p className="mt-4 text-gray-600 leading-relaxed">
Features 54 portraits from 40+ countries exploring themes of identity, belonging,
and what it means to truly inhabit your own creative space.
</p>
<div className="mt-8 flex flex-col sm:flex-row gap-4">
<Link
href="/in-your-own-skin"
className="inline-block px-8 py-4 bg-[#222] text-white text-sm uppercase tracking-wider rounded-full hover:bg-[#444] transition-colors text-center"
>
Watch now
</Link>
<a
href="https://www.amazon.co.uk/dp/B07KQXNV1S"
target="_blank"
rel="noopener noreferrer"
className="inline-block px-8 py-4 border-2 border-[#222] text-[#222] text-sm uppercase tracking-wider rounded-full hover:bg-[#222] hover:text-white transition-colors text-center"
>
Buy on Amazon
</a>
</div>
</div>
</div>
</div>
</section>
{/* EVENTS Section */}
<section className="py-20 bg-white" id="events">
<div className="mx-auto max-w-4xl px-4">
<div className="text-center mb-12">
<p className="text-xs uppercase tracking-[0.3em] text-gray-500 mb-2">EVENTS</p>
<h2 className="font-serif text-3xl md:text-4xl text-[#222]">Upcoming</h2>
</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.
</p>
<Link href="/events" className="mt-3 inline-block text-sm text-[#222] underline underline-offset-4 hover:no-underline">
Learn More
</Link>
</div>
</div>
<div className="mt-12 text-center">
<Link
href="/events"
className="inline-block px-8 py-4 border-2 border-[#222] text-[#222] text-sm uppercase tracking-wider rounded-full hover:bg-[#222] hover:text-white transition-colors"
>
View Events
</Link>
</div>
</div>
</section>
{/* WISDOM WORDS Section */}
<section className="py-20 bg-gray-50" id="wisdom-words">
<div className="mx-auto max-w-6xl px-4">
<div className="text-center mb-12">
<p className="text-xs uppercase tracking-[0.3em] text-gray-500 mb-2">INSPIRATION</p>
<h2 className="font-serif text-3xl md:text-4xl text-[#222]">Wisdom Words</h2>
<p className="mt-4 text-gray-600">
This is a treasure trove of favorite inspirations and wise words to share.
</p>
</div>
<WisdomWordsCarousel images={wisdomImages} interval={6000} />
<div className="mt-12 text-center">
<Link
href="/wisdom-words"
className="inline-block px-8 py-4 border-2 border-[#222] text-[#222] text-sm uppercase tracking-wider rounded-full hover:bg-[#222] hover:text-white transition-colors"
>
Wisdom Words
</Link>
</div>
</div>
</section>
{/* INSTAGRAM Section */}
<section className="py-20 bg-white" id="instagram">
<div className="mx-auto max-w-6xl px-4">
<div className="text-center mb-12">
<p className="text-xs uppercase tracking-[0.3em] text-gray-500 mb-2">FOLLOW ALONG</p>
<h2 className="font-serif text-3xl md:text-4xl text-[#222]">Instagram</h2>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2">
{instagramImages.map((img, index) => (
<a
key={index}
href="https://instagram.com/katheryn_trenshaw"
target="_blank"
rel="noopener noreferrer"
className="relative aspect-square group overflow-hidden"
>
<Image
src={img.src}
alt={img.alt}
fill
className="object-cover transition-transform duration-300 group-hover:scale-110"
sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 16vw"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-center justify-center">
<svg
className="w-8 h-8 text-white opacity-0 group-hover:opacity-100 transition-opacity"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z" />
</svg>
</div>
</a>
))}
</div>
<div className="mt-12 text-center">
<a
href="https://instagram.com/katheryn_trenshaw"
target="_blank"
rel="noopener noreferrer"
className="inline-block px-8 py-4 bg-[#222] text-white text-sm uppercase tracking-wider rounded-full hover:bg-[#444] transition-colors"
>
Visit Instagram
</a>
</div>
</div>
</section>
{/* SUBSCRIBE Section - Dark footer style */}
<section className="py-20 bg-[#222] text-white">
<div className="mx-auto max-w-xl px-4 text-center">
<h2 className="font-serif text-3xl md:text-4xl">Get in Touch</h2>
<p className="mt-4 text-gray-300">
Receive updates and occasional newsletters about new artwork, events, and creative offerings.
</p>
<form className="mt-8 flex flex-col sm:flex-row gap-3">
<input
type="email"
placeholder="Enter your email"
className="flex-1 px-4 py-4 bg-transparent border border-white/30 text-white placeholder-gray-400 focus:border-white focus:outline-none rounded-full"
/>
<button
type="submit"
className="px-8 py-4 bg-white text-[#222] text-sm uppercase tracking-wider rounded-full hover:bg-gray-100 transition-colors"
>
Subscribe
</button>
</form>
{/* Social Links */}
<div className="mt-12 flex justify-center gap-6">
<a
href="mailto:post@ktrenshaw.com"
className="w-10 h-10 border border-white/30 rounded-full flex items-center justify-center hover:border-white transition-colors"
aria-label="Email"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</a>
<a
href="https://www.youtube.com/channel/UC37yzOhJTfAFGxI_0fL-kUA"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 border border-white/30 rounded-full flex items-center justify-center hover:border-white transition-colors"
aria-label="YouTube"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
</a>
<a
href="https://instagram.com/katheryn_trenshaw"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 border border-white/30 rounded-full flex items-center justify-center hover:border-white transition-colors"
aria-label="Instagram"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z" />
</svg>
</a>
<a
href="https://www.facebook.com/KatherynTrenshawRadicalWellbeing"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 border border-white/30 rounded-full flex items-center justify-center hover:border-white transition-colors"
aria-label="Facebook"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" />
</svg>
</a>
</div>
{/* Logo */}
<div className="mt-12">
<Image
src="/images/logo-kt.png"
alt="Katheryn Trenshaw"
width={200}
height={60}
className="mx-auto opacity-80"
/>
</div>
{/* Copyright */}
<p className="mt-8 text-xs text-gray-500">
images and writing © Katheryn M. Trenshaw 2025
</p>
</div>
</section>
</div>
);
}

54
src/app/sessions/page.tsx Normal file
View File

@ -0,0 +1,54 @@
import { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Sessions',
description: 'One-to-one creative sessions tailored to your personal journey',
};
export default function SessionsPage() {
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h1 className="font-serif text-4xl md:text-5xl">Sessions</h1>
<p className="mt-4 text-gray-600">
One-to-one creative sessions tailored to your journey
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-3xl px-4 py-16">
<div className="prose prose-lg mx-auto">
<p>
Individual sessions offer a dedicated space for you to explore your
creativity in a supportive, one-to-one environment. Each session is
tailored to where you are in your journey and what you need most.
</p>
<h2 className="font-serif">What Sessions Include</h2>
<ul>
<li>A confidential, supportive space for exploration</li>
<li>Guided creative exercises and practices</li>
<li>Reflective conversation and insight</li>
<li>Tools and techniques you can use on your own</li>
</ul>
<h2 className="font-serif">Session Options</h2>
<p>
Sessions are available in person (when possible) or online via video call.
Please get in touch to discuss what would work best for you.
</p>
</div>
<div className="mt-12 text-center">
<Link href="/contact" className="btn btn-primary">
Book a Session
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,293 @@
'use client';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { getArtwork, getArtworks, getAssetUrl, Artwork } from '@/lib/directus';
import { useCart } from '@/context/cart-context';
export default function ArtworkDetailPage() {
const params = useParams();
const slug = params.slug as string;
const [artwork, setArtwork] = useState<Artwork | null>(null);
const [relatedWorks, setRelatedWorks] = useState<Artwork[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedImage, setSelectedImage] = useState(0);
const { addItem, isInCart } = useCart();
const inCart = artwork ? isInCart(artwork.id) : false;
const isSold = artwork?.status === 'sold';
const isAvailable = artwork?.status === 'published' && artwork?.price && artwork.price > 0;
useEffect(() => {
async function loadArtwork() {
try {
setLoading(true);
const data = await getArtwork(slug);
setArtwork(data);
// Fetch related works
const allWorks = await getArtworks({ status: 'published', limit: 4 });
setRelatedWorks(allWorks.filter((w) => w.id !== data.id).slice(0, 3));
} catch (err) {
setError('Artwork not found');
console.error(err);
} finally {
setLoading(false);
}
}
loadArtwork();
}, [slug]);
const handleAddToCart = () => {
if (artwork && isAvailable && !inCart) {
addItem(artwork);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center pt-24">
<div className="animate-pulse text-gray-400">Loading...</div>
</div>
);
}
if (error || !artwork) {
return (
<div className="min-h-screen flex flex-col items-center justify-center pt-24">
<h1 className="font-serif text-2xl">Artwork not found</h1>
<Link href="/store" className="mt-4 text-sm underline">
Back to Store
</Link>
</div>
);
}
const mainImageUrl = getAssetUrl(artwork.image, { width: 1200, quality: 90, format: 'webp' });
const galleryImages = artwork.gallery || [];
return (
<div className="min-h-screen bg-white pt-24">
{/* Breadcrumb */}
<div className="border-b border-gray-100">
<div className="mx-auto max-w-7xl px-4 py-4">
<nav className="text-sm text-gray-500">
<Link href="/" className="hover:text-gray-900">Home</Link>
<span className="mx-2">/</span>
<Link href="/store" className="hover:text-gray-900">Store</Link>
<span className="mx-2">/</span>
<span className="text-gray-900">{artwork.title}</span>
</nav>
</div>
</div>
{/* Main content */}
<div className="mx-auto max-w-7xl px-4 py-12">
<div className="grid gap-12 lg:grid-cols-2">
{/* Images */}
<div>
<div className="relative aspect-[4/5] bg-gray-100">
{artwork.image ? (
<Image
src={mainImageUrl}
alt={artwork.title}
fill
className="object-contain"
sizes="(max-width: 1024px) 100vw, 50vw"
priority
/>
) : (
<div className="flex h-full items-center justify-center text-gray-400">
No image available
</div>
)}
{isSold && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<span className="bg-white px-6 py-2 text-sm font-medium uppercase tracking-wider">
Sold
</span>
</div>
)}
</div>
{/* Thumbnail gallery */}
{galleryImages.length > 0 && (
<div className="mt-4 flex gap-2 overflow-x-auto">
<button
onClick={() => setSelectedImage(0)}
className={`relative h-20 w-20 flex-shrink-0 bg-gray-100 ${
selectedImage === 0 ? 'ring-2 ring-gray-900' : ''
}`}
>
{artwork.image && (
<Image
src={getAssetUrl(artwork.image, { width: 200 })}
alt={artwork.title}
fill
className="object-cover"
/>
)}
</button>
{galleryImages.map((img, index) => (
<button
key={index}
onClick={() => setSelectedImage(index + 1)}
className={`relative h-20 w-20 flex-shrink-0 bg-gray-100 ${
selectedImage === index + 1 ? 'ring-2 ring-gray-900' : ''
}`}
>
<Image
src={getAssetUrl(img, { width: 200 })}
alt={`${artwork.title} - Image ${index + 2}`}
fill
className="object-cover"
/>
</button>
))}
</div>
)}
</div>
{/* Details */}
<div className="lg:py-8">
<h1 className="font-serif text-3xl md:text-4xl">{artwork.title}</h1>
{artwork.year && (
<p className="mt-2 text-gray-500">{artwork.year}</p>
)}
{/* Price */}
<div className="mt-6">
{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-lg text-gray-600">Price on request</p>
)}
</div>
{/* Add to cart */}
{isAvailable && !isSold && (
<div className="mt-8">
<button
onClick={handleAddToCart}
disabled={inCart}
className={`w-full py-4 text-center text-sm font-medium uppercase tracking-wider transition ${
inCart
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
: 'bg-gray-900 text-white hover:bg-gray-700'
}`}
>
{inCart ? 'Added to Cart' : 'Add to Cart'}
</button>
</div>
)}
{/* Enquire button for sold or POA items */}
{(isSold || !artwork.price) && (
<div className="mt-8">
<Link
href={`/contact?artwork=${encodeURIComponent(artwork.title)}`}
className="block w-full py-4 text-center text-sm font-medium uppercase tracking-wider border border-gray-900 text-gray-900 hover:bg-gray-900 hover:text-white transition"
>
Enquire About This Work
</Link>
</div>
)}
{/* Details */}
<div className="mt-8 space-y-4 border-t border-gray-200 pt-8">
{artwork.medium && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Medium</span>
<span className="text-gray-900">{artwork.medium}</span>
</div>
)}
{artwork.dimensions && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Dimensions</span>
<span className="text-gray-900">{artwork.dimensions}</span>
</div>
)}
{artwork.series && typeof artwork.series === 'object' && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Series</span>
<Link
href={`/gallery/series/${artwork.series.slug || artwork.series.id}`}
className="text-gray-900 underline underline-offset-2 hover:no-underline"
>
{artwork.series.name}
</Link>
</div>
)}
</div>
{/* Description */}
{artwork.description && (
<div className="mt-8 border-t border-gray-200 pt-8">
<h3 className="text-sm font-medium uppercase tracking-wider text-gray-500">
About This Work
</h3>
<div
className="mt-4 prose prose-sm text-gray-600"
dangerouslySetInnerHTML={{ __html: artwork.description }}
/>
</div>
)}
{/* Shipping info */}
<div className="mt-8 border-t border-gray-200 pt-8">
<div className="flex items-start gap-4">
<svg className="h-5 w-5 text-gray-400 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
<div>
<p className="text-sm font-medium text-gray-900">Secure Shipping</p>
<p className="mt-1 text-sm text-gray-500">
All artworks are carefully packaged and shipped with insurance.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Related works */}
{relatedWorks.length > 0 && (
<div className="border-t border-gray-200 bg-gray-50">
<div className="mx-auto max-w-7xl px-4 py-16">
<h2 className="font-serif text-2xl text-center mb-12">You May Also Like</h2>
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{relatedWorks.map((work) => (
<Link key={work.id} href={`/store/${work.slug || work.id}`} className="group">
<div className="img-zoom relative aspect-[4/5] bg-gray-100">
{work.image && (
<Image
src={getAssetUrl(work.image, { width: 600, quality: 85, format: 'webp' })}
alt={work.title}
fill
className="object-cover"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
)}
</div>
<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>
)}
</div>
</Link>
))}
</div>
</div>
</div>
)}
</div>
);
}

134
src/app/store/page.tsx Normal file
View File

@ -0,0 +1,134 @@
import { Metadata } from 'next';
import { getArtworks, Artwork } from '@/lib/directus';
import { ArtworkCard } from '@/components/artwork-card';
export const metadata: Metadata = {
title: 'Store',
description: 'Shop original artworks, prints, and unique pieces by Katheryn Trenshaw',
};
export const revalidate = 60;
async function getShopItems(): Promise<{ available: Artwork[]; sold: Artwork[] }> {
try {
const allArtworks = await getArtworks({});
const available = allArtworks.filter(
(a) => a.status === 'published' && a.price && a.price > 0
);
const sold = allArtworks.filter((a) => a.status === 'sold');
return { available, sold };
} catch (error) {
console.error('Error fetching shop items:', error);
return { available: [], sold: [] };
}
}
export default async function StorePage() {
const { available, sold } = await getShopItems();
const allItems = [...available, ...sold];
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h1 className="font-serif text-4xl md:text-5xl">Store</h1>
<p className="mt-4 text-gray-600">
Original artworks and unique pieces
</p>
</div>
</div>
{/* Filter tabs */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4">
<div className="flex gap-8 py-4 text-sm">
<button className="font-medium text-gray-900 border-b-2 border-gray-900 pb-4 -mb-[17px]">
All ({allItems.length})
</button>
<button className="text-gray-500 hover:text-gray-900 pb-4 -mb-[17px]">
Available ({available.length})
</button>
<button className="text-gray-500 hover:text-gray-900 pb-4 -mb-[17px]">
Sold ({sold.length})
</button>
</div>
</div>
</div>
{/* Products Grid - 2 columns to match original */}
<div className="mx-auto max-w-5xl px-4 py-12">
{allItems.length > 0 ? (
<div className="grid gap-8 grid-cols-1 md:grid-cols-2">
{allItems.map((artwork, index) => (
<ArtworkCard
key={artwork.id}
artwork={artwork}
priority={index < 2}
aspectRatio="3:2"
/>
))}
</div>
) : (
<div className="py-20 text-center">
<h3 className="font-serif text-xl text-gray-900">
Coming Soon
</h3>
<p className="mt-2 text-gray-500">
New works will be available shortly.
</p>
<p className="mt-4 text-sm text-gray-500">
<a href="/contact" className="underline underline-offset-2 hover:no-underline">
Contact us
</a>{' '}
for enquiries about specific pieces.
</p>
</div>
)}
</div>
{/* Info Section */}
<div className="border-t border-gray-200 bg-gray-50">
<div className="mx-auto max-w-7xl px-4 py-16">
<div className="grid gap-12 md:grid-cols-3 text-center">
<div>
<div className="mx-auto h-12 w-12 flex items-center justify-center">
<svg className="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
</div>
<h3 className="mt-4 font-medium text-gray-900">Secure Packaging</h3>
<p className="mt-2 text-sm text-gray-600">
Each artwork is carefully packaged to ensure it arrives in perfect condition.
</p>
</div>
<div>
<div className="mx-auto h-12 w-12 flex items-center justify-center">
<svg className="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="mt-4 font-medium text-gray-900">Certificate of Authenticity</h3>
<p className="mt-2 text-sm text-gray-600">
All original artworks come with a signed certificate.
</p>
</div>
<div>
<div className="mx-auto h-12 w-12 flex items-center justify-center">
<svg className="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="mt-4 font-medium text-gray-900">Personal Service</h3>
<p className="mt-2 text-sm text-gray-600">
Questions? Get in touch for personalized assistance.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,58 @@
import { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Workshops',
description: 'Group workshops exploring art, creativity, and self-expression',
};
export default function WorkshopsPage() {
return (
<div className="min-h-screen bg-white pt-24">
{/* Header */}
<div className="border-b border-gray-200">
<div className="mx-auto max-w-7xl px-4 py-16 text-center">
<h1 className="font-serif text-4xl md:text-5xl">Workshops</h1>
<p className="mt-4 text-gray-600">
Group experiences exploring art, creativity, and self-expression
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-3xl px-4 py-16">
<div className="prose prose-lg mx-auto">
<p>
Workshops offer a unique opportunity to explore creativity in a group
setting. The energy and insights that emerge when people create together
can be truly transformative.
</p>
<h2 className="font-serif">Workshop Themes</h2>
<p>
Workshops vary in focus and duration, but typically explore themes such as:
</p>
<ul>
<li>Creative self-expression and identity</li>
<li>Art as a tool for reflection and insight</li>
<li>Colour, mark-making, and visual language</li>
<li>Connecting with nature through creativity</li>
<li>Collaborative art-making</li>
</ul>
<h2 className="font-serif">Upcoming Workshops</h2>
<p>
Check the <Link href="/events">Events page</Link> for upcoming workshop
dates and details, or sign up for the newsletter to be notified of new offerings.
</p>
</div>
<div className="mt-12 text-center">
<Link href="/events" className="btn btn-primary">
View Events
</Link>
</div>
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More