feat(components): add new UI components for enhanced user experience
- Introduced multiple new components including AnimatedLink, Button, Calendar, Card, ColorSwatch, Divider, Filmstrip, FormContainer, FormField, GalleryCard, Heading, HeroOverlay, IdentitySection, ImmersionSection, NewArtistsSection, NewContactSection, NewHero, NewNavigation, Reveal, SectionLabel, StickySplit, and Toast. - Each component is designed with responsive layouts and customizable styles to improve the overall UI consistency and interactivity. - Implemented accessibility features and animations to enhance user engagement. This commit significantly expands the component library, providing a robust foundation for building a cohesive user interface.
This commit is contained in:
parent
02dcfa043d
commit
3e739877b4
32
components/united/animated-link.tsx
Normal file
32
components/united/animated-link.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface AnimatedLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {}
|
||||
|
||||
const AnimatedLink = React.forwardRef<HTMLAnchorElement, AnimatedLinkProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
className={cn("relative inline-block text-xl pb-0.5 group", "text-[var(--ink)]", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span
|
||||
className={cn(
|
||||
"absolute bottom-0 left-0 w-full h-[2px]",
|
||||
"bg-[var(--burnt)]",
|
||||
"origin-left scale-x-0",
|
||||
"transition-transform duration-300 ease-out",
|
||||
"group-hover:scale-x-100",
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
},
|
||||
)
|
||||
AnimatedLink.displayName = "AnimatedLink"
|
||||
|
||||
export { AnimatedLink }
|
||||
53
components/united/button.tsx
Normal file
53
components/united/button.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "ghost"
|
||||
size?: "default" | "sm" | "lg"
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "primary", size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center font-medium uppercase tracking-[0.2em] transition-all duration-200 ease-out",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--rose)] focus-visible:ring-offset-2",
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
"rounded-[12px]",
|
||||
|
||||
size === "default" && "px-6 py-4 text-[0.85rem]",
|
||||
size === "sm" && "px-4 py-3 text-[0.75rem]",
|
||||
size === "lg" && "px-8 py-5 text-[0.9rem]",
|
||||
|
||||
variant === "primary" && [
|
||||
"bg-[var(--burnt)] text-white",
|
||||
"shadow-[0_10px_22px_rgba(176,71,30,0.25)]",
|
||||
"hover:-translate-y-0.5 hover:scale-[1.03]",
|
||||
"hover:shadow-[0_14px_28px_rgba(176,71,30,0.35)]",
|
||||
],
|
||||
variant === "secondary" && [
|
||||
"bg-[var(--terracotta)] text-white",
|
||||
"shadow-[0_10px_22px_rgba(216,120,80,0.25)]",
|
||||
"hover:-translate-y-0.5 hover:scale-[1.03]",
|
||||
"hover:shadow-[0_14px_28px_rgba(216,120,80,0.35)]",
|
||||
],
|
||||
variant === "ghost" && [
|
||||
"bg-[var(--sand)] text-[var(--ink)]",
|
||||
"border border-[rgba(31,27,23,0.2)]",
|
||||
"hover:-translate-y-0.5",
|
||||
],
|
||||
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button }
|
||||
57
components/united/calendar.tsx
Normal file
57
components/united/calendar.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface CalendarDay {
|
||||
day: number
|
||||
isBooked?: boolean
|
||||
isMuted?: boolean
|
||||
}
|
||||
|
||||
export interface CalendarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title?: string
|
||||
days: CalendarDay[]
|
||||
}
|
||||
|
||||
const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(({ className, title, days, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-[20px] p-6",
|
||||
"bg-gradient-to-br from-[rgba(242,227,208,0.98)] to-[rgba(255,247,236,0.95)]",
|
||||
"border border-[rgba(210,106,50,0.2)]",
|
||||
"shadow-[0_14px_24px_rgba(31,27,23,0.18)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{title && (
|
||||
<span className="block text-[0.75rem] font-semibold uppercase tracking-[0.3em] text-[var(--moss)] mb-4">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
<div className="grid grid-cols-7 gap-1.5">
|
||||
{days.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"aspect-square rounded-lg flex items-center justify-center",
|
||||
"text-[0.75rem] font-medium",
|
||||
day.isBooked
|
||||
? "bg-[var(--terracotta)] text-white shadow-[0_10px_18px_rgba(216,120,80,0.3)]"
|
||||
: "bg-[rgba(255,255,255,0.8)] border border-[rgba(210,106,50,0.2)] text-[rgba(31,27,23,0.8)]",
|
||||
day.isMuted && "opacity-40",
|
||||
)}
|
||||
>
|
||||
{day.day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
export { Calendar }
|
||||
84
components/united/card.tsx
Normal file
84
components/united/card.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "default" | "component" | "motion" | "scroll-step" | "sticky"
|
||||
}
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, variant = "default", ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-[22px]",
|
||||
|
||||
variant === "default" && [
|
||||
"bg-gradient-to-br from-[rgba(242,227,208,0.95)] to-[rgba(255,247,236,0.9)]",
|
||||
"border border-[rgba(122,139,139,0.2)]",
|
||||
"shadow-[0_20px_35px_rgba(31,27,23,0.1)]",
|
||||
"p-7",
|
||||
],
|
||||
|
||||
variant === "component" && [
|
||||
"bg-[rgba(255,255,255,0.88)]",
|
||||
"border border-[rgba(31,27,23,0.08)]",
|
||||
"rounded-[18px]",
|
||||
"p-6",
|
||||
],
|
||||
|
||||
variant === "motion" && [
|
||||
"bg-gradient-to-br from-[rgba(255,247,236,0.95)] to-[rgba(242,227,208,0.9)]",
|
||||
"border border-[rgba(122,139,139,0.25)]",
|
||||
"shadow-[0_12px_28px_rgba(31,27,23,0.1)]",
|
||||
"p-8 rounded-[20px]",
|
||||
"transition-all duration-300 ease-out",
|
||||
"hover:-translate-y-0.5 hover:shadow-[0_16px_36px_rgba(31,27,23,0.15)]",
|
||||
],
|
||||
|
||||
variant === "scroll-step" && [
|
||||
"bg-[rgba(255,255,255,0.92)]",
|
||||
"border border-[rgba(31,27,23,0.08)]",
|
||||
"shadow-[0_18px_28px_rgba(31,27,23,0.06)]",
|
||||
"rounded-[20px] p-6",
|
||||
],
|
||||
|
||||
variant === "sticky" && [
|
||||
"sticky top-8",
|
||||
"bg-[rgba(255,255,255,0.95)]",
|
||||
"border border-[rgba(31,27,23,0.12)]",
|
||||
"shadow-[0_25px_40px_rgba(31,27,23,0.08)]",
|
||||
"rounded-[22px] p-7",
|
||||
],
|
||||
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex justify-between items-baseline mb-4", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("text-[0.95rem] font-semibold uppercase tracking-[0.2em] text-[var(--moss)]", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn("", className)} {...props} />,
|
||||
)
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
export { Card, CardHeader, CardTitle, CardContent }
|
||||
40
components/united/color-swatch.tsx
Normal file
40
components/united/color-swatch.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface ColorSwatchProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
hex: string
|
||||
name: string
|
||||
usage: string
|
||||
bgColor?: string
|
||||
}
|
||||
|
||||
const ColorSwatch = React.forwardRef<HTMLDivElement, ColorSwatchProps>(
|
||||
({ className, hex, name, usage, bgColor, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-[24px] p-6 text-white min-h-[220px]",
|
||||
"flex flex-col justify-between",
|
||||
"shadow-[var(--shadow-subtle)]",
|
||||
"transition-transform duration-200 ease-out",
|
||||
"hover:scale-105",
|
||||
className,
|
||||
)}
|
||||
style={{ backgroundColor: bgColor || hex }}
|
||||
{...props}
|
||||
>
|
||||
<div>
|
||||
<strong className="text-xl font-semibold tracking-wide block">{hex}</strong>
|
||||
<span className="text-[0.9rem] opacity-90">{name}</span>
|
||||
</div>
|
||||
<span className="text-[0.75rem] uppercase tracking-[0.15em] opacity-85">{usage}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
ColorSwatch.displayName = "ColorSwatch"
|
||||
|
||||
export { ColorSwatch }
|
||||
21
components/united/divider.tsx
Normal file
21
components/united/divider.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface DividerProps extends React.HTMLAttributes<HTMLHRElement> {}
|
||||
|
||||
const Divider = React.forwardRef<HTMLHRElement, DividerProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<hr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-0 h-px my-16",
|
||||
"bg-gradient-to-r from-transparent via-[rgba(36,27,22,0.15)] to-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Divider.displayName = "Divider"
|
||||
|
||||
export { Divider }
|
||||
116
components/united/filmstrip.tsx
Normal file
116
components/united/filmstrip.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
export interface FilmstripItem {
|
||||
src: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface FilmstripProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
items: FilmstripItem[]
|
||||
title?: string
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
const Filmstrip = React.forwardRef<HTMLDivElement, FilmstripProps>(
|
||||
({ className, items, title, subtitle, ...props }, ref) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const trackRef = useRef<HTMLDivElement>(null)
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
const track = trackRef.current
|
||||
if (!container || !track) return
|
||||
|
||||
const handleScroll = () => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const viewHeight = window.innerHeight
|
||||
const containerHeight = rect.height
|
||||
|
||||
let scrollProgress = -rect.top / (containerHeight - viewHeight)
|
||||
scrollProgress = Math.min(Math.max(scrollProgress, 0), 1)
|
||||
setProgress(scrollProgress)
|
||||
|
||||
const trackWidth = track.scrollWidth
|
||||
const maxTranslate = trackWidth - window.innerWidth
|
||||
track.style.transform = `translateX(${-maxTranslate * scrollProgress}px)`
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||
handleScroll()
|
||||
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(node) => {
|
||||
// @ts-ignore - assigning to ref.current is needed for ref merging
|
||||
containerRef.current = node
|
||||
if (typeof ref === "function") ref(node)
|
||||
else if (ref) {
|
||||
// @ts-ignore
|
||||
ref.current = node
|
||||
}
|
||||
}}
|
||||
className={cn("h-[300vh] relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="sticky top-0 h-screen overflow-hidden flex flex-col justify-center">
|
||||
{(title || subtitle) && (
|
||||
<div className="text-center mb-8 px-6">
|
||||
{subtitle && (
|
||||
<span className="block text-[0.75rem] font-semibold uppercase tracking-[0.3em] text-[var(--moss)] mb-3">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
{title && <h2 className="font-serif text-[clamp(1.9rem,4vw,3rem)] leading-[1.15]">{title}</h2>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={trackRef} className="flex gap-[clamp(1rem,2vw,2rem)] pl-[10vw] will-change-transform">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex-shrink-0",
|
||||
"w-[clamp(400px,60vw,800px)] aspect-[4/5]",
|
||||
"rounded-[32px]",
|
||||
"bg-cover bg-center",
|
||||
"shadow-[var(--shadow-filmic)]",
|
||||
"relative",
|
||||
)}
|
||||
style={{ backgroundImage: `url(${item.src})` }}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute left-8 bottom-8",
|
||||
"text-[0.75rem] uppercase tracking-[0.25em]",
|
||||
"text-white/95",
|
||||
"drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)]",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 w-[200px] h-0.5 bg-[rgba(36,27,22,0.1)] rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[var(--burnt)] to-[var(--rose)]"
|
||||
style={{ transform: `scaleX(${progress})`, transformOrigin: "left" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
Filmstrip.displayName = "Filmstrip"
|
||||
|
||||
export { Filmstrip }
|
||||
24
components/united/form-container.tsx
Normal file
24
components/united/form-container.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface FormContainerProps extends React.FormHTMLAttributes<HTMLFormElement> {}
|
||||
|
||||
const FormContainer = React.forwardRef<HTMLFormElement, FormContainerProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-[22px] p-8",
|
||||
"bg-gradient-to-br from-[rgba(242,227,208,0.98)] to-[rgba(255,247,236,0.95)]",
|
||||
"border border-[rgba(210,106,50,0.2)]",
|
||||
"shadow-[0_14px_24px_rgba(31,27,23,0.18)]",
|
||||
"grid gap-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormContainer.displayName = "FormContainer"
|
||||
|
||||
export { FormContainer }
|
||||
68
components/united/form-field.tsx
Normal file
68
components/united/form-field.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface FormFieldProps {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const FormField = ({ label, children, className }: FormFieldProps) => {
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
<label className="block text-[0.75rem] font-semibold uppercase tracking-[0.25em] text-[rgba(31,27,23,0.7)] mb-2">
|
||||
{label}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"w-full rounded-[12px] border border-[rgba(210,106,50,0.25)]",
|
||||
"px-5 py-4 text-[0.95rem] font-medium",
|
||||
"bg-[rgba(255,255,255,0.9)] text-[rgba(31,27,23,0.9)]",
|
||||
"placeholder:text-[rgba(31,27,23,0.5)]",
|
||||
"transition-all duration-200 ease-out",
|
||||
"focus:outline-none focus:border-[var(--terracotta)]",
|
||||
"focus:shadow-[0_0_0_3px_rgba(210,106,50,0.2)]",
|
||||
"focus:bg-[rgba(255,255,255,0.95)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"w-full rounded-[12px] border border-[rgba(210,106,50,0.25)]",
|
||||
"px-5 py-4 text-[0.95rem] font-medium",
|
||||
"bg-[rgba(255,255,255,0.9)] text-[rgba(31,27,23,0.9)]",
|
||||
"placeholder:text-[rgba(31,27,23,0.5)]",
|
||||
"transition-all duration-200 ease-out",
|
||||
"focus:outline-none focus:border-[var(--terracotta)]",
|
||||
"focus:shadow-[0_0_0_3px_rgba(210,106,50,0.2)]",
|
||||
"focus:bg-[rgba(255,255,255,0.95)]",
|
||||
"resize-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { FormField, Input, Textarea }
|
||||
37
components/united/gallery-card.tsx
Normal file
37
components/united/gallery-card.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface GalleryCardProps extends React.HTMLAttributes<HTMLElement> {
|
||||
src: string
|
||||
alt: string
|
||||
label: string
|
||||
aspectRatio?: string
|
||||
}
|
||||
|
||||
const GalleryCard = React.forwardRef<HTMLElement, GalleryCardProps>(
|
||||
({ className, src, alt, label, aspectRatio = "3/4", ...props }, ref) => {
|
||||
return (
|
||||
<figure ref={ref} className={cn("m-0 group", className)} {...props}>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-[28px] overflow-hidden mb-4",
|
||||
"shadow-[var(--shadow-lg)]",
|
||||
"transition-all duration-300 ease-out",
|
||||
"group-hover:-translate-y-1.5 group-hover:shadow-[var(--shadow-bloom)]",
|
||||
)}
|
||||
style={{ aspectRatio }}
|
||||
>
|
||||
<img
|
||||
src={src || "/placeholder.svg"}
|
||||
alt={alt}
|
||||
className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-[1.03]"
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="text-[0.7rem] uppercase tracking-[0.25em] opacity-80">{label}</figcaption>
|
||||
</figure>
|
||||
)
|
||||
},
|
||||
)
|
||||
GalleryCard.displayName = "GalleryCard"
|
||||
|
||||
export { GalleryCard }
|
||||
49
components/united/heading.tsx
Normal file
49
components/united/heading.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
level?: 1 | 2 | 3 | 4
|
||||
}
|
||||
|
||||
const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
|
||||
({ className, level = 2, children, ...props }, ref) => {
|
||||
const baseClasses = cn(
|
||||
"font-serif font-normal",
|
||||
level === 1 && "text-[clamp(2.5rem,5vw,3.8rem)] leading-[1.1] tracking-tight mb-5",
|
||||
level === 2 && "text-[clamp(1.9rem,4vw,3rem)] leading-[1.15] mb-4",
|
||||
level === 3 && "text-[0.95rem] font-sans font-semibold uppercase tracking-[0.2em] text-[var(--moss)] mb-3",
|
||||
level === 4 && "text-[0.85rem] font-sans font-semibold tracking-[0.2em] mb-2",
|
||||
className,
|
||||
)
|
||||
|
||||
if (level === 1) {
|
||||
return (
|
||||
<h1 ref={ref} className={baseClasses} {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
if (level === 2) {
|
||||
return (
|
||||
<h2 ref={ref} className={baseClasses} {...props}>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
if (level === 3) {
|
||||
return (
|
||||
<h3 ref={ref} className={baseClasses} {...props}>
|
||||
{children}
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<h4 ref={ref} className={baseClasses} {...props}>
|
||||
{children}
|
||||
</h4>
|
||||
)
|
||||
},
|
||||
)
|
||||
Heading.displayName = "Heading"
|
||||
|
||||
export { Heading }
|
||||
26
components/united/hero-overlay.tsx
Normal file
26
components/united/hero-overlay.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface HeroOverlayProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const HeroOverlay = React.forwardRef<HTMLDivElement, HeroOverlayProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"w-[min(620px,90vw)] p-[clamp(2rem,4vw,3.5rem)]",
|
||||
"bg-[rgba(242,227,208,0.85)]",
|
||||
"rounded-[24px]",
|
||||
"border border-[rgba(36,27,22,0.08)]",
|
||||
"shadow-[var(--shadow-lg)]",
|
||||
"backdrop-blur-[12px] backdrop-saturate-[110%]",
|
||||
"text-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
HeroOverlay.displayName = "HeroOverlay"
|
||||
|
||||
export { HeroOverlay }
|
||||
79
components/united/identity-section.tsx
Normal file
79
components/united/identity-section.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import { StickySplit } from "./sticky-split"
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" // Import from ui/card to check compat or united/card?
|
||||
// Wait, I moved united/card to components/united/card.tsx.
|
||||
// United card has variants. Utils/shadcn card does not in the same way.
|
||||
import { Card as UnitedCard, CardHeader as UnitedCardHeader, CardTitle as UnitedCardTitle, CardContent as UnitedCardContent } from "./card"
|
||||
import { ColorSwatch } from "./color-swatch"
|
||||
import { SectionLabel } from "./section-label"
|
||||
import { Reveal } from "./reveal"
|
||||
|
||||
export function IdentitySection() {
|
||||
return (
|
||||
<section className="py-[clamp(3.5rem,6vw,6rem)] px-[clamp(1.5rem,4vw,5rem)] max-w-[1600px] mx-auto relative bg-[var(--sand)]/30">
|
||||
<StickySplit
|
||||
sidebar={
|
||||
<div className="space-y-6">
|
||||
<SectionLabel>02 • The Studio</SectionLabel>
|
||||
<h2 className="font-serif text-[clamp(1.9rem,4vw,3rem)] leading-[1.15] text-[var(--ink)]">
|
||||
A space for art, not just ink.
|
||||
</h2>
|
||||
<p className="text-[clamp(0.95rem,2vw,1.3rem)] leading-[1.65] text-[var(--ink)]/75 max-w-[54ch]">
|
||||
Located in Fountain, Colorado, United Tattoo is a collective of artists dedicated to the craft.
|
||||
We prioritize custom work, clean lines, and a welcoming environment.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-8">
|
||||
<Reveal>
|
||||
<UnitedCard variant="default" className="backdrop-blur-sm bg-white/60">
|
||||
<UnitedCardHeader>
|
||||
<UnitedCardTitle>The Philosophy</UnitedCardTitle>
|
||||
<span className="text-[0.7rem] uppercase tracking-[0.25em] opacity-80">Est. 2012</span>
|
||||
</UnitedCardHeader>
|
||||
<UnitedCardContent>
|
||||
<p className="text-[var(--ink)]/80 mb-8 leading-relaxed">
|
||||
We believe in tattoos that age as well as you do. Our process is collaborative,
|
||||
transparent, and focused on creating lasting pieces of art.
|
||||
</p>
|
||||
|
||||
{/* Decorative swatches to mimic design system look */}
|
||||
<div className="hidden md:grid grid-cols-3 gap-4">
|
||||
<div className="h-24 rounded-xl bg-[var(--burnt-orange)] flex items-end p-4 text-white text-xs uppercase tracking-wider font-medium">Bold</div>
|
||||
<div className="h-24 rounded-xl bg-[var(--sage)] flex items-end p-4 text-white text-xs uppercase tracking-wider font-medium">Clean</div>
|
||||
<div className="h-24 rounded-xl bg-[var(--charcoal)] flex items-end p-4 text-white text-xs uppercase tracking-wider font-medium">Lasting</div>
|
||||
</div>
|
||||
</UnitedCardContent>
|
||||
</UnitedCard>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.1}>
|
||||
<UnitedCard variant="component" className="bg-white/80 border-dashed border-[var(--sage-concrete)]/40">
|
||||
<UnitedCardHeader>
|
||||
<UnitedCardTitle>The Space</UnitedCardTitle>
|
||||
</UnitedCardHeader>
|
||||
<UnitedCardContent className="space-y-4">
|
||||
<p className="leading-relaxed">
|
||||
Our studio is designed to be a sanctuary. Bright, open, and professional.
|
||||
We strictly adhere to the highest standards of safety and sterilization.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||
<div className="p-4 rounded-lg bg-[var(--sand)]/50 border border-[var(--ink)]/5">
|
||||
<span className="block text-xs uppercase tracking-widest text-[var(--moss)] mb-1">Location</span>
|
||||
<span className="font-medium">Fountain, CO</span>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-[var(--sand)]/50 border border-[var(--ink)]/5">
|
||||
<span className="block text-xs uppercase tracking-widest text-[var(--moss)] mb-1">Atmosphere</span>
|
||||
<span className="font-medium">Private & Relaxed</span>
|
||||
</div>
|
||||
</div>
|
||||
</UnitedCardContent>
|
||||
</UnitedCard>
|
||||
</Reveal>
|
||||
</div>
|
||||
</StickySplit>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
24
components/united/immersion-section.tsx
Normal file
24
components/united/immersion-section.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import { Filmstrip } from "./filmstrip"
|
||||
|
||||
const immersionImages = [
|
||||
{ src: "/images/UP1_00007_.png", label: "Monument Prep" },
|
||||
{ src: "/images/UP1_00009_.png", label: "Avian Story" },
|
||||
{ src: "/images/UP1_00010_.png", label: "Architectural Study" },
|
||||
{ src: "/images/UP1_00018_.png", label: "Liberty Detail" },
|
||||
{ src: "/images/0_1.png", label: "Warm Plaster" },
|
||||
{ src: "/images/0_3.png", label: "Shadow Glyph" },
|
||||
]
|
||||
|
||||
export function ImmersionSection() {
|
||||
return (
|
||||
<section className="relative">
|
||||
<Filmstrip
|
||||
items={immersionImages}
|
||||
title="Sunbleached walls & charcoal studies."
|
||||
subtitle="01 • Immersion Gallery"
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
35
components/united/index.ts
Normal file
35
components/united/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// Core Typography
|
||||
export { Heading } from "./heading"
|
||||
export { LeadText } from "./lead-text"
|
||||
export { SectionLabel } from "./section-label"
|
||||
export { Metadata } from "./metadata"
|
||||
|
||||
// Interactive Elements
|
||||
export { Button } from "./button"
|
||||
export { AnimatedLink } from "./animated-link"
|
||||
|
||||
// Cards
|
||||
export { Card, CardHeader, CardTitle, CardContent } from "./card"
|
||||
export { LiftCard } from "./lift-card"
|
||||
export { MotionCard } from "./motion-card"
|
||||
export { GalleryCard } from "./gallery-card"
|
||||
|
||||
// Form Components
|
||||
export { FormContainer } from "./form-container"
|
||||
export { FormField, Input, Textarea } from "./form-field"
|
||||
export { Calendar } from "./calendar"
|
||||
|
||||
// Feedback
|
||||
export { Toast } from "./toast"
|
||||
|
||||
// Design System
|
||||
export { ColorSwatch } from "./color-swatch"
|
||||
|
||||
// Layout
|
||||
export { HeroOverlay } from "./hero-overlay"
|
||||
export { StickySplit } from "./sticky-split"
|
||||
export { Divider } from "./divider"
|
||||
export { Filmstrip } from "./filmstrip"
|
||||
|
||||
// Animation
|
||||
export { Reveal } from "./reveal"
|
||||
21
components/united/lead-text.tsx
Normal file
21
components/united/lead-text.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface LeadTextProps extends React.HTMLAttributes<HTMLParagraphElement> {}
|
||||
|
||||
const LeadText = React.forwardRef<HTMLParagraphElement, LeadTextProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-[clamp(0.95rem,2vw,1.3rem)] text-[rgba(31,27,23,0.75)]",
|
||||
"max-w-[54ch] leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
LeadText.displayName = "LeadText"
|
||||
|
||||
export { LeadText }
|
||||
28
components/united/lift-card.tsx
Normal file
28
components/united/lift-card.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface LiftCardProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const LiftCard = React.forwardRef<HTMLDivElement, LiftCardProps>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-6 bg-white rounded-[12px]",
|
||||
"shadow-[var(--shadow-subtle)]",
|
||||
"transition-all duration-300 ease-out",
|
||||
"hover:-translate-y-1 hover:shadow-[var(--shadow-bloom)]",
|
||||
"cursor-default",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
LiftCard.displayName = "LiftCard"
|
||||
|
||||
export { LiftCard }
|
||||
11
components/united/metadata.tsx
Normal file
11
components/united/metadata.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface MetadataProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
||||
|
||||
const Metadata = React.forwardRef<HTMLSpanElement, MetadataProps>(({ className, ...props }, ref) => {
|
||||
return <span ref={ref} className={cn("text-[0.7rem] uppercase tracking-[0.25em] opacity-80", className)} {...props} />
|
||||
})
|
||||
Metadata.displayName = "Metadata"
|
||||
|
||||
export { Metadata }
|
||||
28
components/united/motion-card.tsx
Normal file
28
components/united/motion-card.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface MotionCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title: string
|
||||
description: string
|
||||
highlight?: "burnt" | "terracotta"
|
||||
}
|
||||
|
||||
const MotionCard = React.forwardRef<HTMLDivElement, MotionCardProps>(
|
||||
({ className, title, description, highlight = "burnt", ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("p-6 bg-white rounded-[18px]", "shadow-[var(--shadow-subtle)]", className)}
|
||||
{...props}
|
||||
>
|
||||
<strong className={cn("block", highlight === "burnt" ? "text-[var(--burnt)]" : "text-[var(--terracotta)]")}>
|
||||
{title}
|
||||
</strong>
|
||||
<p className="text-[0.9rem] mt-2 mb-0 opacity-80">{description}</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
MotionCard.displayName = "MotionCard"
|
||||
|
||||
export { MotionCard }
|
||||
98
components/united/new-artists-section.tsx
Normal file
98
components/united/new-artists-section.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import Link from "next/link"
|
||||
import { StickySplit } from "./sticky-split"
|
||||
import { SectionLabel } from "./section-label"
|
||||
import { Reveal } from "./reveal"
|
||||
import { Button } from "@/components/ui/button" // Shadcn button for variety or United button
|
||||
// Use United Button
|
||||
import { Button as UnitedButton } from "./button"
|
||||
|
||||
import { artists as staticArtists } from "@/data/artists"
|
||||
import { useActiveArtists } from "@/hooks/use-artists"
|
||||
|
||||
export function NewArtistsSection() {
|
||||
// Fetch artists logic
|
||||
const { data: dbArtistsData, isLoading, error } = useActiveArtists()
|
||||
|
||||
const artists = useMemo(() => {
|
||||
if (isLoading || error || !dbArtistsData) {
|
||||
return staticArtists
|
||||
}
|
||||
return staticArtists.map(staticArtist => {
|
||||
const dbArtist = dbArtistsData.artists.find(
|
||||
(db) => db.slug === staticArtist.slug || db.name === staticArtist.name
|
||||
)
|
||||
if (dbArtist && dbArtist.portfolioImages.length > 0) {
|
||||
return {
|
||||
...staticArtist,
|
||||
workImages: dbArtist.portfolioImages.map(img => img.url)
|
||||
}
|
||||
}
|
||||
return staticArtist
|
||||
})
|
||||
}, [dbArtistsData, isLoading, error])
|
||||
|
||||
return (
|
||||
<section id="artists" className="py-[clamp(3.5rem,6vw,6rem)] px-[clamp(1.5rem,4vw,5rem)] max-w-[1600px] mx-auto">
|
||||
<StickySplit
|
||||
sidebar={
|
||||
<div className="space-y-6">
|
||||
<SectionLabel>03 • The Team</SectionLabel>
|
||||
<h2 className="font-serif text-[clamp(1.9rem,4vw,3rem)] leading-[1.15] text-[var(--ink)]">
|
||||
Resident Artists
|
||||
</h2>
|
||||
<p className="text-[clamp(0.95rem,2vw,1.3rem)] leading-[1.65] text-[var(--ink)]/75 mb-8">
|
||||
Each artist brings a unique style and perspective. From realism to traditional, we have a specialist for your idea.
|
||||
</p>
|
||||
<div className="hidden md:block">
|
||||
<Link href="/book">
|
||||
<UnitedButton variant="primary" className="w-full md:w-auto">
|
||||
Book an Artist
|
||||
</UnitedButton>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{artists.map((artist, i) => (
|
||||
<Reveal key={artist.id} delay={i * 0.1}>
|
||||
<Link href={"/artists/" + artist.slug} className="group block h-full">
|
||||
<div className="relative overflow-hidden rounded-[24px] bg-[var(--sage-concrete)] aspect-[3/4] shadow-[var(--shadow-lg)] transition-all duration-500 group-hover:-translate-y-2 group-hover:shadow-[var(--shadow-bloom)]">
|
||||
{/* Image */}
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={artist.name}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-60 group-hover:opacity-80 transition-opacity duration-300" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 text-white transform translate-y-2 group-hover:translate-y-0 transition-transform duration-300">
|
||||
<span className="block text-[0.7rem] uppercase tracking-[0.25em] mb-2 opacity-90">
|
||||
{artist.specialty}
|
||||
</span>
|
||||
<h3 className="font-serif text-2xl mb-2">{artist.name}</h3>
|
||||
<div className="h-1 w-12 bg-[var(--terracotta)] rounded-full transform scale-x-0 group-hover:scale-x-100 origin-left transition-transform duration-300 delay-100" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-10 md:hidden">
|
||||
<Link href="/book">
|
||||
<UnitedButton variant="primary" className="w-full">
|
||||
Book an Artist
|
||||
</UnitedButton>
|
||||
</Link>
|
||||
</div>
|
||||
</StickySplit>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
141
components/united/new-contact-section.tsx
Normal file
141
components/united/new-contact-section.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { StickySplit } from "./sticky-split"
|
||||
import { SectionLabel } from "./section-label"
|
||||
import { FormContainer } from "./form-container"
|
||||
import { FormField, Input, Textarea } from "./form-field"
|
||||
import { Button } from "./button"
|
||||
import { MapPin, Phone, Mail, Clock } from "lucide-react"
|
||||
|
||||
export function NewContactSection() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
message: "",
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
// In a real app, you'd handle submission here or use server actions
|
||||
console.log("Form submitted:", formData)
|
||||
alert("Thank you. We will be in touch shortly.")
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contact" className="py-[clamp(3.5rem,6vw,6rem)] px-[clamp(1.5rem,4vw,5rem)] max-w-[1600px] mx-auto">
|
||||
<StickySplit
|
||||
sidebar={
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<SectionLabel>04 • Booking</SectionLabel>
|
||||
<h2 className="font-serif text-[clamp(1.9rem,4vw,3rem)] leading-[1.15] text-[var(--ink)] mb-4">
|
||||
Begin your commission.
|
||||
</h2>
|
||||
<p className="text-[clamp(0.95rem,2vw,1.3rem)] leading-[1.65] text-[var(--ink)]/75">
|
||||
Ready to create something amazing? Fill out the brief to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-8 border-t border-[var(--ink)]/10">
|
||||
{[
|
||||
{
|
||||
icon: MapPin,
|
||||
title: "Visit Us",
|
||||
content: "5160 Fontaine Blvd, Fountain, CO 80817",
|
||||
},
|
||||
{
|
||||
icon: Phone,
|
||||
title: "Call Us",
|
||||
content: "(719) 698-9004",
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
title: "Email Us",
|
||||
content: "info@united-tattoo.com",
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: "Hours",
|
||||
content: "Mon-Wed: 10AM-6PM, Thu-Sat: 10AM-8PM, Sun: 10AM-6PM",
|
||||
},
|
||||
].map((item, index) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={index} className="flex items-start space-x-4 text-left">
|
||||
<div className="p-2 bg-[var(--white)] rounded-lg shadow-sm">
|
||||
<Icon className="w-4 h-4 text-[var(--burnt)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[var(--ink)] font-medium text-sm uppercase tracking-wider mb-1">{item.title}</p>
|
||||
<p className="text-[var(--ink)]/70 text-sm">{item.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormContainer onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField label="Full Name">
|
||||
<Input
|
||||
name="name"
|
||||
placeholder="Eden Morales"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Phone">
|
||||
<Input
|
||||
name="phone"
|
||||
type="tel"
|
||||
placeholder="(555) 123-4567"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Email Address">
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="eden@example.com"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Concept Notes">
|
||||
<Textarea
|
||||
name="message"
|
||||
rows={5}
|
||||
placeholder="Describe your idea, placement, and scale..."
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button type="submit" variant="primary" className="w-full md:w-auto">
|
||||
Submit Brief
|
||||
</Button>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</StickySplit>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
75
components/united/new-hero.tsx
Normal file
75
components/united/new-hero.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import { useRef } from "react"
|
||||
import { motion, useScroll, useTransform } from "framer-motion"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Link from "next/link"
|
||||
|
||||
export function NewHero() {
|
||||
const ref = useRef<HTMLElement>(null)
|
||||
const { scrollY } = useScroll()
|
||||
const y = useTransform(scrollY, [0, 1000], [0, 200])
|
||||
const opacity = useTransform(scrollY, [0, 300], [1, 0])
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={ref}
|
||||
className="relative h-[95vh] flex flex-col justify-end pb-[10vh] overflow-hidden"
|
||||
>
|
||||
{/* Parallax Background */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-cover bg-center z-0"
|
||||
style={{
|
||||
backgroundImage: "url('/images/UP1_00010_.png')",
|
||||
y,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Gradient Scrim for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[var(--sand)] to-transparent z-1" style={{
|
||||
backgroundImage: "linear-gradient(to top, var(--sand) 0%, rgba(242, 227, 208, 0.85) 20%, rgba(242, 227, 208, 0.5) 40%, transparent 100%)"
|
||||
}} />
|
||||
|
||||
{/* Editorial Content */}
|
||||
<motion.div
|
||||
className="relative z-10 px-[clamp(1.5rem,5vw,5rem)] w-full max-w-[1800px] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-8 items-end"
|
||||
style={{ opacity }}
|
||||
>
|
||||
<div className="lg:col-span-8">
|
||||
<span className="block text-[0.85rem] uppercase tracking-[0.4em] text-[var(--burnt)] font-bold mb-6 animate-fade-in-up">
|
||||
Fountain • Colorado
|
||||
</span>
|
||||
<h1 className="font-serif text-[clamp(3.5rem,8vw,8.5rem)] leading-[0.9] text-[var(--ink)] tracking-tight mb-8 animate-fade-in-up [animation-delay:100ms]">
|
||||
UNITED<br/>
|
||||
<span className="italic text-[var(--charcoal)]/80">TATTOO</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 pb-4 animate-fade-in-up [animation-delay:200ms]">
|
||||
<p className="text-[1.1rem] leading-[1.7] text-[var(--ink)]/80 mb-8 max-w-[34ch] border-l border-[var(--terracotta)] pl-6">
|
||||
A creative collective specializing in custom narrative work, fine line, and traditional tattooing.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 pl-6">
|
||||
<Link href="/book">
|
||||
<Button
|
||||
className="bg-[var(--burnt)] text-white hover:bg-[var(--terracotta)] uppercase tracking-widest px-8 py-6 h-auto rounded-full shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1 w-full sm:w-auto"
|
||||
>
|
||||
Commission
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/artists">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[var(--ink)] hover:bg-[var(--ink)]/5 uppercase tracking-widest px-8 py-6 h-auto rounded-full transition-all duration-300 border border-[var(--ink)]/10 hover:border-[var(--ink)]/30 w-full sm:w-auto"
|
||||
>
|
||||
Portfolio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
175
components/united/new-navigation.tsx
Normal file
175
components/united/new-navigation.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ name: "Artists", href: "/artists" },
|
||||
{ name: "Your Deposit", href: "/deposit" },
|
||||
{ name: "Aftercare", href: "/aftercare" },
|
||||
{ name: "Contact", href: "/contact" },
|
||||
];
|
||||
|
||||
export function Navigation() {
|
||||
const pathname = usePathname();
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 24);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav
|
||||
className={cn(
|
||||
"fixed top-0 left-0 right-0 z-50 transition-all duration-500 ease-out",
|
||||
isScrolled
|
||||
? "bg-sand/98 backdrop-blur-md shadow-sm border-b border-ink/5"
|
||||
: "bg-transparent",
|
||||
)}
|
||||
>
|
||||
<div className="max-w-[1600px] mx-auto px-[clamp(1.5rem,4vw,5rem)]">
|
||||
<div className="flex items-center justify-between py-6">
|
||||
{/* Logo - Metadata Style */}
|
||||
<Link href="/" className="group">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span
|
||||
className="font-playfair text-2xl tracking-tight text-charcoal transition-opacity duration-300 group-hover:opacity-70"
|
||||
style={{ letterSpacing: "-0.02em" }}
|
||||
>
|
||||
United Tattoo
|
||||
</span>
|
||||
<span className="font-grotesk text-[0.65rem] uppercase tracking-[0.3em] text-moss opacity-80">
|
||||
Fountain, CO
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation - Whisper Metadata */}
|
||||
<div className="hidden lg:flex items-center gap-12">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="group relative"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"font-grotesk text-[0.75rem] uppercase tracking-[0.3em] font-medium transition-all duration-300",
|
||||
isActive ? "text-charcoal" : "text-charcoal/70 hover:text-charcoal",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -bottom-1 left-0 h-[1px] bg-burnt transition-all duration-300 origin-left",
|
||||
isActive ? "w-full" : "w-0 group-hover:w-full",
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Book CTA - Warm Stamp */}
|
||||
<Link
|
||||
href="/book"
|
||||
className="group relative"
|
||||
onMouseEnter={(e) => {
|
||||
const btn = e.currentTarget.querySelector("button");
|
||||
if (btn) {
|
||||
btn.style.transform = "translateY(-1px) scale(1.03)";
|
||||
btn.style.boxShadow = "0 14px 28px rgba(176, 71, 30, 0.35)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const btn = e.currentTarget.querySelector("button");
|
||||
if (btn) {
|
||||
btn.style.transform = "none";
|
||||
btn.style.boxShadow = "0 10px 22px rgba(176, 71, 30, 0.25)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="font-grotesk text-[0.85rem] uppercase tracking-[0.2em] font-medium px-8 py-3 transition-all duration-200"
|
||||
style={{
|
||||
backgroundColor: "#b0471e",
|
||||
color: "#ffffff",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0 10px 22px rgba(176, 71, 30, 0.25)",
|
||||
}}
|
||||
>
|
||||
Book Now
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<button
|
||||
className="lg:hidden p-2 text-charcoal hover:text-burnt transition-colors duration-200"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={mobileMenuOpen}
|
||||
>
|
||||
{mobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="lg:hidden pb-8 border-t border-ink/5">
|
||||
<div className="pt-6 space-y-1">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={cn(
|
||||
"block py-3 px-4 font-grotesk text-sm uppercase tracking-[0.25em] transition-all duration-200",
|
||||
isActive
|
||||
? "text-charcoal border-l-2 border-burnt pl-6"
|
||||
: "text-charcoal/70 hover:text-charcoal hover:pl-6",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Mobile Book Button */}
|
||||
<div className="pt-6">
|
||||
<Link href="/book" onClick={() => setMobileMenuOpen(false)}>
|
||||
<button
|
||||
className="w-full font-grotesk text-base uppercase tracking-[0.2em] font-medium px-8 py-4"
|
||||
style={{
|
||||
backgroundColor: "#b0471e",
|
||||
color: "#ffffff",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0 10px 22px rgba(176, 71, 30, 0.25)",
|
||||
}}
|
||||
>
|
||||
Book Now
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
56
components/united/reveal.tsx
Normal file
56
components/united/reveal.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
export interface RevealProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
delay?: number
|
||||
}
|
||||
|
||||
const Reveal = React.forwardRef<HTMLDivElement, RevealProps>(({ className, delay = 0, children, ...props }, ref) => {
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => setIsVisible(true), delay)
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.15, rootMargin: "0px 0px -50px 0px" },
|
||||
)
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [delay])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(node) => {
|
||||
elementRef.current = node
|
||||
if (typeof ref === "function") ref(node)
|
||||
else if (ref) ref.current = node
|
||||
}}
|
||||
className={cn(
|
||||
"transition-all duration-[0.8s] ease-out",
|
||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-[60px]",
|
||||
className,
|
||||
)}
|
||||
style={{ transitionDelay: `${delay}ms` }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Reveal.displayName = "Reveal"
|
||||
|
||||
export { Reveal }
|
||||
27
components/united/section-label.tsx
Normal file
27
components/united/section-label.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface SectionLabelProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
number?: string
|
||||
}
|
||||
|
||||
const SectionLabel = React.forwardRef<HTMLSpanElement, SectionLabelProps>(
|
||||
({ className, number, children, ...props }, ref) => {
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"block text-[0.75rem] font-semibold uppercase tracking-[0.3em] text-[var(--moss)] mb-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{number && <>{number} • </>}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
)
|
||||
SectionLabel.displayName = "SectionLabel"
|
||||
|
||||
export { SectionLabel }
|
||||
29
components/united/sticky-split.tsx
Normal file
29
components/united/sticky-split.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface StickySplitProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
sidebar: React.ReactNode
|
||||
sidebarClassName?: string
|
||||
}
|
||||
|
||||
const StickySplit = React.forwardRef<HTMLDivElement, StickySplitProps>(
|
||||
({ className, sidebar, sidebarClassName, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid gap-[clamp(1.5rem,4vw,4rem)]",
|
||||
"grid-cols-1 md:grid-cols-[minmax(260px,0.85fr)_minmax(320px,1fr)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<aside className={cn("md:sticky md:top-20 md:self-start", sidebarClassName)}>{sidebar}</aside>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
StickySplit.displayName = "StickySplit"
|
||||
|
||||
export { StickySplit }
|
||||
38
components/united/toast.tsx
Normal file
38
components/united/toast.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface ToastProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "success" | "alert"
|
||||
title: string
|
||||
description?: string
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
|
||||
({ className, variant = "success", title, description, icon, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center gap-5 px-5 py-4 rounded-[14px] text-white",
|
||||
"shadow-[var(--shadow-md)]",
|
||||
variant === "success" && "bg-[var(--sage)]",
|
||||
variant === "alert" && "bg-[var(--rose)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-white/20 grid place-items-center font-semibold">
|
||||
{icon || (variant === "success" ? "✓" : "!")}
|
||||
</div>
|
||||
<div>
|
||||
<strong className="block text-[0.9rem]">{title}</strong>
|
||||
{description && <span className="text-[0.8rem] opacity-90">{description}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
Toast.displayName = "Toast"
|
||||
|
||||
export { Toast }
|
||||
Loading…
x
Reference in New Issue
Block a user