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}

MESSAGE

${safeMessage}

- 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}
+