From c7b97aa0cc403770fb723f6f44d03f39e14eab5b Mon Sep 17 00:00:00 2001
From: Jeff Emmett
Date: Wed, 1 Apr 2026 10:47:08 -0700
Subject: [PATCH] ci: use internal registry (bypass Cloudflare upload limit)
Co-Authored-By: Claude Opus 4.6
---
.gitea/workflows/ci.yml | 4 +-
Dockerfile | 4 +
docker-compose.yml | 2 +-
src/app/api/contact/route.ts | 13 +-
src/app/globals.css | 106 +++++++++
src/app/layout.tsx | 6 +-
src/app/page.tsx | 410 ++++++++++++++++++++++++++++++++++-
7 files changed, 532 insertions(+), 13 deletions(-)
diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index bccd585..2f4a8df 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -9,8 +9,8 @@ on:
branches: [main]
env:
- REGISTRY: gitea.jeffemmett.com
- IMAGE: gitea.jeffemmett.com/jeffemmett/xhivart-mirror
+ REGISTRY: localhost:3000
+ IMAGE: localhost:3000/jeffemmett/xhivart-mirror
jobs:
deploy:
diff --git a/Dockerfile b/Dockerfile
index db84889..79fcdf9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -34,6 +34,10 @@ RUN mkdir -p /data && chown nextjs:nodejs /data
RUN mkdir .next
RUN chown nextjs:nodejs .next
+# Create CMS data directories
+RUN mkdir -p /app/data/content /app/data/uploads
+RUN chown -R nextjs:nodejs /app/data
+
# Copy standalone output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
diff --git a/docker-compose.yml b/docker-compose.yml
index ce8ea8b..4a85c19 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,7 +11,7 @@ services:
- SMTP_FROM=${SMTP_FROM}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
volumes:
- - xhivart-data:/data
+ - xhivart-data:/app/data
networks:
- traefik-public
labels:
diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts
index adf7c29..e38de91 100644
--- a/src/app/api/contact/route.ts
+++ b/src/app/api/contact/route.ts
@@ -22,7 +22,7 @@ function escapeHtml(str: string): string {
export async function POST(request: Request) {
try {
const body = await request.json();
- const { name, email, message } = body;
+ const { name, email, service, message } = body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
@@ -36,8 +36,16 @@ export async function POST(request: Request) {
const safeName = escapeHtml(name.trim());
const safeEmail = escapeHtml(email.trim());
+ const safeService = service && typeof service === 'string' ? escapeHtml(service.trim()) : '';
const safeMessage = escapeHtml(message.trim());
+ const serviceSection = safeService
+ ? `
+
SERVICE
+
${safeService}
+
`
+ : '';
+
await transporter.sendMail({
from: `XHIVA Art <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
to: 'xhivart@gmail.com',
@@ -60,13 +68,14 @@ export async function POST(request: Request) {
${safeEmail}
+ ${serviceSection}
- Sent from xhivart.jeffemmett.com contact form
+ Sent from xhiva.art contact form
diff --git a/src/app/globals.css b/src/app/globals.css
index 58ceb9c..dc139a4 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -89,12 +89,71 @@
color: var(--text-dark);
transition: color 0.3s ease;
padding: 0.5rem 1rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
}
.nav-link:hover {
color: var(--accent-gold);
}
+ .nav-link-active {
+ color: var(--accent-gold);
+ }
+
+ .nav-chevron {
+ transition: transform 0.2s ease;
+ }
+
+ /* Dropdown wrapper */
+ .nav-dropdown-wrapper {
+ position: relative;
+ }
+
+ .nav-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ min-width: 200px;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ border-radius: 0.75rem;
+ padding: 0.5rem 0;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s ease, visibility 0.2s ease;
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
+ }
+
+ .nav-dropdown-wrapper:hover .nav-dropdown {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .nav-dropdown-wrapper:hover .nav-chevron {
+ transform: rotate(180deg);
+ }
+
+ .nav-dropdown-item {
+ display: block;
+ padding: 0.5rem 1.5rem;
+ font-family: var(--font-montserrat), 'Montserrat', sans-serif;
+ font-size: 0.7rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ text-decoration: none;
+ color: var(--text-dark);
+ transition: color 0.2s ease, background 0.2s ease;
+ }
+
+ .nav-dropdown-item:hover {
+ color: var(--accent-gold);
+ background: rgba(201, 169, 98, 0.05);
+ }
+
/* Elegant button styles */
.btn-outline {
display: inline-block;
@@ -178,6 +237,18 @@
}
}
+ /* Page hero — shorter than homepage hero */
+ .page-hero {
+ min-height: 60vh;
+ padding: 8rem 2rem 4rem;
+ }
+
+ @media (min-width: 768px) {
+ .page-hero {
+ padding: 10rem 4rem 6rem;
+ }
+ }
+
/* Decorative elements */
.divider {
width: 60px;
@@ -219,6 +290,41 @@
text-align: center;
}
+ /* Price tag */
+ .price-tag {
+ font-family: var(--font-montserrat), 'Montserrat', sans-serif;
+ font-size: 0.875rem;
+ font-weight: 500;
+ letter-spacing: 0.1em;
+ color: var(--accent-gold);
+ }
+
+ /* Methodology step */
+ .methodology-step {
+ padding: 1.5rem;
+ border-left: 2px solid rgba(201, 169, 98, 0.3);
+ transition: border-color 0.3s ease;
+ }
+
+ .methodology-step:hover {
+ border-color: var(--accent-gold);
+ }
+
+ /* Role card */
+ .role-card {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 1rem;
+ padding: 2rem;
+ transition: all 0.3s ease;
+ text-align: center;
+ }
+
+ .role-card:hover {
+ background: rgba(255, 255, 255, 0.08);
+ border-color: var(--accent-gold);
+ }
+
/* Footer */
.footer-link {
font-family: var(--font-montserrat), 'Montserrat', sans-serif;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 512353c..f37034b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { Cormorant_Garamond, Montserrat } from "next/font/google";
import "./globals.css";
+import Navigation from "@/components/Navigation";
+import Footer from "@/components/Footer";
const cormorant = Cormorant_Garamond({
variable: "--font-cormorant",
@@ -34,7 +36,9 @@ export default function RootLayout({
- {children}
+
+ {children}
+