Initial commit: Katheryn frontend (Directus inventory display)
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.env*.local
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 65 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
|
@ -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!"
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 1006 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 515 KiB |
|
After Width: | Height: | Size: 645 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 486 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 522 KiB |
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 753 KiB |
|
After Width: | Height: | Size: 808 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 794 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 & 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 “You Stole My Voice” represents the central theme of this
|
||||
project – 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">
|
||||
“You Stole My Voice” – 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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'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'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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'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 “correctly” –
|
||||
it'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
“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.”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||