first commit
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import Link from "next/link";
|
||||
import { MapPin, Phone, Mail } from "lucide-react";
|
||||
import { Locale } from "../dictionaries";
|
||||
|
||||
export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
|
||||
return (
|
||||
<footer className="bg-[var(--color-moy-dark)] text-white pt-16 pb-8 border-t border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12">
|
||||
{/* Brand */}
|
||||
<div className="space-y-4">
|
||||
<img src="/logo.png" alt="Moy Group Logo" className="h-12 w-auto mb-6" />
|
||||
<p className="text-gray-400 text-sm leading-relaxed">
|
||||
{dict.description}
|
||||
</p>
|
||||
<div className="flex gap-4 pt-4">
|
||||
<a href="#" className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="20" x="2" y="2" rx="5" ry="5" /><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" /><line x1="17.5" x2="17.51" y1="6.5" y2="6.5" /></svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h3 className="text-lg font-serif mb-6 text-white uppercase tracking-wider">{dict.quickLinks}</h3>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<Link href={`/${lang}/kurumsal`} className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors text-sm">{dict.about}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={`/${lang}/galeri`} className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors text-sm">Galeri</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link href={`/${lang}/iletisim`} className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors text-sm">{dict.contactInfo}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Locations */}
|
||||
<div>
|
||||
<h3 className="text-lg font-serif mb-6 text-white uppercase tracking-wider">{dict.ourVenues}</h3>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<Link href={`/${lang}/#mekanlar`} className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors text-sm">Moy Beach</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={`/${lang}/#mekanlar`} className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors text-sm">Kite Beach Hotel</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={`/${lang}/#mekanlar`} className="text-gray-400 hover:text-[var(--color-moy-gold)] transition-colors text-sm">Volkswagen Kite</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<h3 className="text-lg font-serif mb-6 text-white uppercase tracking-wider">{dict.contactInfo}</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPin size={18} className="text-[var(--color-moy-gold)] shrink-0 mt-0.5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-sm mb-1">Moy Beach</span>
|
||||
<span className="text-gray-400 text-xs">Atatürk 1. Cd No:86, Akyaka, Ula/Muğla</span>
|
||||
<a href="tel:+905344656235" className="text-[var(--color-moy-gold)] hover:text-white transition-colors text-xs mt-1">+90 534 465 62 35</a>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPin size={18} className="text-[var(--color-moy-gold)] shrink-0 mt-0.5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-sm mb-1">Kite Beach Hotel</span>
|
||||
<span className="text-gray-400 text-xs">Akçapınar Sok. Kiteboard Sahili, Muğla</span>
|
||||
<a href="tel:+905330816181" className="text-[var(--color-moy-gold)] hover:text-white transition-colors text-xs mt-1">+90 533 081 61 81</a>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPin size={18} className="text-[var(--color-moy-gold)] shrink-0 mt-0.5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-sm mb-1">Volkswagen Kite</span>
|
||||
<span className="text-gray-400 text-xs">Akçapınar Kite Sahili, Ula/Muğla</span>
|
||||
<div className="flex flex-col gap-1 mt-1">
|
||||
<a href="tel:+905378823137" className="text-[var(--color-moy-gold)] hover:text-white transition-colors text-xs">+90 537 882 31 37</a>
|
||||
<a href="tel:+905052049856" className="text-[var(--color-moy-gold)] hover:text-white transition-colors text-xs">+90 505 204 98 56</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom */}
|
||||
<div className="pt-8 border-t border-gray-800 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-gray-500 text-xs">
|
||||
© {new Date().getFullYear()} Moy Group. {dict.rights}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<a href="https://ayris.tech" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-[var(--color-moy-gold)] transition-colors text-xs flex items-center gap-1">
|
||||
created by <span className="font-medium">ayris.tech</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, X, Globe } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Locale } from "../dictionaries";
|
||||
|
||||
export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const isHome = pathname === `/${lang}` || pathname === `/${lang}/`;
|
||||
|
||||
const navLinks = [
|
||||
{ name: dict.home, href: `/${lang}` },
|
||||
{ name: dict.corporate, href: `/${lang}/kurumsal` },
|
||||
{ name: dict.venues, href: `/${lang}/#mekanlar` },
|
||||
{ name: dict.gallery, href: `/${lang}/galeri` },
|
||||
{ name: dict.contact, href: `/${lang}/iletisim` },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 50);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const switchLanguage = (newLang: string) => {
|
||||
if (!pathname) return '/';
|
||||
// Replace the current lang code in the URL with the new one
|
||||
const segments = pathname.split('/');
|
||||
if (segments[1] === 'tr' || segments[1] === 'en') {
|
||||
segments[1] = newLang;
|
||||
} else {
|
||||
segments.splice(1, 0, newLang);
|
||||
}
|
||||
return segments.join('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 w-full z-50 transition-all duration-300 ${
|
||||
isScrolled || !isHome
|
||||
? "bg-[var(--color-moy-dark)]/90 backdrop-blur-md py-4 shadow-lg"
|
||||
: "bg-transparent py-6"
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Logo */}
|
||||
<Link href={`/${lang}`} className="flex-shrink-0">
|
||||
<img src="/logo.png" alt="Moy Group Logo" className="h-10 w-auto" />
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
className="text-white hover:text-[var(--color-moy-gold)] transition-colors text-sm uppercase tracking-wider font-medium"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Language Switcher */}
|
||||
<div className="flex items-center gap-2 ml-4 border-l border-gray-600 pl-4">
|
||||
<Globe size={16} className="text-[var(--color-moy-gold)]" />
|
||||
<Link href={switchLanguage('tr')} className={`text-sm font-medium ${lang === 'tr' ? 'text-white' : 'text-gray-400 hover:text-white'}`}>TR</Link>
|
||||
<span className="text-gray-600">|</span>
|
||||
<Link href={switchLanguage('en')} className={`text-sm font-medium ${lang === 'en' ? 'text-white' : 'text-gray-400 hover:text-white'}`}>EN</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
className="md:hidden text-white"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? <X size={28} /> : <Menu size={28} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Nav */}
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="absolute top-full left-0 w-full bg-[var(--color-moy-dark)] border-t border-gray-800 shadow-xl md:hidden"
|
||||
>
|
||||
<div className="px-4 pt-2 pb-6 space-y-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="block px-3 py-4 text-white border-b border-gray-800 hover:text-[var(--color-moy-gold)] uppercase tracking-wider text-sm"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex items-center gap-4 px-3 py-4 text-white">
|
||||
<Globe size={18} className="text-[var(--color-moy-gold)]" />
|
||||
<Link href={switchLanguage('tr')} onClick={() => setMobileMenuOpen(false)} className={`text-sm ${lang === 'tr' ? 'text-[var(--color-moy-gold)] font-bold' : ''}`}>Türkçe</Link>
|
||||
<Link href={switchLanguage('en')} onClick={() => setMobileMenuOpen(false)} className={`text-sm ${lang === 'en' ? 'text-[var(--color-moy-gold)] font-bold' : ''}`}>English</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AnimatedNumberProps {
|
||||
value: number;
|
||||
suffix?: string;
|
||||
prefix?: string;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AnimatedNumber({
|
||||
value,
|
||||
suffix = "",
|
||||
prefix = "",
|
||||
duration = 1800,
|
||||
className,
|
||||
}: AnimatedNumberProps) {
|
||||
const [count, setCount] = useState(0);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView) return;
|
||||
|
||||
let rafId: number;
|
||||
let startTime: number | null = null;
|
||||
|
||||
const step = (ts: number) => {
|
||||
if (!startTime) startTime = ts;
|
||||
const progress = Math.min((ts - startTime) / duration, 1);
|
||||
// Ease-out cubic
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
setCount(Math.round(eased * value));
|
||||
if (progress < 1) rafId = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
rafId = requestAnimationFrame(step);
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [isInView, value, duration]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className={cn(className)}>
|
||||
{prefix}{count}{suffix}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Variant = "gold" | "blue" | "green" | "dark" | "light";
|
||||
|
||||
interface BadgeProps {
|
||||
variant?: Variant;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
gold: "bg-[var(--color-moy-gold-faint)] text-[var(--color-moy-gold-dark)] border border-[var(--color-moy-gold-subtle)]",
|
||||
blue: "bg-[var(--color-moy-blue)]/10 text-[var(--color-moy-blue)] border border-[var(--color-moy-blue)]/20",
|
||||
green: "bg-[var(--color-moy-green)]/10 text-[var(--color-moy-green)] border border-[var(--color-moy-green)]/20",
|
||||
dark: "bg-[var(--color-moy-dark)] text-white border border-[var(--color-moy-dark-border)]",
|
||||
light: "bg-white text-[var(--color-moy-dark)] border border-gray-200",
|
||||
};
|
||||
|
||||
export function Badge({ variant = "gold", children, className }: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] rounded-full",
|
||||
variants[variant],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { type ButtonHTMLAttributes, forwardRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Variant = "primary" | "secondary" | "ghost" | "outline-light";
|
||||
type Size = "sm" | "md" | "lg";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
shimmer?: boolean;
|
||||
as?: "button";
|
||||
}
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
primary:
|
||||
"bg-[var(--color-moy-gold)] text-[var(--color-moy-dark)] hover:bg-[var(--color-moy-gold-light)] font-bold shadow-[var(--shadow-gold)] hover:shadow-[var(--shadow-gold-lg)]",
|
||||
secondary:
|
||||
"border border-[var(--color-moy-gold)] text-[var(--color-moy-gold)] hover:bg-[var(--color-moy-gold)] hover:text-[var(--color-moy-dark)] font-medium",
|
||||
ghost:
|
||||
"text-[var(--color-moy-gold)] hover:bg-[var(--color-moy-gold-faint)] font-medium",
|
||||
"outline-light":
|
||||
"border border-white/50 text-white hover:border-white hover:bg-white/10 font-medium",
|
||||
};
|
||||
|
||||
const sizes: Record<Size, string> = {
|
||||
sm: "px-5 py-2 text-[11px] tracking-[0.15em]",
|
||||
md: "px-8 py-3.5 text-[11px] tracking-[0.18em]",
|
||||
lg: "px-10 py-4 text-[11px] tracking-[0.2em]",
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "primary", size = "md", shimmer = false, children, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center uppercase overflow-hidden",
|
||||
"transition-all duration-300",
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{shimmer && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 moy-shimmer opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
/>
|
||||
)}
|
||||
<span className="relative">{children}</span>
|
||||
</button>
|
||||
)
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/* ── Base Card ── */
|
||||
interface CardProps {
|
||||
children: React.ReactNode;
|
||||
hover?: boolean;
|
||||
glass?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Card({ children, hover = true, glass = false, className }: CardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={hover ? { y: -6 } : undefined}
|
||||
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden transition-shadow duration-300",
|
||||
glass
|
||||
? "bg-white/10 backdrop-blur-md border border-white/20"
|
||||
: "bg-white border border-gray-100 shadow-[var(--shadow-sm)] hover:shadow-[var(--shadow-lg)]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Image Card ── */
|
||||
interface ImageCardProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
badge?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ImageCard({ src, alt, title, subtitle, badge, className }: ImageCardProps) {
|
||||
return (
|
||||
<Card className={cn("group cursor-pointer", className)}>
|
||||
<div className="relative h-64 overflow-hidden">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-108"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-400" />
|
||||
{badge && (
|
||||
<span className="absolute top-4 left-4 text-[10px] uppercase tracking-[0.18em] font-semibold px-3 py-1 bg-[var(--color-moy-gold)] text-[var(--color-moy-dark)] rounded-full">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-6 border-t-[1.5px] border-transparent group-hover:border-[var(--color-moy-gold)] transition-[border-color] duration-300">
|
||||
<h3 className="text-xl font-serif text-[var(--color-moy-dark)] mb-2 leading-snug">{title}</h3>
|
||||
{subtitle && (
|
||||
<p className="text-sm text-gray-500 leading-relaxed">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { Button } from "./button";
|
||||
export { Badge } from "./badge";
|
||||
export { SectionHeader } from "./section-header";
|
||||
export { Card, ImageCard } from "./card";
|
||||
export { Input, Textarea } from "./input";
|
||||
export { AnimatedNumber } from "./animated-number";
|
||||
export { ShimmerButton } from "./shimmer-button";
|
||||
export { Marquee } from "./marquee";
|
||||
export { TextReveal } from "./text-reveal";
|
||||
@@ -0,0 +1,68 @@
|
||||
import { forwardRef, type InputHTMLAttributes, type TextareaHTMLAttributes } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/* ── Input ── */
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, label, error, id, ...props }, ref) => (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{label && (
|
||||
<label htmlFor={id} className="text-[10px] font-medium uppercase tracking-[0.16em] text-gray-500">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={cn(
|
||||
"w-full px-4 py-3.5 bg-white border border-gray-200 text-[var(--color-moy-dark)] text-sm",
|
||||
"focus:outline-none focus:ring-2 focus:ring-[var(--color-moy-gold)]/40 focus:border-[var(--color-moy-gold)]",
|
||||
"transition-all duration-200 placeholder:text-gray-300",
|
||||
"rounded-none", // sharp corners = editorial feel
|
||||
error && "border-red-300 focus:ring-red-200/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span className="text-xs text-red-500">{error}</span>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
/* ── Textarea ── */
|
||||
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, label, error, id, ...props }, ref) => (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{label && (
|
||||
<label htmlFor={id} className="text-[10px] font-medium uppercase tracking-[0.16em] text-gray-500">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={cn(
|
||||
"w-full px-4 py-3.5 bg-white border border-gray-200 text-[var(--color-moy-dark)] text-sm resize-none",
|
||||
"focus:outline-none focus:ring-2 focus:ring-[var(--color-moy-gold)]/40 focus:border-[var(--color-moy-gold)]",
|
||||
"transition-all duration-200 placeholder:text-gray-300",
|
||||
"rounded-none",
|
||||
error && "border-red-300 focus:ring-red-200/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span className="text-xs text-red-500">{error}</span>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MarqueeProps {
|
||||
items: string[];
|
||||
separator?: string;
|
||||
speed?: "slow" | "normal" | "fast";
|
||||
reverse?: boolean;
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
separatorClassName?: string;
|
||||
}
|
||||
|
||||
const speeds = { slow: "42s", normal: "28s", fast: "16s" };
|
||||
|
||||
export function Marquee({
|
||||
items,
|
||||
separator = "✦",
|
||||
speed = "normal",
|
||||
reverse = false,
|
||||
className,
|
||||
itemClassName,
|
||||
separatorClassName,
|
||||
}: MarqueeProps) {
|
||||
// Double for seamless infinite loop
|
||||
const doubled = [...items, ...items];
|
||||
|
||||
return (
|
||||
<div className={cn("overflow-hidden whitespace-nowrap select-none", className)}>
|
||||
<div
|
||||
className="inline-flex"
|
||||
style={{
|
||||
animation: `marquee ${speeds[speed]} linear infinite${reverse ? " reverse" : ""}`,
|
||||
willChange: "transform",
|
||||
}}
|
||||
>
|
||||
{doubled.map((item, i) => (
|
||||
<span key={i} className="inline-flex items-center">
|
||||
<span className={cn("px-7 text-[11px] font-medium uppercase tracking-[0.18em]", itemClassName)}>
|
||||
{item}
|
||||
</span>
|
||||
<span className={cn("opacity-50 text-sm", separatorClassName)}>
|
||||
{separator}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
eyebrow?: string;
|
||||
align?: "left" | "center" | "right";
|
||||
light?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SectionHeader({
|
||||
title,
|
||||
subtitle,
|
||||
eyebrow,
|
||||
align = "center",
|
||||
light = false,
|
||||
className,
|
||||
}: SectionHeaderProps) {
|
||||
const alignCls = {
|
||||
left: "items-start text-left",
|
||||
center: "items-center text-center",
|
||||
right: "items-end text-right",
|
||||
}[align];
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col mb-16", alignCls, className)}>
|
||||
{eyebrow && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[10px] font-medium uppercase tracking-[0.22em] mb-5",
|
||||
light ? "text-[var(--color-moy-gold-light)]" : "text-[var(--color-moy-gold-dark)]"
|
||||
)}
|
||||
>
|
||||
{eyebrow}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h2
|
||||
className={cn(
|
||||
"text-4xl md:text-[3.25rem] font-serif leading-tight mb-5",
|
||||
light ? "text-white" : "text-[var(--color-moy-dark)]"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"moy-gold-line",
|
||||
align === "center" && "mx-auto",
|
||||
align === "right" && "ml-auto"
|
||||
)}
|
||||
/>
|
||||
|
||||
{subtitle && (
|
||||
<p
|
||||
className={cn(
|
||||
"mt-6 text-base md:text-lg leading-relaxed max-w-2xl",
|
||||
align === "center" && "mx-auto",
|
||||
light ? "text-gray-300/80" : "text-gray-500"
|
||||
)}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, type ButtonHTMLAttributes } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ShimmerButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
shimmerColor?: string;
|
||||
background?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ShimmerButton = forwardRef<HTMLButtonElement, ShimmerButtonProps>(
|
||||
(
|
||||
{
|
||||
shimmerColor = "rgba(201,169,110,0.55)",
|
||||
background = "var(--color-moy-dark)",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group relative inline-flex items-center justify-center overflow-hidden",
|
||||
"px-10 py-4 text-[11px] font-medium uppercase tracking-[0.2em] text-white",
|
||||
"transition-all duration-300",
|
||||
className
|
||||
)}
|
||||
style={{ background }}
|
||||
{...props}
|
||||
>
|
||||
{/* Animated shimmer strip */}
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background: `linear-gradient(105deg, transparent 35%, ${shimmerColor} 50%, transparent 65%)`,
|
||||
backgroundSize: "200% 100%",
|
||||
animation: "shimmer 2.8s linear infinite",
|
||||
}}
|
||||
/>
|
||||
{/* Inset bg to sit above shimmer */}
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute inset-[1px]"
|
||||
style={{ background }}
|
||||
/>
|
||||
<span className="relative">{children}</span>
|
||||
</button>
|
||||
)
|
||||
);
|
||||
ShimmerButton.displayName = "ShimmerButton";
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TextRevealProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
once?: boolean;
|
||||
stagger?: number;
|
||||
}
|
||||
|
||||
export function TextReveal({
|
||||
text,
|
||||
className,
|
||||
delay = 0,
|
||||
once = true,
|
||||
stagger = 0.055,
|
||||
}: TextRevealProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const isInView = useInView(ref, { once, margin: "-60px" });
|
||||
const words = text.split(" ");
|
||||
|
||||
return (
|
||||
<span ref={ref} className={cn("inline", className)}>
|
||||
{words.map((word, i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className="inline-block mr-[0.28em] last:mr-0"
|
||||
initial={{ opacity: 0, y: 22, filter: "blur(4px)" }}
|
||||
animate={
|
||||
isInView
|
||||
? { opacity: 1, y: 0, filter: "blur(0px)" }
|
||||
: { opacity: 0, y: 22, filter: "blur(4px)" }
|
||||
}
|
||||
transition={{
|
||||
duration: 0.55,
|
||||
delay: delay + i * stagger,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
}}
|
||||
>
|
||||
{word}
|
||||
</motion.span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user