first commit

This commit is contained in:
mstfyldz
2026-06-03 16:13:15 +03:00
parent a3b76fa65d
commit 716bad9d7c
40 changed files with 2698 additions and 135 deletions
+50
View File
@@ -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>
);
}
+31
View File
@@ -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>
);
}
+56
View File
@@ -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";
+66
View File
@@ -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>
);
}
+9
View File
@@ -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";
+68
View File
@@ -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";
+49
View File
@@ -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>
);
}
+69
View File
@@ -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>
);
}
+54
View File
@@ -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";
+49
View File
@@ -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>
);
}