fix: improve mobile UX, polish artist cards, and enhance contact modal with responsive backgrounds
43
.clinerules/cloudflare.md
Normal file
@ -0,0 +1,43 @@
|
||||
# NextJS + Cloudflare + OpenNext Deployment
|
||||
|
||||
## Setup Requirements
|
||||
- Node.js 18+ and Cloudflare account required
|
||||
- **@opennextjs/cloudflare** adapter mandatory (not edge runtime)
|
||||
- Global Wrangler CLI: `npm install -g wrangler`
|
||||
- All deployments via OpenNext adapter; no direct NextJS builds
|
||||
|
||||
## Project Configuration
|
||||
- **wrangler.toml**: compatibility_date ≥ "2024-09-23", nodejs_compat flag
|
||||
- **package.json**: `pages:build` script runs `npx @opennextjs/cloudflare@latest`
|
||||
- **next.config.js**: `output: 'standalone'`, image optimization configured
|
||||
- Build output directory: `.vercel/output/static`
|
||||
|
||||
## Build & Deploy Process
|
||||
- Build command: `npm run pages:build` (transforms NextJS → Workers)
|
||||
- Local testing: `npm run preview` (required before deploy)
|
||||
- Deploy: `npm run deploy` or Cloudflare Pages Git integration
|
||||
- Never deploy untested builds; preview mimics production runtime
|
||||
|
||||
## Environment & Security
|
||||
- Environment variables in both Cloudflare Dashboard and `wrangler.toml`
|
||||
- Secrets via `wrangler secret put SECRET_NAME` (not in wrangler.toml)
|
||||
- Security headers required in API routes (X-Frame-Options, CSP, etc.)
|
||||
- Cache headers mandatory for API endpoints: `s-maxage=86400, stale-while-revalidate`
|
||||
|
||||
## Performance & Limits
|
||||
- Bundle size limits: 3MB free tier, 15MB paid
|
||||
- Dynamic imports for heavy components to reduce cold starts
|
||||
- Static files in `public/` directory only
|
||||
- Image optimization via Cloudflare Images or custom loader
|
||||
|
||||
## Database & Storage
|
||||
- Cloudflare D1 binding in wrangler.toml for SQL databases
|
||||
- Workers KV for key-value storage
|
||||
- All DB operations via environment bindings (env.DB, env.KV)
|
||||
- No direct database connections; use Cloudflare services
|
||||
|
||||
## CI/CD Integration
|
||||
- GitHub Actions with CLOUDFLARE_API_TOKEN secret
|
||||
- Build step: `npm run pages:build`
|
||||
- Deploy: `wrangler pages deploy .vercel/output/static`
|
||||
- Fail builds on type/compatibility errors
|
||||
@ -6,8 +6,6 @@ import { ArtistsSection } from "@/components/artists-section"
|
||||
import { ServicesSection } from "@/components/services-section"
|
||||
import { ContactSection } from "@/components/contact-section"
|
||||
import { Footer } from "@/components/footer"
|
||||
import { MobileBookingBar } from "@/components/mobile-booking-bar"
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
@ -27,7 +25,6 @@ export default function HomePage() {
|
||||
<ContactSection />
|
||||
</div>
|
||||
<Footer />
|
||||
<MobileBookingBar />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export function ArtistsSection() {
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" },
|
||||
{ threshold: 0.2, rootMargin: "0px 0px 0px 0px" },
|
||||
)
|
||||
|
||||
const cards = sectionRef.current?.querySelectorAll("[data-index]")
|
||||
@ -55,29 +55,30 @@ export function ArtistsSection() {
|
||||
const sectionTop = sectionRef.current?.offsetTop || 0
|
||||
const relativeScroll = scrollY - sectionTop
|
||||
|
||||
leftColumnRef.current.style.transform = `translateY(${relativeScroll * -0.05}px)`
|
||||
leftColumnRef.current.style.transform = `translateY(${relativeScroll * -0.025}px)`
|
||||
centerColumnRef.current.style.transform = `translateY(0px)`
|
||||
rightColumnRef.current.style.transform = `translateY(${relativeScroll * 0.05}px)`
|
||||
rightColumnRef.current.style.transform = `translateY(${relativeScroll * 0.025}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)`
|
||||
;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.01}px)`
|
||||
})
|
||||
centerImages.forEach((img) => {
|
||||
;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.015}px)`
|
||||
;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.0075}px)`
|
||||
})
|
||||
rightImages.forEach((img) => {
|
||||
;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.01}px)`
|
||||
;(img as HTMLElement).style.transform = `translateY(${relativeScroll * -0.005}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)
|
||||
// Better distribution for visual balance
|
||||
const leftColumn = [artists[0], artists[3], artists[6]] // Christy, Donovan, John
|
||||
const centerColumn = [artists[1], artists[4], artists[7]] // Angel, EJ, Pako
|
||||
const rightColumn = [artists[2], artists[5], artists[8]] // Amari, Heather, Sole
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} id="artists" className="relative overflow-hidden bg-black">
|
||||
@ -112,7 +113,7 @@ export function ArtistsSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 px-8 lg:px-16 pb-20">
|
||||
<div className="relative z-10 px-8 lg:px-16 pb-32">
|
||||
<div className="max-w-screen-2xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div ref={leftColumnRef} className="space-y-8">
|
||||
@ -126,25 +127,33 @@ export function ArtistsSection() {
|
||||
: "opacity-0 translate-y-8"
|
||||
}`}
|
||||
style={{
|
||||
transitionDelay: `${artists.indexOf(artist) * 100}ms`,
|
||||
transitionDelay: `${artists.indexOf(artist) * 50}ms`,
|
||||
}}
|
||||
>
|
||||
<div className="relative h-[600px] overflow-hidden rounded-lg shadow-2xl">
|
||||
<div className="relative w-full aspect-[4/5] overflow-hidden rounded-lg shadow-2xl">
|
||||
<div className="absolute inset-0 bg-black artist-image">
|
||||
<div className="absolute left-0 top-0 w-1/2 h-full">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={`${artist.name} portrait`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-0 top-0 w-1/2 h-full">
|
||||
{/* Portfolio background - full width */}
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={artist.workImages?.[0] || "/placeholder.svg"}
|
||||
alt={`${artist.name} tattoo work`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
/>
|
||||
{/* Darkening overlay to push background further back */}
|
||||
<div className="absolute inset-0 bg-black/40"></div>
|
||||
</div>
|
||||
|
||||
{/* Artist portrait - with proper feathered mask */}
|
||||
<div className="absolute left-0 top-0 w-3/5 h-full">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={`${artist.name} portrait`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to right, black 0%, black 70%, transparent 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to right, black 0%, black 70%, transparent 100%)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -155,7 +164,7 @@ export function ArtistsSection() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-6 translate-y-full group-hover:translate-y-0 transition-transform duration-500">
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-6 translate-y-0 lg:translate-y-full lg:group-hover:translate-y-0 transition-transform duration-500">
|
||||
<h3 className="text-2xl font-bold tracking-tight mb-2 text-white">{artist.name}</h3>
|
||||
<p className="text-sm font-medium text-white/90 mb-3">{artist.specialty}</p>
|
||||
<p className="text-sm text-white/80 mb-4 leading-relaxed">{artist.bio}</p>
|
||||
@ -194,25 +203,33 @@ export function ArtistsSection() {
|
||||
: "opacity-0 translate-y-8"
|
||||
}`}
|
||||
style={{
|
||||
transitionDelay: `${artists.indexOf(artist) * 100}ms`,
|
||||
transitionDelay: `${artists.indexOf(artist) * 50}ms`,
|
||||
}}
|
||||
>
|
||||
<div className="relative h-[600px] overflow-hidden rounded-lg shadow-2xl">
|
||||
<div className="relative w-full aspect-[4/5] overflow-hidden rounded-lg shadow-2xl">
|
||||
<div className="absolute inset-0 bg-black artist-image">
|
||||
<div className="absolute left-0 top-0 w-1/2 h-full">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={`${artist.name} portrait`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-0 top-0 w-1/2 h-full">
|
||||
{/* Portfolio background - full width */}
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={artist.workImages?.[0] || "/placeholder.svg"}
|
||||
alt={`${artist.name} tattoo work`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
/>
|
||||
{/* Darkening overlay to push background further back */}
|
||||
<div className="absolute inset-0 bg-black/40"></div>
|
||||
</div>
|
||||
|
||||
{/* Artist portrait - with proper feathered mask */}
|
||||
<div className="absolute left-0 top-0 w-3/5 h-full">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={`${artist.name} portrait`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to right, black 0%, black 70%, transparent 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to right, black 0%, black 70%, transparent 100%)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -223,7 +240,7 @@ export function ArtistsSection() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-6 translate-y-full group-hover:translate-y-0 transition-transform duration-500">
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-6 translate-y-0 lg:translate-y-full lg:group-hover:translate-y-0 transition-transform duration-500">
|
||||
<h3 className="text-2xl font-bold tracking-tight mb-2 text-white">{artist.name}</h3>
|
||||
<p className="text-sm font-medium text-white/90 mb-3">{artist.specialty}</p>
|
||||
<p className="text-sm text-white/80 mb-4 leading-relaxed">{artist.bio}</p>
|
||||
@ -262,25 +279,33 @@ export function ArtistsSection() {
|
||||
: "opacity-0 translate-y-8"
|
||||
}`}
|
||||
style={{
|
||||
transitionDelay: `${artists.indexOf(artist) * 100}ms`,
|
||||
transitionDelay: `${artists.indexOf(artist) * 50}ms`,
|
||||
}}
|
||||
>
|
||||
<div className="relative h-[600px] overflow-hidden rounded-lg shadow-2xl">
|
||||
<div className="relative w-full aspect-[4/5] overflow-hidden rounded-lg shadow-2xl">
|
||||
<div className="absolute inset-0 bg-black artist-image">
|
||||
<div className="absolute left-0 top-0 w-1/2 h-full">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={`${artist.name} portrait`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-0 top-0 w-1/2 h-full">
|
||||
{/* Portfolio background - full width */}
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={artist.workImages?.[0] || "/placeholder.svg"}
|
||||
alt={`${artist.name} tattoo work`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
/>
|
||||
{/* Darkening overlay to push background further back */}
|
||||
<div className="absolute inset-0 bg-black/40"></div>
|
||||
</div>
|
||||
|
||||
{/* Artist portrait - with proper feathered mask */}
|
||||
<div className="absolute left-0 top-0 w-3/5 h-full">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={`${artist.name} portrait`}
|
||||
className="w-full h-full object-cover scale-110"
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to right, black 0%, black 70%, transparent 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to right, black 0%, black 70%, transparent 100%)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -291,7 +316,7 @@ export function ArtistsSection() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-6 translate-y-full group-hover:translate-y-0 transition-transform duration-500">
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-6 translate-y-0 lg:translate-y-full lg:group-hover:translate-y-0 transition-transform duration-500">
|
||||
<h3 className="text-2xl font-bold tracking-tight mb-2 text-white">{artist.name}</h3>
|
||||
<p className="text-sm font-medium text-white/90 mb-3">{artist.specialty}</p>
|
||||
<p className="text-sm text-white/80 mb-4 leading-relaxed">{artist.bio}</p>
|
||||
@ -322,7 +347,7 @@ export function ArtistsSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-black text-white py-20 px-8 lg:px-16">
|
||||
<div className="relative z-20 bg-black text-white py-20 px-8 lg:px-16">
|
||||
<div className="max-w-screen-2xl mx-auto text-center">
|
||||
<h3 className="text-5xl lg:text-7xl font-bold tracking-tight mb-8">READY?</h3>
|
||||
<p className="text-xl text-white/70 mb-12 max-w-2xl mx-auto">
|
||||
|
||||
@ -103,7 +103,9 @@ export function ContactModal({ children }: ContactModalProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto bg-white border-0 shadow-2xl relative">
|
||||
{/* Solid background overlay to block any background images */}
|
||||
<div className="absolute inset-0 bg-white -z-10 rounded-lg"></div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-playfair text-2xl">Get In Touch</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@ -36,17 +36,23 @@ export function ContactSection() {
|
||||
|
||||
return (
|
||||
<section id="contact" className="min-h-screen bg-black relative overflow-hidden">
|
||||
{/* Background logo - desktop only */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03] bg-cover bg-center bg-no-repeat blur-sm"
|
||||
className="absolute inset-0 opacity-[0.03] bg-cover bg-center bg-no-repeat blur-sm hidden lg:block"
|
||||
style={{
|
||||
backgroundImage: "url('/united-logo-full.jpg')",
|
||||
transform: `translateY(${scrollY * 0.2}px)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Mobile solid background */}
|
||||
<div className="absolute inset-0 bg-black lg:hidden"></div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row min-h-screen relative z-10">
|
||||
<div className="w-full lg:w-1/2 bg-black flex items-center justify-center p-8 lg:p-12">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="w-full lg:w-1/2 bg-black flex items-center justify-center p-8 lg:p-12 relative">
|
||||
{/* Mobile background overlay to hide logo */}
|
||||
<div className="absolute inset-0 bg-black lg:bg-transparent"></div>
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-4xl font-bold text-white mb-2">Let's Talk</h2>
|
||||
<p className="text-gray-400">Ready to create something amazing?</p>
|
||||
|
||||
@ -45,7 +45,7 @@ export function Navigation() {
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-700 ease-out ${
|
||||
isScrolled
|
||||
? "bg-black/95 backdrop-blur-md shadow-lg border-b border-white/10 opacity-100"
|
||||
: "bg-transparent lg:opacity-0 lg:pointer-events-none opacity-100 bg-black/80 backdrop-blur-md"
|
||||
: "bg-black/80 backdrop-blur-md lg:bg-transparent lg:opacity-0 lg:pointer-events-none opacity-100"
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-screen-2xl mx-auto px-6 lg:px-12">
|
||||
|
||||
@ -61,8 +61,8 @@ export function ServicesMobileCarousel() {
|
||||
>
|
||||
<CarouselContent className="-ml-2 md:-ml-4">
|
||||
{services.map((service, index) => (
|
||||
<CarouselItem key={index} className="pl-2 md:pl-4 basis-[85%] sm:basis-[75%]">
|
||||
<div className="min-h-[70vh] flex items-center justify-center p-6 relative">
|
||||
<CarouselItem key={index} className="pl-2 md:pl-4 basis-[85%] sm:basis-[75%] md:basis-[70%]">
|
||||
<div className="min-h-[50vh] flex items-center justify-center p-4 relative">
|
||||
<div className="max-w-sm relative">
|
||||
<div className="mb-6">
|
||||
<span className="text-sm font-medium tracking-widest text-white/60 uppercase">
|
||||
|
||||
116
components/services-mobile-only.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel"
|
||||
import Link from "next/link"
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: "Black & Grey Realism",
|
||||
description: "Photorealistic tattoos with incredible depth and detail using black and grey shading techniques.",
|
||||
features: ["Lifelike portraits", "Detailed shading", "3D effects"],
|
||||
price: "Starting at $250",
|
||||
},
|
||||
{
|
||||
title: "Cover-ups & Blackout",
|
||||
description: "Transform old tattoos into stunning new pieces with expert cover-up techniques or bold blackout designs.",
|
||||
features: ["Free consultation", "Creative solutions", "Complete coverage"],
|
||||
price: "Starting at $300",
|
||||
},
|
||||
{
|
||||
title: "Fine Line & Micro Realism",
|
||||
description: "Delicate, precise linework and tiny realistic designs that showcase incredible detail.",
|
||||
features: ["Single needle work", "Intricate details", "Minimalist aesthetic"],
|
||||
price: "Starting at $150",
|
||||
},
|
||||
{
|
||||
title: "Traditional & Neo-Traditional",
|
||||
description: "Bold American traditional and neo-traditional styles with vibrant colors and strong lines.",
|
||||
features: ["Classic designs", "Bold color palettes", "Timeless appeal"],
|
||||
price: "Starting at $200",
|
||||
},
|
||||
{
|
||||
title: "Anime & Watercolor",
|
||||
description: "Vibrant anime characters and painterly watercolor effects that bring art to life on skin.",
|
||||
features: ["Character designs", "Soft color blends", "Artistic techniques"],
|
||||
price: "Starting at $250",
|
||||
},
|
||||
]
|
||||
|
||||
export function ServicesMobileOnly() {
|
||||
return (
|
||||
<section className="lg:hidden bg-black text-white py-16">
|
||||
{/* Header */}
|
||||
<div className="px-6 mb-12 text-center">
|
||||
<div className="mb-4">
|
||||
<span className="text-sm font-medium tracking-widest text-white/60 uppercase">Our Services</span>
|
||||
</div>
|
||||
<h2 className="text-4xl font-bold tracking-tight mb-4">Choose Your Style</h2>
|
||||
<p className="text-white/70 max-w-md mx-auto">
|
||||
From custom designs to cover-ups, we offer comprehensive tattoo services with the highest standards.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Carousel */}
|
||||
<div className="px-4">
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "start",
|
||||
loop: true,
|
||||
}}
|
||||
className="w-full max-w-sm mx-auto"
|
||||
>
|
||||
<CarouselContent className="-ml-2">
|
||||
{services.map((service, index) => (
|
||||
<CarouselItem key={index} className="pl-2 basis-full">
|
||||
<Card className="bg-black border-white/20 text-white h-full">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="text-xs font-medium tracking-widest text-white/60 uppercase mb-2">
|
||||
Service {String(index + 1).padStart(2, "0")}
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold leading-tight">
|
||||
{service.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/80 text-base leading-relaxed">
|
||||
{service.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pb-4">
|
||||
<div className="space-y-2 mb-6">
|
||||
{service.features.map((feature, idx) => (
|
||||
<div key={idx} className="flex items-center text-white/70">
|
||||
<span className="w-1.5 h-1.5 bg-white/40 rounded-full mr-3 flex-shrink-0"></span>
|
||||
<span className="text-sm">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-xl font-bold text-white mb-4">
|
||||
{service.price}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="pt-0">
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-white text-black hover:bg-gray-100 !text-black font-medium"
|
||||
>
|
||||
<Link href="/book">BOOK NOW</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
|
||||
<div className="flex justify-center mt-8 gap-4">
|
||||
<CarouselPrevious className="relative translate-y-0 left-0 bg-white/10 border-white/20 text-white hover:bg-white/20" />
|
||||
<CarouselNext className="relative translate-y-0 right-0 bg-white/10 border-white/20 text-white hover:bg-white/20" />
|
||||
</div>
|
||||
</Carousel>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Link from "next/link"
|
||||
import { ServicesMobileCarousel } from "@/components/services-mobile-carousel"
|
||||
import { ServicesMobileOnly } from "@/components/services-mobile-only"
|
||||
|
||||
const services = [
|
||||
{
|
||||
@ -101,10 +101,10 @@ export function ServicesSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-black text-white relative z-10">
|
||||
<div className="hidden lg:block bg-black text-white relative z-10">
|
||||
<div className="flex">
|
||||
{/* Left Side - Enhanced with split composition styling */}
|
||||
<div className="hidden lg:block w-1/2 sticky top-0 h-screen bg-black relative">
|
||||
<div className="w-1/2 sticky top-0 h-screen bg-black relative">
|
||||
<div className="absolute right-0 top-0 w-px h-full bg-white/10"></div>
|
||||
<div className="h-full flex flex-col justify-center p-16 relative">
|
||||
<div className="space-y-8">
|
||||
@ -201,8 +201,9 @@ export function ServicesSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ServicesMobileCarousel />
|
||||
</div>
|
||||
|
||||
<ServicesMobileOnly />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -269,7 +269,7 @@ export const artists: Artist[] = [
|
||||
name: "Steven 'Sole' Cedre",
|
||||
title: "It has to have soul, Sole!",
|
||||
specialty: "Gritty Realism & Comic Art",
|
||||
faceImage: "/artists/sole-cedre-portrait.jpg",
|
||||
faceImage: "/artists/steven-sole-cedre.jpg",
|
||||
workImages: [
|
||||
"/artists/sole-cedre-work-1.jpg",
|
||||
"/artists/sole-cedre-work-2.jpg",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 141 KiB |
BIN
public/artists/dez-portrait.jpg
Normal file
|
After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 56 KiB |
BIN
public/artists/donovan-lankford-portrait.png
Normal file
|
After Width: | Height: | Size: 632 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 121 KiB |
BIN
public/artists/john-lapides-portrait.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
public/artists/steven-sole-cedre.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |