From 06abb520246582352303b8af7a3794bdb21cc680 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Tue, 16 Sep 2025 21:36:20 -0600 Subject: [PATCH] v05 push to origin --- .dockerignore | 29 + .gitignore | 28 +- Dockerfile | 30 + README.md | 135 +- app/ClientLayout.tsx | 23 + app/aftercare/page.tsx | 15 + app/artists/[id]/book/page.tsx | 21 + app/artists/[id]/page.tsx | 21 + app/artists/page.tsx | 13 + app/book/page.tsx | 15 + app/contact/page.tsx | 15 + app/deposit/page.tsx | 15 + app/gift-cards/page.tsx | 15 + app/globals.css | 311 +- app/layout.tsx | 48 +- app/page.tsx | 126 +- app/privacy/page.tsx | 15 + app/specials/page.tsx | 15 + app/terms/page.tsx | 15 + components.json | 21 + components/aftercare-page.tsx | 352 + components/artist-portfolio.tsx | 413 + components/artists-grid.tsx | 210 + components/artists-page-section.tsx | 432 + components/artists-section.tsx | 341 + components/booking-form.tsx | 579 + components/contact-modal.tsx | 459 + components/contact-page.tsx | 489 + components/contact-section.tsx | 188 + components/deposit-page.tsx | 378 + components/footer.tsx | 207 + components/gift-card-balance-checker.tsx | 84 + components/gift-cards-page.tsx | 488 + components/hero-section.tsx | 70 + components/navigation.tsx | 125 + components/privacy-page.tsx | 173 + components/scroll-progress.tsx | 24 + components/scroll-to-section.tsx | 32 + components/section-header.tsx | 55 + components/services-section.tsx | 255 + components/smooth-scroll-provider.tsx | 44 + components/specials-page.tsx | 284 + components/terms-page.tsx | 173 + components/theme-provider.tsx | 11 + components/ui/accordion.tsx | 53 + components/ui/alert-dialog.tsx | 157 + components/ui/alert.tsx | 66 + components/ui/aspect-ratio.tsx | 11 + components/ui/avatar.tsx | 53 + components/ui/badge.tsx | 46 + components/ui/breadcrumb.tsx | 109 + components/ui/button.tsx | 59 + components/ui/calendar.tsx | 158 + components/ui/card.tsx | 92 + components/ui/carousel.tsx | 241 + components/ui/chart.tsx | 353 + components/ui/checkbox.tsx | 29 + components/ui/collapsible.tsx | 33 + components/ui/command.tsx | 137 + components/ui/context-menu.tsx | 211 + components/ui/dialog.tsx | 123 + components/ui/drawer.tsx | 135 + components/ui/dropdown-menu.tsx | 219 + components/ui/form.tsx | 167 + components/ui/hover-card.tsx | 44 + components/ui/input-otp.tsx | 68 + components/ui/input.tsx | 21 + components/ui/label.tsx | 24 + components/ui/menubar.tsx | 236 + components/ui/navigation-menu.tsx | 142 + components/ui/pagination.tsx | 100 + components/ui/popover.tsx | 48 + components/ui/progress.tsx | 31 + components/ui/radio-group.tsx | 33 + components/ui/resizable.tsx | 48 + components/ui/scroll-area.tsx | 58 + components/ui/select.tsx | 160 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 103 + components/ui/sidebar.tsx | 677 + components/ui/skeleton.tsx | 13 + components/ui/slider.tsx | 63 + components/ui/sonner.tsx | 25 + components/ui/switch.tsx | 31 + components/ui/table.tsx | 116 + components/ui/tabs.tsx | 66 + components/ui/textarea.tsx | 18 + components/ui/toast.tsx | 129 + components/ui/toaster.tsx | 35 + components/ui/toggle-group.tsx | 73 + components/ui/toggle.tsx | 47 + components/ui/tooltip.tsx | 61 + components/ui/use-mobile.tsx | 19 + components/ui/use-toast.ts | 191 + copy-artist-images.sh | 70 + data/artists.ts | 301 + hooks/use-mobile.ts | 19 + hooks/use-toast.ts | 191 + lib/utils.ts | 6 + next.config.mjs | 15 +- package-lock.json | 11073 +++++++++------- package.json | 73 +- pnpm-lock.yaml | 5 + postcss.config.mjs | 6 +- public/abstract-geometric-shapes.png | Bin 0 -> 675864 bytes public/american-traditional-anchor-tattoo.jpg | Bin 0 -> 128081 bytes public/artists/amari-rodriguez-portrait.jpg | Bin 0 -> 1635 bytes public/artists/amari-rodriguez-work-1.jpg | Bin 0 -> 128081 bytes public/artists/amari-rodriguez-work-2.jpg | Bin 0 -> 151817 bytes public/artists/amari-rodriguez-work-3.jpg | Bin 0 -> 79250 bytes public/artists/angel-andrade-portrait.jpg | Bin 0 -> 45432 bytes public/artists/angel-andrade-work-1.jpg | Bin 0 -> 247218 bytes public/artists/angel-andrade-work-2.jpg | Bin 0 -> 262726 bytes public/artists/angel-andrade-work-3.jpg | Bin 0 -> 193385 bytes public/artists/angel-andrade-work-4.jpg | Bin 0 -> 214689 bytes public/artists/christy-lumberg-portrait.jpg | Bin 0 -> 86607 bytes public/artists/christy-lumberg-work-1.jpg | Bin 0 -> 231164 bytes public/artists/christy-lumberg-work-2.jpg | Bin 0 -> 133115 bytes public/artists/christy-lumberg-work-3.jpg | Bin 0 -> 85402 bytes public/artists/christy-lumberg-work-4.jpg | Bin 0 -> 169220 bytes public/artists/donovan-lankford-portrait.jpg | Bin 0 -> 167374 bytes public/artists/ej-segoviano-portrait.jpg | Bin 0 -> 1635 bytes public/artists/ej-segoviano-work-1.jpg | Bin 0 -> 133115 bytes public/artists/ej-segoviano-work-2.jpg | Bin 0 -> 141583 bytes public/artists/ej-segoviano-work-3.jpg | Bin 0 -> 115966 bytes public/artists/pako-martinez-portrait.jpg | Bin 0 -> 1635 bytes public/artists/pako-martinez-work-1.jpg | Bin 0 -> 231164 bytes public/artists/pako-martinez-work-2.jpg | Bin 0 -> 128081 bytes public/artists/pako-martinez-work-3.jpg | Bin 0 -> 151817 bytes public/biomechanical-scifi-tattoo-artwork.jpg | Bin 0 -> 182674 bytes ...k-and-grey-portrait-tattoo-masterpiece.jpg | Bin 0 -> 133115 bytes public/blackwork-tribal-tattoo-artwork.jpg | Bin 0 -> 170122 bytes public/botanical-nature-tattoo-artwork.jpg | Bin 0 -> 95406 bytes public/colorful-traditional-bird-tattoo.jpg | Bin 0 -> 85402 bytes public/delicate-fine-line-flower-tattoo.jpg | Bin 0 -> 75449 bytes public/fine-line-botanical-tattoo-elegant.jpg | Bin 0 -> 79250 bytes .../fine-line-minimalist-tattoo-artwork.jpg | Bin 0 -> 121861 bytes public/geometric-abstract-tattoo-artwork.jpg | Bin 0 -> 169792 bytes public/hyperrealistic-eye-tattoo-design.jpg | Bin 0 -> 195891 bytes public/japanese-koi-fish-tattoo-colorful.jpg | Bin 0 -> 134473 bytes public/japanese-oriental-tattoo-artwork.jpg | Bin 0 -> 98142 bytes .../japanese-samurai-tattoo-traditional.jpg | Bin 0 -> 11265 bytes public/minimalist-geometric-tattoo-design.jpg | Bin 0 -> 70840 bytes public/minimalist-tattoo-design-on-arm.jpg | Bin 0 -> 88416 bytes public/neo-traditional-wolf-tattoo-design.jpg | Bin 0 -> 169220 bytes .../oriental-cherry-blossom-tattoo-design.jpg | Bin 0 -> 162484 bytes ...alistic-portrait-tattoo-black-and-grey.jpg | Bin 0 -> 141583 bytes public/placeholder-0h0qb.png | Bin 0 -> 11265 bytes public/placeholder-882fw.png | Bin 0 -> 11265 bytes public/placeholder-ah8n2.png | Bin 0 -> 11265 bytes public/placeholder-e6fqm.png | Bin 0 -> 11265 bytes public/placeholder-jk026.png | Bin 0 -> 11265 bytes public/placeholder-jmey3.png | Bin 0 -> 11265 bytes public/placeholder-ju7df.png | Bin 0 -> 11265 bytes public/placeholder-lh3ki.png | Bin 0 -> 11265 bytes public/placeholder-logo.png | Bin 0 -> 568 bytes public/placeholder-logo.svg | 1 + public/placeholder-mjx9t.png | Bin 0 -> 11265 bytes public/placeholder-mykqu.png | Bin 0 -> 11265 bytes public/placeholder-qrydh.png | Bin 0 -> 11265 bytes public/placeholder-r6l7b.png | Bin 0 -> 11265 bytes public/placeholder-s31fj.png | Bin 0 -> 11265 bytes public/placeholder-s803z.png | Bin 0 -> 11265 bytes public/placeholder-user.jpg | Bin 0 -> 1635 bytes public/placeholder-xzjye.png | Bin 0 -> 11265 bytes public/placeholder.jpg | Bin 0 -> 1064 bytes public/placeholder.svg | 1 + ...-female-tattoo-artist-botanical-nature.jpg | Bin 0 -> 163267 bytes ...-female-tattoo-artist-geometric-design.jpg | Bin 0 -> 122027 bytes ...-female-tattoo-artist-watercolor-style.jpg | Bin 0 -> 198092 bytes ...-female-tattoo-artist-with-delicate-fi.jpg | Bin 0 -> 89624 bytes ...-female-tattoo-artist-with-traditional.jpg | Bin 0 -> 117410 bytes ...-male-tattoo-artist-biomechanical-sci-.jpg | Bin 0 -> 126280 bytes ...al-male-tattoo-artist-blackwork-tribal.jpg | Bin 0 -> 113449 bytes ...-male-tattoo-artist-specializing-in-re.jpg | Bin 0 -> 107221 bytes ...-male-tattoo-artist-with-japanese-styl.jpg | Bin 0 -> 132486 bytes ...-tattoo-artist-working-on-detailed-tat.jpg | Bin 0 -> 121448 bytes ...alistic-animal-tattoo-detailed-shading.jpg | Bin 0 -> 201337 bytes public/realistic-portrait-tattoo-artwork.jpg | Bin 0 -> 115966 bytes public/simple-line-work-tattoo-artistic.jpg | Bin 0 -> 65459 bytes public/tattoo-artist-workspace.jpg | Bin 0 -> 147406 bytes public/tattoo-equipment-and-tools.jpg | Bin 0 -> 117966 bytes ...ditional-japanese-dragon-tattoo-sleeve.jpg | Bin 0 -> 166311 bytes ...itional-neo-traditional-tattoo-artwork.jpg | Bin 0 -> 231164 bytes ...aditional-rose-tattoo-with-bold-colors.jpg | Bin 0 -> 151817 bytes public/united-logo-full.jpg | Bin 0 -> 452470 bytes public/united-logo-lettering.png | Bin 0 -> 663701 bytes public/united-logo-text.png | Bin 0 -> 663701 bytes public/united-logo-website.jpg | Bin 0 -> 452470 bytes public/united-studio-main.jpg | Bin 0 -> 273964 bytes ...watercolor-illustrative-tattoo-artwork.jpg | Bin 0 -> 107946 bytes styles/globals.css | 125 + tailwind.config.ts | 60 +- tsconfig.json | 1 + 194 files changed, 18635 insertions(+), 5264 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 app/ClientLayout.tsx create mode 100644 app/aftercare/page.tsx create mode 100644 app/artists/[id]/book/page.tsx create mode 100644 app/artists/[id]/page.tsx create mode 100644 app/artists/page.tsx create mode 100644 app/book/page.tsx create mode 100644 app/contact/page.tsx create mode 100644 app/deposit/page.tsx create mode 100644 app/gift-cards/page.tsx create mode 100644 app/privacy/page.tsx create mode 100644 app/specials/page.tsx create mode 100644 app/terms/page.tsx create mode 100644 components.json create mode 100644 components/aftercare-page.tsx create mode 100644 components/artist-portfolio.tsx create mode 100644 components/artists-grid.tsx create mode 100644 components/artists-page-section.tsx create mode 100644 components/artists-section.tsx create mode 100644 components/booking-form.tsx create mode 100644 components/contact-modal.tsx create mode 100644 components/contact-page.tsx create mode 100644 components/contact-section.tsx create mode 100644 components/deposit-page.tsx create mode 100644 components/footer.tsx create mode 100644 components/gift-card-balance-checker.tsx create mode 100644 components/gift-cards-page.tsx create mode 100644 components/hero-section.tsx create mode 100644 components/navigation.tsx create mode 100644 components/privacy-page.tsx create mode 100644 components/scroll-progress.tsx create mode 100644 components/scroll-to-section.tsx create mode 100644 components/section-header.tsx create mode 100644 components/services-section.tsx create mode 100644 components/smooth-scroll-provider.tsx create mode 100644 components/specials-page.tsx create mode 100644 components/terms-page.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/ui/use-mobile.tsx create mode 100644 components/ui/use-toast.ts create mode 100755 copy-artist-images.sh create mode 100644 data/artists.ts create mode 100644 hooks/use-mobile.ts create mode 100644 hooks/use-toast.ts create mode 100644 lib/utils.ts create mode 100644 pnpm-lock.yaml create mode 100644 public/abstract-geometric-shapes.png create mode 100644 public/american-traditional-anchor-tattoo.jpg create mode 100644 public/artists/amari-rodriguez-portrait.jpg create mode 100644 public/artists/amari-rodriguez-work-1.jpg create mode 100644 public/artists/amari-rodriguez-work-2.jpg create mode 100644 public/artists/amari-rodriguez-work-3.jpg create mode 100644 public/artists/angel-andrade-portrait.jpg create mode 100644 public/artists/angel-andrade-work-1.jpg create mode 100644 public/artists/angel-andrade-work-2.jpg create mode 100644 public/artists/angel-andrade-work-3.jpg create mode 100644 public/artists/angel-andrade-work-4.jpg create mode 100644 public/artists/christy-lumberg-portrait.jpg create mode 100644 public/artists/christy-lumberg-work-1.jpg create mode 100644 public/artists/christy-lumberg-work-2.jpg create mode 100644 public/artists/christy-lumberg-work-3.jpg create mode 100644 public/artists/christy-lumberg-work-4.jpg create mode 100644 public/artists/donovan-lankford-portrait.jpg create mode 100644 public/artists/ej-segoviano-portrait.jpg create mode 100644 public/artists/ej-segoviano-work-1.jpg create mode 100644 public/artists/ej-segoviano-work-2.jpg create mode 100644 public/artists/ej-segoviano-work-3.jpg create mode 100644 public/artists/pako-martinez-portrait.jpg create mode 100644 public/artists/pako-martinez-work-1.jpg create mode 100644 public/artists/pako-martinez-work-2.jpg create mode 100644 public/artists/pako-martinez-work-3.jpg create mode 100644 public/biomechanical-scifi-tattoo-artwork.jpg create mode 100644 public/black-and-grey-portrait-tattoo-masterpiece.jpg create mode 100644 public/blackwork-tribal-tattoo-artwork.jpg create mode 100644 public/botanical-nature-tattoo-artwork.jpg create mode 100644 public/colorful-traditional-bird-tattoo.jpg create mode 100644 public/delicate-fine-line-flower-tattoo.jpg create mode 100644 public/fine-line-botanical-tattoo-elegant.jpg create mode 100644 public/fine-line-minimalist-tattoo-artwork.jpg create mode 100644 public/geometric-abstract-tattoo-artwork.jpg create mode 100644 public/hyperrealistic-eye-tattoo-design.jpg create mode 100644 public/japanese-koi-fish-tattoo-colorful.jpg create mode 100644 public/japanese-oriental-tattoo-artwork.jpg create mode 100644 public/japanese-samurai-tattoo-traditional.jpg create mode 100644 public/minimalist-geometric-tattoo-design.jpg create mode 100644 public/minimalist-tattoo-design-on-arm.jpg create mode 100644 public/neo-traditional-wolf-tattoo-design.jpg create mode 100644 public/oriental-cherry-blossom-tattoo-design.jpg create mode 100644 public/photorealistic-portrait-tattoo-black-and-grey.jpg create mode 100644 public/placeholder-0h0qb.png create mode 100644 public/placeholder-882fw.png create mode 100644 public/placeholder-ah8n2.png create mode 100644 public/placeholder-e6fqm.png create mode 100644 public/placeholder-jk026.png create mode 100644 public/placeholder-jmey3.png create mode 100644 public/placeholder-ju7df.png create mode 100644 public/placeholder-lh3ki.png create mode 100644 public/placeholder-logo.png create mode 100644 public/placeholder-logo.svg create mode 100644 public/placeholder-mjx9t.png create mode 100644 public/placeholder-mykqu.png create mode 100644 public/placeholder-qrydh.png create mode 100644 public/placeholder-r6l7b.png create mode 100644 public/placeholder-s31fj.png create mode 100644 public/placeholder-s803z.png create mode 100644 public/placeholder-user.jpg create mode 100644 public/placeholder-xzjye.png create mode 100644 public/placeholder.jpg create mode 100644 public/placeholder.svg create mode 100644 public/professional-female-tattoo-artist-botanical-nature.jpg create mode 100644 public/professional-female-tattoo-artist-geometric-design.jpg create mode 100644 public/professional-female-tattoo-artist-watercolor-style.jpg create mode 100644 public/professional-female-tattoo-artist-with-delicate-fi.jpg create mode 100644 public/professional-female-tattoo-artist-with-traditional.jpg create mode 100644 public/professional-male-tattoo-artist-biomechanical-sci-.jpg create mode 100644 public/professional-male-tattoo-artist-blackwork-tribal.jpg create mode 100644 public/professional-male-tattoo-artist-specializing-in-re.jpg create mode 100644 public/professional-male-tattoo-artist-with-japanese-styl.jpg create mode 100644 public/professional-tattoo-artist-working-on-detailed-tat.jpg create mode 100644 public/realistic-animal-tattoo-detailed-shading.jpg create mode 100644 public/realistic-portrait-tattoo-artwork.jpg create mode 100644 public/simple-line-work-tattoo-artistic.jpg create mode 100644 public/tattoo-artist-workspace.jpg create mode 100644 public/tattoo-equipment-and-tools.jpg create mode 100644 public/traditional-japanese-dragon-tattoo-sleeve.jpg create mode 100644 public/traditional-neo-traditional-tattoo-artwork.jpg create mode 100644 public/traditional-rose-tattoo-with-bold-colors.jpg create mode 100644 public/united-logo-full.jpg create mode 100644 public/united-logo-lettering.png create mode 100644 public/united-logo-text.png create mode 100644 public/united-logo-website.jpg create mode 100644 public/united-studio-main.jpg create mode 100644 public/watercolor-illustrative-tattoo-artwork.jpg create mode 100644 styles/globals.css diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0f163201b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules +.pnpm-store + +# Build output +.next +out +dist + +# Logs +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* +*.log + +# Environment files +.env +.env.local +.env.* +.envrc + +# VCS +.git +.gitignore + +# Editor/misc +.vscode +.DS_Store +*.swp diff --git a/.gitignore b/.gitignore index fd3dbb571..c2dd114d8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,6 @@ # dependencies /node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage # next.js /.next/ @@ -16,17 +10,14 @@ # production /build -# misc -.DS_Store -*.pem - # debug npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* -# local env files -.env*.local +# env files +.env* # vercel .vercel @@ -34,3 +25,16 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# project temp and large binary assets (avoid committing raw media dumps) +temp/ +temp/** +*.mp4 +*.mov +*.avi +*.mkv +*.psd +*.ai +*.zip +*.7z +*.rar diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..cc0ac9ee5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +# 1) Install dependencies +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --legacy-peer-deps + +# 2) Build the Next.js app (standalone output is enabled in next.config.mjs) +FROM node:20-alpine AS builder +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +# 3) Production runner (small image using standalone output) +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +# Copy standalone server and static files +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/README.md b/README.md index e215bc4cc..74bd3ded0 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,125 @@ -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). +# United Tattoo — Official Website (Next.js + ShadCN UI) -## Getting Started +Hi, I’m Nicholai. I built this site for my friend Christy (aka Ink Mama) and the United Tattoo crew in Fountain, CO. The goal was simple: give the studio a site that actually reflects the art, the people, and the experience — not the stiff, generic stuff you usually see. This is also a thank you for everything Christy has done for Amari (my girlfriend and soulmate), who was her apprentice. So yeah, this is personal — and it shows. -First, run the development server: +This repo powers the official United Tattoo website, built with: +- Next.js App Router +- TypeScript +- Tailwind CSS +- ShadCN UI components (used across all pages) +- Lenis (smooth scroll) +- Lucide (icons) -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +Live dev server: http://localhost:3000 -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. +## Project Structure -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. +- app/ + - page.tsx — homepage (Hero, Artists, Services, Contact sections) + - aftercare/page.tsx — aftercare instructions (ShadCN-driven) + - deposit/page.tsx — deposit policy + payment options (Afterpay, Stripe) + - terms/page.tsx — terms of service + - privacy/page.tsx — privacy policy + - artists/ — artists listing + dynamic routes for profiles (coming from data) + - book/page.tsx — booking flow + - specials/page.tsx — promotions (monthly specials, VIP list) + - contact/page.tsx — contact + - gift-cards/page.tsx — gift card info -## Learn More +- components/ + - hero-section.tsx, artists-section.tsx, services-section.tsx, contact-section.tsx + - aftercare-page.tsx, deposit-page.tsx, terms-page.tsx, privacy-page.tsx + - booking-form.tsx — multi-step form using ShadCN components + - footer.tsx — contains direct links to Aftercare, Deposit Policy, Terms, and Privacy + - ui/ — ShadCN UI primitives -To learn more about Next.js, take a look at the following resources: +- data/ + - artists.ts — single source of truth for artist metadata and images used across pages -- [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. +- public/ + - united-logo-*.png/jpg, artists/, and other stable assets -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! -## Deploy on Vercel +## Content & Assets -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. +- All the “real” content, bios, and images are now wired in (not seed placeholders). +- Artist portraits and tattoo samples live under public/artists/. +- Pages like Aftercare, Deposit, Terms, and Privacy use consistent styling patterned after the homepage and portfolio pages — powered by ShadCN components. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +If you need to re-copy images from the temp folder into public (on your machine), there’s a helper script: +- ./copy-artist-images.sh +Note: This script expects the temp directory structure to exist locally; it silently skips if a source is missing. + + +## Getting Started (Local Dev) + +- Install deps: + - npm install + +- Run dev server: + - npm run dev + - Open http://localhost:3000 + +- Lint (optional): + - npm run lint + +Build: +- npm run build +- npm start + + +## Docker + +This repo is docker-ready. We build a standalone Next.js app for a smaller runtime image. + +Build image: +- docker build -t united-tattoo:latest . + +Run container (port 3000): +- docker run --rm -p 3000:3000 -e PORT=3000 united-tattoo:latest +- Open http://localhost:3000 + +Notes: +- next.config.mjs sets output: "standalone" +- The Dockerfile copies .next/standalone + .next/static and runs the server with HOSTNAME=0.0.0.0 + + +## Pages Overview + +- Home — Bold, high-contrast, split imagery, parallax accents. This sets the identity. +- Artists — Grid and profile surfaces wired to data/artists.ts. Each artist shows image, specialties, and sample work. +- Aftercare — Two flows: General Aftercare and Transparent Bandage Aftercare (accurate, readable, ShadCN cards + alerts). +- Deposit — Clear policy, payment options, and compliance notes (LW2 Investments, LLC oversight). +- Terms & Privacy — Straightforward, legally sound, human-readable. Both accessible from the footer. +- Booking — Multi-step form with ShadCN components and validation-friendly structure. +- Specials — Marketing surface for time-bound promotions and membership-like advantages. + + +## Design Language + +- ShadCN components everywhere possible +- Monochrome foundation with high contrast and cinematic image splits +- Type scales + spacing match the homepage/portfolio feeling +- Lucide icons for affordances + + +## Tech Notes + +- TypeScript errors are ignored during build in CI to allow non-blocking content/design iteration (next.config.mjs). +- Images are unoptimized (no Next image loader), served statically; change if you plan to put this behind a CDN with transforms. +- Smooth scroll and parallax-style offsets are kept subtle to let the work shine. + + +## Deployment + +- Standard Next.js deploys work (Vercel, Node server, Docker) +- For self-hosting or VPS, use the Dockerfile in the repo +- The site runs on port 3000 by default + + +## Why This Exists + +Because Christy deserved a proper site — and because the previous one was, bluntly, not it. United Tattoo is more than a shop. It’s a community with real people and real art. This site tries to honor that. + +— Nicholai diff --git a/app/ClientLayout.tsx b/app/ClientLayout.tsx new file mode 100644 index 000000000..926e074e7 --- /dev/null +++ b/app/ClientLayout.tsx @@ -0,0 +1,23 @@ +"use client" + +import type React from "react" +import { SmoothScrollProvider } from "@/components/smooth-scroll-provider" +import { useSearchParams } from "next/navigation" +import { Suspense } from "react" +import "./globals.css" + +export default function ClientLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + const searchParams = useSearchParams() + + return ( + <> + Loading...}> + {children} + + + ) +} diff --git a/app/aftercare/page.tsx b/app/aftercare/page.tsx new file mode 100644 index 000000000..07f600ad4 --- /dev/null +++ b/app/aftercare/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { AftercarePage } from "@/components/aftercare-page" +import { Footer } from "@/components/footer" + +export default function Aftercare() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/artists/[id]/book/page.tsx b/app/artists/[id]/book/page.tsx new file mode 100644 index 000000000..145aa88fe --- /dev/null +++ b/app/artists/[id]/book/page.tsx @@ -0,0 +1,21 @@ +import { Navigation } from "@/components/navigation" +import { BookingForm } from "@/components/booking-form" +import { Footer } from "@/components/footer" + +interface BookingPageProps { + params: { + id: string + } +} + +export default function BookingPage({ params }: BookingPageProps) { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/artists/[id]/page.tsx b/app/artists/[id]/page.tsx new file mode 100644 index 000000000..ceabca749 --- /dev/null +++ b/app/artists/[id]/page.tsx @@ -0,0 +1,21 @@ +import { Navigation } from "@/components/navigation" +import { ArtistPortfolio } from "@/components/artist-portfolio" +import { Footer } from "@/components/footer" + +interface ArtistPageProps { + params: { + id: string + } +} + +export default function ArtistPage({ params }: ArtistPageProps) { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/artists/page.tsx b/app/artists/page.tsx new file mode 100644 index 000000000..5481a0129 --- /dev/null +++ b/app/artists/page.tsx @@ -0,0 +1,13 @@ +import { Navigation } from "@/components/navigation" +import { ArtistsPageSection } from "@/components/artists-page-section" +import { Footer } from "@/components/footer" + +export default function ArtistsPage() { + return ( +
+ + +
+
+ ) +} diff --git a/app/book/page.tsx b/app/book/page.tsx new file mode 100644 index 000000000..5ba9ee960 --- /dev/null +++ b/app/book/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { BookingForm } from "@/components/booking-form" +import { Footer } from "@/components/footer" + +export default function BookPage() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/contact/page.tsx b/app/contact/page.tsx new file mode 100644 index 000000000..e1a73251b --- /dev/null +++ b/app/contact/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { ContactPage } from "@/components/contact-page" +import { Footer } from "@/components/footer" + +export default function Contact() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/deposit/page.tsx b/app/deposit/page.tsx new file mode 100644 index 000000000..ae42243d7 --- /dev/null +++ b/app/deposit/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { DepositPage } from "@/components/deposit-page" +import { Footer } from "@/components/footer" + +export default function Deposit() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/gift-cards/page.tsx b/app/gift-cards/page.tsx new file mode 100644 index 000000000..03c1b7d81 --- /dev/null +++ b/app/gift-cards/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { GiftCardsPage } from "@/components/gift-cards-page" +import { Footer } from "@/components/footer" + +export default function GiftCards() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/globals.css b/app/globals.css index 13d40b892..cc1d8727c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,27 +1,296 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); :root { - --background: #ffffff; - --foreground: #171717; + /* Updated color tokens to match United Tattoo design brief */ + --background: oklch(1 0 0); /* White */ + --foreground: oklch(0.145 0 0); /* Dark Slate Gray */ + --card: oklch(1 0 0); /* Light Gray */ + --card-foreground: oklch(0.145 0 0); /* Dark text for cards */ + --popover: oklch(1 0 0); /* White */ + --popover-foreground: oklch(0.145 0 0); /* Dark text */ + --primary: oklch(0.205 0 0); /* Emerald-600 #059669 */ + --primary-foreground: oklch(0.985 0 0); /* White text on primary */ + --secondary: oklch(0.97 0 0); /* Emerald accent #10b981 */ + --secondary-foreground: oklch(0.205 0 0); /* White text on secondary */ + --muted: oklch(0.97 0 0); /* Light Gray */ + --muted-foreground: oklch(0.556 0 0); /* Muted text */ + --accent: oklch(0.97 0 0); /* Emerald accent */ + --accent-foreground: oklch(0.205 0 0); /* White text on accent */ + --destructive: oklch(0.577 0.245 27.325); /* Red for destructive actions */ + --destructive-foreground: oklch(0.985 0 0); /* White text */ + --border: oklch(0.922 0 0); /* Light border */ + --input: oklch(0.922 0 0); /* Input background */ + --ring: oklch(0.708 0 0); /* Focus ring */ + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; -} - -@layer utilities { - .text-balance { - text-wrap: balance; +.dark { + --background: oklch(0.145 0 0); /* Very dark background */ + --foreground: oklch(0.985 0 0); /* Light text */ + --card: oklch(0.205 0 0); /* Dark card background */ + --card-foreground: oklch(0.985 0 0); /* Light text on cards */ + --popover: oklch(0.269 0 0); /* Dark popover */ + --popover-foreground: oklch(0.985 0 0); /* Light text */ + --primary: oklch(0.922 0 0); /* Brighter emerald for dark mode */ + --primary-foreground: oklch(0.205 0 0); /* Dark text on primary */ + --secondary: oklch(0.269 0 0); /* Dark secondary */ + --secondary-foreground: oklch(0.985 0 0); /* Light text */ + --muted: oklch(0.269 0 0); /* Dark muted */ + --muted-foreground: oklch(0.708 0 0); /* Muted text */ + --accent: oklch(0.371 0 0); /* Dark accent */ + --accent-foreground: oklch(0.985 0 0); /* Light text */ + --destructive: oklch(0.704 0.191 22.216); /* Darker red */ + --destructive-foreground: oklch(0.985 0 0); /* Lighter red text */ + --border: oklch(1 0 0 / 10%); /* Dark border */ + --input: oklch(1 0 0 / 15%); /* Dark input */ + --ring: oklch(0.556 0 0); /* Dark focus ring */ + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + + /* Added Lenis smooth scrolling styles */ + html.lenis, + html.lenis body { + height: auto; + } + + .lenis.lenis-smooth { + scroll-behavior: auto !important; + } + + .lenis.lenis-smooth [data-lenis-prevent] { + overscroll-behavior: contain; + } + + .lenis.lenis-stopped { + overflow: hidden; + } + + .lenis.lenis-scrolling iframe { + pointer-events: none; + } + + /* Added smooth scrolling and custom animations */ + html { + scroll-behavior: smooth; + } + + /* Custom scroll animations */ + @keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes fade-in-left { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + @keyframes fade-in-right { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + @keyframes scale-in { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } + } + + .animate-fade-in-up { + animation: fade-in-up 0.6s ease-out forwards; + } + + .animate-fade-in-left { + animation: fade-in-left 0.6s ease-out forwards; + } + + .animate-fade-in-right { + animation: fade-in-right 0.6s ease-out forwards; + } + + .animate-scale-in { + animation: scale-in 0.6s ease-out forwards; + } + + /* Scroll-triggered animations */ + .scroll-animate { + opacity: 0; + transform: translateY(20px); + transition: all 0.6s ease-out; + } + + .scroll-animate.visible { + opacity: 1; + transform: translateY(0); + } + + /* Enhanced scrollbar styling */ + ::-webkit-scrollbar { + width: 8px; + } + + ::-webkit-scrollbar-track { + background: var(--muted); + } + + ::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 4px; + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--primary); + opacity: 0.8; + } + + /* Smooth transitions for all interactive elements */ + button, + a, + input, + textarea { + transition: all 0.2s ease-in-out; + } + + /* Adding marquee animation for scrolling reviews */ + @keyframes marquee { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-100%); + } + } + + .animate-marquee { + animation: marquee 30s linear infinite; + } + + .animate-marquee:hover { + animation-play-state: paused; + } + + /* Enhanced marquee animation with smooth transitions */ + .animate-marquee-smooth { + animation: marquee 40s linear infinite; + transition: animation-play-state 0.5s ease-in-out; + } + + .animate-marquee-smooth:hover { + animation-play-state: paused; + } + + /* Enhanced hover pause with smooth transitions */ + .hover\:pause:hover { + animation-play-state: paused !important; + } + + .hover\:pause-smooth:hover { + animation-play-state: paused !important; + transition: all 0.5s ease-in-out; + } + + /* Adding radial gradient utility for spotlight effect */ + .bg-gradient-radial { + background: radial-gradient(circle, var(--tw-gradient-stops)); } } diff --git a/app/layout.tsx b/app/layout.tsx index a36cde01c..1d8f4c324 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,35 +1,37 @@ -import type { Metadata } from "next"; -import localFont from "next/font/local"; -import "./globals.css"; +import type React from "react" +import type { Metadata } from "next" +import { Playfair_Display, Source_Sans_3 } from "next/font/google" +import "./globals.css" +import ClientLayout from "./ClientLayout" -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", -}); +const playfairDisplay = Playfair_Display({ + subsets: ["latin"], + variable: "--font-playfair", + display: "swap", +}) + +const sourceSans = Source_Sans_3({ + subsets: ["latin"], + variable: "--font-source-sans", + display: "swap", +}) export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; + title: "United Tattoo - Professional Tattoo Studio", + description: "Book appointments with our talented artists and explore stunning tattoo portfolios at United Tattoo.", + generator: "v0.app", +} export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode }>) { return ( - - - {children} + + + {children} - ); + ) } diff --git a/app/page.tsx b/app/page.tsx index 433c8aa7f..dc7b9be2e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,101 +1,31 @@ -import Image from "next/image"; +import { Navigation } from "@/components/navigation" +import { ScrollProgress } from "@/components/scroll-progress" +import { ScrollToSection } from "@/components/scroll-to-section" +import { HeroSection } from "@/components/hero-section" +import { ArtistsSection } from "@/components/artists-section" +import { ServicesSection } from "@/components/services-section" +import { ContactSection } from "@/components/contact-section" +import { Footer } from "@/components/footer" -export default function Home() { +export default function HomePage() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- -
- ); +
+ + + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ) } diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx new file mode 100644 index 000000000..cd027d480 --- /dev/null +++ b/app/privacy/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { PrivacyPage } from "@/components/privacy-page" +import { Footer } from "@/components/footer" + +export default function Privacy() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/specials/page.tsx b/app/specials/page.tsx new file mode 100644 index 000000000..4ba403b75 --- /dev/null +++ b/app/specials/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { SpecialsPage } from "@/components/specials-page" +import { Footer } from "@/components/footer" + +export default function Specials() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/app/terms/page.tsx b/app/terms/page.tsx new file mode 100644 index 000000000..e8af9e383 --- /dev/null +++ b/app/terms/page.tsx @@ -0,0 +1,15 @@ +import { Navigation } from "@/components/navigation" +import { TermsPage } from "@/components/terms-page" +import { Footer } from "@/components/footer" + +export default function Terms() { + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 000000000..4ee62ee10 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/aftercare-page.tsx b/components/aftercare-page.tsx new file mode 100644 index 000000000..6cd1d116e --- /dev/null +++ b/components/aftercare-page.tsx @@ -0,0 +1,352 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { + CheckCircle, + Clock, + Shield, + AlertTriangle, + Droplets, + Phone, + Mail, + Heart, +} from "lucide-react" +import Link from "next/link" + +type Phase = { + phase: string + icon: any + color: string + bgColor: string + steps: string[] +} + +const generalAftercare: Record = { + immediate: { + phase: "Immediate Aftercare", + icon: Clock, + color: "text-red-400", + bgColor: "bg-red-950/20 border-red-900/30", + steps: [ + "Keep the bandage or dressing on for 1 to 4 hours to prevent exposure to airborne bacteria.", + "Wash your hands thoroughly before removing the bandage.", + "Remove the bandage gently and cleanse your tattoo using lukewarm water and mild, unscented antibacterial soap.", + "Pat dry with a clean paper towel — never touch your tattoo unless you have just washed your hands.", + "Apply a very light layer of the recommended aftercare product or fragrance-free lotion.", + ], + }, + general: { + phase: "General Aftercare", + icon: Shield, + color: "text-yellow-400", + bgColor: "bg-yellow-950/20 border-yellow-900/30", + steps: [ + "Cleanse your tattoo multiple times a day with lukewarm water and antibacterial soap.", + "Apply a thin layer of ointment or lotion to keep your tattoo moisturized.", + "After the first few days, transition to a non-scented lotion.", + "Avoid wearing tight clothing over your tattoo.", + "Avoid immersing your tattoo in pools, oceans, lakes, or hot tubs for 2–4 weeks.", + "Minimize activities that lead to excessive sweating and sun exposure.", + "Do not pick, peel, or scratch scabbing or hardened layers.", + ], + }, + longterm: { + phase: "Long-term Aftercare", + icon: Heart, + color: "text-green-400", + bgColor: "bg-green-950/20 border-green-900/30", + steps: [ + "Always use a minimum of SPF 30 sunblock to protect your tattoo from UV rays.", + "Keep your tattoos well-moisturized, especially in areas prone to fading (hands, feet, knees, elbows).", + "The outermost layer of skin typically takes 2–3 weeks to heal.", + "Complete healing may take up to 6 months.", + "Ongoing care will contribute to the longevity and vibrancy of your tattoo.", + ], + }, +} + +const transparentBandage: Record = { + removal: { + phase: "Bandage Removal", + icon: Droplets, + color: "text-blue-400", + bgColor: "bg-blue-950/20 border-blue-900/30", + steps: [ + "Remove bandage in the shower for added comfort — running water helps adhesive detachment.", + "Peel back in the direction of hair growth.", + "Wash hands before handling your tattoo.", + "Cleanse with lukewarm water and mild antibacterial soap multiple times a day.", + "If the tattoo feels slippery, carefully remove excess plasma to avoid scab formation.", + "Air dry or gently pat with a paper towel.", + ], + }, + reapply: { + phase: "Bandage Reapplication (If Advised)", + icon: Shield, + color: "text-purple-400", + bgColor: "bg-purple-950/20 border-purple-900/30", + steps: [ + "DO NOT apply ointments or lotions unless directed by your artist.", + "Apply the bandage only to the tattoo, avoiding surrounding skin.", + "Cut and trim to fit with ~1 inch around all sides (rounded edges adhere better).", + "Keep the new bandage on for 3–6 days unless your artist advises otherwise.", + "Remove earlier if irritation, fluid buildup, or loosening occurs.", + "Avoid reapplying once the tattoo enters the scabbing or flaking phase.", + ], + }, +} + +const infectionWarning = [ + "Increased redness or swelling that spreads beyond the tattoo", + "Pain when touching the tattoo or a throbbing sensation", + "Sensation of heat from the tattoo area", + "Yellow or green discharge with offensive odor", + "Fever or chills", + "Red streaking from the tattoo", + "Excessive swelling after the first day", + "Signs of allergic reaction", +] + +export function AftercarePage() { + const [tab, setTab] = useState<"general" | "transparent">("general") + + return ( +
+ {/* Hero / Header */} +
+
+ +
+ +
+
+

+ Tattoo Aftercare +

+

+ Proper aftercare is crucial for the healing and longevity of your new tattoo. Follow these + instructions carefully to ensure the best results. +

+
+
+
+ + {/* Licensing Notice */} +
+
+ + + + United Tattoo is proudly licensed by the El Paso County Health Department and fully supports + health department regulations to protect the health of our customers. + + +
+
+ + {/* Tabs: General vs Transparent Bandage */} +
+
+ setTab(v as any)} className="w-full"> + + + General Tattoo Aftercare + + + Transparent Bandage Aftercare + + + + {/* General Aftercare */} + +
+ {Object.values(generalAftercare).map((phase, idx) => { + const Icon = phase.icon + return ( + + + + + {phase.phase} + + + +
    + {phase.steps.map((s, i) => ( +
  • + + {s} +
  • + ))} +
+
+
+ ) + })} +
+
+ + {/* Transparent Bandage */} + +
+ {Object.values(transparentBandage).map((phase, idx) => { + const Icon = phase.icon + return ( + + + + + {phase.phase} + + + +
    + {phase.steps.map((s, i) => ( +
  • + + {s} +
  • + ))} +
+
+
+ ) + })} +
+
+
+
+
+ + {/* Infection Warning */} +
+
+ + + + + Signs of Infection — Seek Medical Attention + + + +
+ {infectionWarning.map((sign, i) => ( +
+ + {sign} +
+ ))} +
+ + + + Important + + If you experience any of these symptoms, contact our studio immediately at{" "} + + (719) 698-9004 + {" "} + or seek urgent medical attention. + + +
+
+
+
+ + {/* Healing Timeline */} +
+
+
+ + + Surface Healing + + +

2–3 Weeks

+

+ The outermost layer of skin typically heals in 2–3 weeks. Continue following aftercare during this time. +

+
+
+ + + + Deep Healing + + +

2–4 Months

+

+ Deeper layers of skin continue healing. Maintain a consistent moisturizing routine. +

+
+
+ + + + Complete Healing + + +

Up to 6 Months

+

+ Full healing may take up to 6 months. Protect with SPF and keep moisturized. +

+
+
+
+
+
+ + {/* Contact / Help */} +
+
+ + +

Questions?

+

+ Reach out if you have any aftercare questions or concerns. We’re here to help. +

+
+ + +
+
+
+
+
+
+ ) +} diff --git a/components/artist-portfolio.tsx b/components/artist-portfolio.tsx new file mode 100644 index 000000000..6ff2293cf --- /dev/null +++ b/components/artist-portfolio.tsx @@ -0,0 +1,413 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import Link from "next/link" +import { ArrowLeft, Star, MapPin, Calendar, Instagram, ExternalLink } from "lucide-react" + +// Mock data - in a real app, this would come from a database +const artistsData = { + "1": { + id: "1", + name: "Sarah Chen", + specialty: "Traditional & Neo-Traditional", + image: "/professional-female-tattoo-artist-with-traditional.jpg", + bio: "Specializing in bold traditional designs with a modern twist. Sarah brings 8 years of experience creating vibrant, timeless tattoos that honor the classic American traditional style while incorporating contemporary elements.", + experience: "8 years", + rating: 4.9, + reviews: 127, + location: "Studio A", + availability: "Available", + styles: ["Traditional", "Neo-Traditional", "American Traditional", "Color Work"], + instagram: "@sarahchen_tattoo", + portfolio: [ + { + id: 1, + image: "/traditional-rose-tattoo-with-bold-colors.jpg", + title: "Traditional Rose", + category: "Traditional", + }, + { + id: 2, + image: "/neo-traditional-wolf-tattoo-design.jpg", + title: "Neo-Traditional Wolf", + category: "Neo-Traditional", + }, + { + id: 3, + image: "/american-traditional-anchor-tattoo.jpg", + title: "American Traditional Anchor", + category: "Traditional", + }, + { id: 4, image: "/colorful-traditional-bird-tattoo.jpg", title: "Traditional Bird", category: "Color Work" }, + { id: 5, image: "/placeholder-jmey3.png", title: "Traditional Eagle", category: "Traditional" }, + { id: 6, image: "/placeholder-ah8n2.png", title: "Neo-Traditional Snake", category: "Neo-Traditional" }, + { id: 7, image: "/placeholder-s803z.png", title: "Traditional Panther", category: "Color Work" }, + { id: 8, image: "/placeholder-e6fqm.png", title: "Traditional Ship", category: "Traditional" }, + { id: 9, image: "/placeholder-qrydh.png", title: "Neo-Traditional Fox", category: "Neo-Traditional" }, + { id: 10, image: "/placeholder-s31fj.png", title: "Traditional Dagger", category: "Traditional" }, + { id: 11, image: "/placeholder-xzjye.png", title: "Traditional Butterfly", category: "Color Work" }, + { id: 12, image: "/placeholder-mjx9t.png", title: "Neo-Traditional Deer", category: "Neo-Traditional" }, + { id: 13, image: "/placeholder-882fw.png", title: "Traditional Skull", category: "Traditional" }, + { id: 14, image: "/placeholder-0h0qb.png", title: "Traditional Lighthouse", category: "Traditional" }, + { id: 15, image: "/placeholder-mykqu.png", title: "Neo-Traditional Octopus", category: "Neo-Traditional" }, + { id: 16, image: "/placeholder-jk026.png", title: "Traditional Tiger", category: "Color Work" }, + { id: 17, image: "/placeholder-ju7df.png", title: "Traditional Swallow", category: "Traditional" }, + { id: 18, image: "/placeholder-r6l7b.png", title: "Neo-Traditional Moon", category: "Neo-Traditional" }, + { id: 19, image: "/placeholder-lh3ki.png", title: "Traditional Heart", category: "Traditional" }, + { id: 20, image: "/placeholder.svg?height=400&width=300", title: "Traditional Koi", category: "Color Work" }, + ], + testimonials: [ + { + name: "Jessica M.", + rating: 5, + text: "Sarah created the most beautiful traditional rose tattoo for me. Her attention to detail and color work is incredible!", + }, + { + name: "Mike R.", + rating: 5, + text: "Amazing artist! The neo-traditional piece she did exceeded all my expectations. Highly recommend!", + }, + ], + }, + // Add other artists data here... +} + +interface ArtistPortfolioProps { + artistId: string +} + +export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) { + const [selectedCategory, setSelectedCategory] = useState("All") + const [selectedImage, setSelectedImage] = useState(null) + const [scrollY, setScrollY] = useState(0) + + const artist = artistsData[artistId as keyof typeof artistsData] + + useEffect(() => { + const handleScroll = () => setScrollY(window.scrollY) + window.addEventListener("scroll", handleScroll) + return () => window.removeEventListener("scroll", handleScroll) + }, []) + + if (!artist) { + return ( +
+

Artist not found

+ +
+ ) + } + + const categories = ["All", ...Array.from(new Set(artist.portfolio.map((item) => item.category)))] + const filteredPortfolio = + selectedCategory === "All" + ? artist.portfolio + : artist.portfolio.filter((item) => item.category === selectedCategory) + + return ( +
+ {/* Back Button */} +
+ +
+ + {/* Hero Section with Split Screen */} +
+ {/* Left Side - Artist Image */} +
+
+ {artist.name} +
+
+ + {artist.availability} + +
+
+
+ + {/* Right Side - Artist Info */} +
+
+
+

{artist.name}

+

{artist.specialty}

+
+ + {artist.rating} + ({artist.reviews} reviews) +
+
+ +

{artist.bio}

+ +
+
+ + {artist.experience} experience +
+
+ + {artist.location} +
+
+ + {artist.instagram} +
+
+ +
+

Specializes in:

+
+ {artist.styles.map((style) => ( + + {style} + + ))} +
+
+ +
+ + +
+
+
+ + {/* Curved Border */} +
+ + + +
+
+ + {/* Portfolio Section with Split Screen Layout */} +
+
+ {/* Left Side - Portfolio Grid */} +
+
+ {filteredPortfolio.map((item, index) => ( +
setSelectedImage(item.id)}> +
+ {item.title} +
+
+ +

{item.title}

+
+
+
+
+ ))} +
+
+ + {/* Right Side - Sticky Header and Info */} +
+
+
+

Featured Work

+ {filteredPortfolio.length} +
+ +
+ + +

+ Explore {artist.name}'s portfolio showcasing {artist.experience} of expertise in{" "} + {artist.specialty.toLowerCase()}. Each piece represents a unique collaboration between artist and + client. +

+
+ + {/* Category Filter */} +
+

Filter by Style

+
+ {categories.map((category) => ( + + ))} +
+
+ + {/* Quick Stats */} +
+
+
+
{artist.portfolio.length}
+
Pieces
+
+
+
{artist.rating}
+
Rating
+
+
+
+
+
+
+
+ + {/* Reviews Section */} +
+
+
+

What Clients Say

+
+
+
+ +
+
+ {/* Duplicate testimonials for seamless loop */} + {[...artist.testimonials, ...artist.testimonials, ...artist.testimonials, ...artist.testimonials].map( + (testimonial, index) => ( +
+ {/* Enhanced spotlight background with stronger separation */} +
+
+
+
+
+ {[...Array(testimonial.rating)].map((_, i) => ( + + ))} +
+
+ "{testimonial.text}" +
+ — {testimonial.name} +
+
+
+ ), + )} +
+
+
+ + {/* Contact Section */} +
+
+
+

Ready to Get Started?

+

+ Book a consultation with {artist.name} to discuss your next tattoo. Whether you're looking for a + traditional piece or something with a modern twist, let's bring your vision to life. +

+ +
+ + +
+ +
+
+
+
{artist.experience}
+
Experience
+
+
+
{artist.reviews}+
+
Happy Clients
+
+
+
{artist.rating}/5
+
Average Rating
+
+
+
+
+
+
+ + {/* Image Modal */} + {selectedImage && ( +
setSelectedImage(null)} + > +
+ item.id === selectedImage)?.image || "/placeholder.svg"} + alt="Portfolio piece" + className="max-w-full max-h-full object-contain" + /> + +
+
+ )} +
+ ) +} diff --git a/components/artists-grid.tsx b/components/artists-grid.tsx new file mode 100644 index 000000000..a85f114ff --- /dev/null +++ b/components/artists-grid.tsx @@ -0,0 +1,210 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import Link from "next/link" +import { Star, MapPin, Calendar } from "lucide-react" + +const artists = [ + { + id: "1", + name: "Sarah Chen", + specialty: "Traditional & Neo-Traditional", + image: "/professional-female-tattoo-artist-with-traditional.jpg", + bio: "Specializing in bold traditional designs with a modern twist. Sarah brings 8 years of experience creating vibrant, timeless tattoos.", + experience: "8 years", + rating: 4.9, + reviews: 127, + location: "Studio A", + availability: "Available", + styles: ["Traditional", "Neo-Traditional", "American Traditional", "Color Work"], + portfolio: [ + "/traditional-rose-tattoo-with-bold-colors.jpg", + "/neo-traditional-wolf-tattoo-design.jpg", + "/american-traditional-anchor-tattoo.jpg", + "/colorful-traditional-bird-tattoo.jpg", + ], + }, + { + id: "2", + name: "Marcus Rodriguez", + specialty: "Realism & Portraits", + image: "/professional-male-tattoo-artist-specializing-in-re.jpg", + bio: "Master of photorealistic tattoos and detailed portrait work. Marcus has perfected the art of bringing photographs to life on skin.", + experience: "12 years", + rating: 5.0, + reviews: 89, + location: "Studio B", + availability: "Booked until March", + styles: ["Realism", "Portraits", "Black & Grey", "Photorealism"], + portfolio: [ + "/photorealistic-portrait-tattoo-black-and-grey.jpg", + "/realistic-animal-tattoo-detailed-shading.jpg", + "/black-and-grey-portrait-tattoo-masterpiece.jpg", + "/hyperrealistic-eye-tattoo-design.jpg", + ], + }, + { + id: "3", + name: "Luna Kim", + specialty: "Fine Line & Minimalist", + image: "/professional-female-tattoo-artist-with-delicate-fi.jpg", + bio: "Creating elegant, minimalist designs with precision and grace. Luna's delicate touch brings subtle beauty to every piece.", + experience: "6 years", + rating: 4.8, + reviews: 156, + location: "Studio C", + availability: "Available", + styles: ["Fine Line", "Minimalist", "Geometric", "Botanical"], + portfolio: [ + "/delicate-fine-line-flower-tattoo.jpg", + "/minimalist-geometric-tattoo-design.jpg", + "/fine-line-botanical-tattoo-elegant.jpg", + "/simple-line-work-tattoo-artistic.jpg", + ], + }, + { + id: "4", + name: "Jake Thompson", + specialty: "Japanese & Oriental", + image: "/professional-male-tattoo-artist-with-japanese-styl.jpg", + bio: "Traditional Japanese tattooing with authentic techniques passed down through generations. Jake honors the ancient art form.", + experience: "15 years", + rating: 4.9, + reviews: 203, + location: "Studio D", + availability: "Limited slots", + styles: ["Japanese", "Oriental", "Irezumi", "Traditional Japanese"], + portfolio: [ + "/traditional-japanese-dragon-tattoo-sleeve.jpg", + "/japanese-koi-fish-tattoo-colorful.jpg", + "/oriental-cherry-blossom-tattoo-design.jpg", + "/japanese-samurai-tattoo-traditional.jpg", + ], + }, +] + +const specialties = ["All", "Traditional", "Realism", "Fine Line", "Japanese", "Portraits", "Minimalist"] + +export function ArtistsGrid() { + const [selectedSpecialty, setSelectedSpecialty] = useState("All") + + const filteredArtists = + selectedSpecialty === "All" + ? artists + : artists.filter((artist) => + artist.styles.some((style) => style.toLowerCase().includes(selectedSpecialty.toLowerCase())), + ) + + return ( +
+
+
+

Our Artists

+

+ Meet our talented team of tattoo artists, each bringing their unique style and years of experience to create + your perfect tattoo. +

+
+ + {/* Filter Buttons */} +
+ {specialties.map((specialty) => ( + + ))} +
+ + {/* Artists Grid */} +
+ {filteredArtists.map((artist) => ( + +
+ {/* Artist Image */} +
+ {artist.name} +
+ + {artist.availability} + +
+
+ + {/* Artist Info */} + +
+
+

{artist.name}

+

{artist.specialty}

+
+
+ + {artist.rating} + ({artist.reviews}) +
+
+ +

{artist.bio}

+ +
+
+ + {artist.experience} experience +
+
+ + {artist.location} +
+
+ + {/* Styles */} +
+

Specializes in:

+
+ {artist.styles.slice(0, 3).map((style) => ( + + {style} + + ))} + {artist.styles.length > 3 && ( + + +{artist.styles.length - 3} more + + )} +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ ))} +
+
+
+ ) +} diff --git a/components/artists-page-section.tsx b/components/artists-page-section.tsx new file mode 100644 index 000000000..f0c3f5ebf --- /dev/null +++ b/components/artists-page-section.tsx @@ -0,0 +1,432 @@ +"use client" + +import { useState, useEffect, useRef } from "react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import Link from "next/link" +import { artists } from "@/data/artists" + +const specialties = [ + "All", + "Traditional", + "Realism", + "Fine Line", + "Japanese", + "Geometric", + "Blackwork", + "Watercolor", + "Illustrative", + "Cover-ups", + "Neo-Traditional", + "Anime", +] + +export function ArtistsPageSection() { + const [selectedSpecialty, setSelectedSpecialty] = useState("All") + const [visibleCards, setVisibleCards] = useState([]) + const [scrollY, setScrollY] = useState(0) + const sectionRef = useRef(null) + const leftColumnRef = useRef(null) + const centerColumnRef = useRef(null) + const rightColumnRef = useRef(null) + + const filteredArtists = + selectedSpecialty === "All" + ? artists + : artists.filter((artist) => + artist.styles.some((style) => style.toLowerCase().includes(selectedSpecialty.toLowerCase())), + ) + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const cardIndex = Number.parseInt(entry.target.getAttribute("data-index") || "0") + setVisibleCards((prev) => [...new Set([...prev, cardIndex])]) + } + }) + }, + { threshold: 0.1, rootMargin: "0px 0px -50px 0px" }, + ) + + const cards = sectionRef.current?.querySelectorAll("[data-index]") + cards?.forEach((card) => observer.observe(card)) + + return () => observer.disconnect() + }, [filteredArtists]) + + useEffect(() => { + let ticking = false + + const handleScroll = () => { + if (!ticking) { + requestAnimationFrame(() => { + const scrollTop = window.pageYOffset + setScrollY(scrollTop) + ticking = false + }) + ticking = true + } + } + + window.addEventListener("scroll", handleScroll, { passive: true }) + return () => window.removeEventListener("scroll", handleScroll) + }, []) + + useEffect(() => { + if (leftColumnRef.current && centerColumnRef.current && rightColumnRef.current) { + const sectionTop = sectionRef.current?.offsetTop || 0 + const relativeScroll = scrollY - sectionTop + + leftColumnRef.current.style.transform = `translateY(${relativeScroll * -0.05}px)` + centerColumnRef.current.style.transform = `translateY(0px)` + rightColumnRef.current.style.transform = `translateY(${relativeScroll * 0.05}px)` + + const leftImages = leftColumnRef.current.querySelectorAll(".artist-image") + const centerImages = centerColumnRef.current.querySelectorAll(".artist-image") + const rightImages = rightColumnRef.current.querySelectorAll(".artist-image") + + leftImages.forEach((img) => { + ;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.02}px)` + }) + centerImages.forEach((img) => { + ;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.015}px)` + }) + rightImages.forEach((img) => { + ;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.01}px)` + }) + } + }, [scrollY]) + + const leftColumn = filteredArtists.filter((_, index) => index % 3 === 0) + const centerColumn = filteredArtists.filter((_, index) => index % 3 === 1) + const rightColumn = filteredArtists.filter((_, index) => index % 3 === 2) + + return ( +
+ {/* Background */} +
+ +
+
+ + {/* Header */} +
+
+
+
+

OUR ARTISTS

+

+ Meet our exceptional team of tattoo artists, each bringing unique expertise and artistic vision to + create your perfect tattoo. +

+
+
+ +
+
+ + {/* Filter Buttons */} +
+ {specialties.map((specialty) => ( + + ))} +
+
+
+ + {/* Artists Grid with Parallax */} +
+
+
+
+ {leftColumn.map((artist, index) => ( +
+
+
+
+ {`${artist.name} +
+ +
+ {`${artist.name} +
+
+ +
+
+ + {artist.experience} + + + {artist.availability} + +
+ +
+

{artist.name}

+

{artist.specialty}

+

{artist.bio}

+ +
+ + ★ {artist.rating} ({artist.reviews} reviews) + +
+ +
+ + +
+
+
+
+
+ ))} +
+ +
+ {centerColumn.map((artist, index) => ( +
+
+
+
+ {`${artist.name} +
+ +
+ {`${artist.name} +
+
+ +
+
+ + {artist.experience} + + + {artist.availability} + +
+ +
+

{artist.name}

+

{artist.specialty}

+

{artist.bio}

+ +
+ + ★ {artist.rating} ({artist.reviews} reviews) + +
+ +
+ + +
+
+
+
+
+ ))} +
+ +
+ {rightColumn.map((artist, index) => ( +
+
+
+
+ {`${artist.name} +
+ +
+ {`${artist.name} +
+
+ +
+
+ + {artist.experience} + + + {artist.availability} + +
+ +
+

{artist.name}

+

{artist.specialty}

+

{artist.bio}

+ +
+ + ★ {artist.rating} ({artist.reviews} reviews) + +
+ +
+ + +
+
+
+
+
+ ))} +
+
+
+
+ + {/* Call to Action */} +
+
+

READY?

+

+ Choose your artist and start your tattoo journey with United Tattoo. +

+ +
+
+
+ ) +} diff --git a/components/artists-section.tsx b/components/artists-section.tsx new file mode 100644 index 000000000..568065acc --- /dev/null +++ b/components/artists-section.tsx @@ -0,0 +1,341 @@ +"use client" + +import { useState, useEffect, useRef } from "react" +import { Button } from "@/components/ui/button" +import Link from "next/link" +import { artists } from "@/data/artists" + +export function ArtistsSection() { + const [visibleCards, setVisibleCards] = useState([]) + const [scrollY, setScrollY] = useState(0) + const sectionRef = useRef(null) + const leftColumnRef = useRef(null) + const centerColumnRef = useRef(null) + const rightColumnRef = useRef(null) + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const cardIndex = Number.parseInt(entry.target.getAttribute("data-index") || "0") + setVisibleCards((prev) => [...new Set([...prev, cardIndex])]) + } + }) + }, + { threshold: 0.1, rootMargin: "0px 0px -50px 0px" }, + ) + + const cards = sectionRef.current?.querySelectorAll("[data-index]") + cards?.forEach((card) => observer.observe(card)) + + return () => observer.disconnect() + }, []) + + useEffect(() => { + let ticking = false + + const handleScroll = () => { + if (!ticking) { + requestAnimationFrame(() => { + const scrollTop = window.pageYOffset + setScrollY(scrollTop) + ticking = false + }) + ticking = true + } + } + + window.addEventListener("scroll", handleScroll, { passive: true }) + return () => window.removeEventListener("scroll", handleScroll) + }, []) + + useEffect(() => { + if (leftColumnRef.current && centerColumnRef.current && rightColumnRef.current) { + const sectionTop = sectionRef.current?.offsetTop || 0 + const relativeScroll = scrollY - sectionTop + + leftColumnRef.current.style.transform = `translateY(${relativeScroll * -0.05}px)` + centerColumnRef.current.style.transform = `translateY(0px)` + rightColumnRef.current.style.transform = `translateY(${relativeScroll * 0.05}px)` + + const leftImages = leftColumnRef.current.querySelectorAll(".artist-image") + const centerImages = centerColumnRef.current.querySelectorAll(".artist-image") + const rightImages = rightColumnRef.current.querySelectorAll(".artist-image") + + leftImages.forEach((img) => { + ;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.02}px)` + }) + centerImages.forEach((img) => { + ;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.015}px)` + }) + rightImages.forEach((img) => { + ;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.01}px)` + }) + } + }, [scrollY]) + + const leftColumn = artists.filter((_, index) => index % 3 === 0) + const centerColumn = artists.filter((_, index) => index % 3 === 1) + const rightColumn = artists.filter((_, index) => index % 3 === 2) + + return ( +
+
+ +
+
+ +
+
+
+
+

ARTISTS

+

+ Our exceptional team of tattoo artists, each bringing unique expertise and artistic vision to create your perfect + tattoo. +

+
+
+ +
+
+
+
+ +
+
+
+
+ {leftColumn.map((artist, index) => ( +
+
+
+
+ {`${artist.name} +
+ +
+ {`${artist.name} +
+
+ +
+
+ + {artist.experience} + +
+ +
+

{artist.name}

+

{artist.specialty}

+

{artist.bio}

+ +
+ + +
+
+
+
+
+ ))} +
+ +
+ {centerColumn.map((artist, index) => ( +
+
+
+
+ {`${artist.name} +
+ +
+ {`${artist.name} +
+
+ +
+
+ + {artist.experience} + +
+ +
+

{artist.name}

+

{artist.specialty}

+

{artist.bio}

+ +
+ + +
+
+
+
+
+ ))} +
+ +
+ {rightColumn.map((artist, index) => ( +
+
+
+
+ {`${artist.name} +
+ +
+ {`${artist.name} +
+
+ +
+
+ + {artist.experience} + +
+ +
+

{artist.name}

+

{artist.specialty}

+

{artist.bio}

+ +
+ + +
+
+
+
+
+ ))} +
+
+
+
+ +
+
+

READY?

+

+ Choose your artist and start your tattoo journey with United Tattoo. +

+ +
+
+
+ ) +} diff --git a/components/booking-form.tsx b/components/booking-form.tsx new file mode 100644 index 000000000..b9504cfec --- /dev/null +++ b/components/booking-form.tsx @@ -0,0 +1,579 @@ +"use client" + +import type React from "react" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Checkbox } from "@/components/ui/checkbox" +import { Calendar } from "@/components/ui/calendar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { CalendarIcon, DollarSign, User, MessageSquare } from "lucide-react" +import { format } from "date-fns" +import Link from "next/link" +import { artists } from "@/data/artists" + + +const timeSlots = ["10:00 AM", "11:00 AM", "12:00 PM", "1:00 PM", "2:00 PM", "3:00 PM", "4:00 PM", "5:00 PM", "6:00 PM"] + +const tattooSizes = [ + { size: "Small (2-4 inches)", duration: "1-2 hours", price: "150-300" }, + { size: "Medium (4-6 inches)", duration: "2-4 hours", price: "300-600" }, + { size: "Large (6+ inches)", duration: "4-6 hours", price: "600-1000" }, + { size: "Full Session", duration: "6-8 hours", price: "1000-1500" }, +] + +interface BookingFormProps { + artistId?: string +} + +export function BookingForm({ artistId }: BookingFormProps) { + const [step, setStep] = useState(1) + const [selectedDate, setSelectedDate] = useState() + const [formData, setFormData] = useState({ + // Personal Info + firstName: "", + lastName: "", + email: "", + phone: "", + age: "", + + // Appointment Details + artistId: artistId || "", + preferredDate: "", + preferredTime: "", + alternateDate: "", + alternateTime: "", + + // Tattoo Details + tattooDescription: "", + tattooSize: "", + placement: "", + isFirstTattoo: false, + hasAllergies: false, + allergyDetails: "", + referenceImages: "", + + // Additional Info + specialRequests: "", + depositAmount: 100, + agreeToTerms: false, + agreeToDeposit: false, + }) + + const selectedArtist = artists.find((a) => String(a.id) === formData.artistId || a.slug === formData.artistId) + const selectedSize = tattooSizes.find((size) => size.size === formData.tattooSize) + + const handleInputChange = (field: string, value: any) => { + setFormData((prev) => ({ ...prev, [field]: value })) + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + // Handle form submission + console.log("Booking submitted:", formData) + // In a real app, this would send data to your backend + } + + const nextStep = () => setStep((prev) => Math.min(prev + 1, 4)) + const prevStep = () => setStep((prev) => Math.max(prev - 1, 1)) + + return ( +
+
+ {/* Header */} +
+

Book Your Appointment

+

+ Let's create something amazing together. Fill out the form below to schedule your tattoo session. +

+
+ + {/* Progress Indicator */} +
+
+ {[1, 2, 3, 4].map((stepNumber) => ( +
+
= stepNumber ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground" + }`} + > + {stepNumber} +
+ {stepNumber < 4 && ( +
stepNumber ? "bg-primary" : "bg-muted"}`} /> + )} +
+ ))} +
+
+ +
+ {/* Step 1: Personal Information */} + {step === 1 && ( + + + + + Personal Information + + + +
+
+ + handleInputChange("firstName", e.target.value)} + required + /> +
+
+ + handleInputChange("lastName", e.target.value)} + required + /> +
+
+ +
+
+ + handleInputChange("email", e.target.value)} + required + /> +
+
+ + handleInputChange("phone", e.target.value)} + required + /> +
+
+ +
+
+ + handleInputChange("age", e.target.value)} + required + /> +

Must be 18 or older

+
+
+ +
+ handleInputChange("isFirstTattoo", checked)} + /> + +
+ +
+
+ handleInputChange("hasAllergies", checked)} + /> + +
+ + {formData.hasAllergies && ( +
+ +