feat: dark luxury redesign with black heart branding

Complete visual overhaul: dark-only mode, rose/crimson accent system,
glassmorphism header, animated hero with floating hearts and glow orbs,
photo-backed cards, infinite team carousel with drag support,
redesigned modals with hero images, black heart logo with rose glow silhouette.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 23:30:10 +03:00
parent 1f6e314af6
commit 9cf09b6894
16 changed files with 762 additions and 244 deletions

View File

@@ -1,21 +1,29 @@
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
import { Heart } from "lucide-react";
export function About() {
const { about } = siteContent;
return (
<section id="about" className="surface-muted section-padding">
<section id="about" className="relative section-padding bg-neutral-100 dark:bg-[#0a0a0a]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container">
<Reveal>
<SectionHeading>{about.title}</SectionHeading>
</Reveal>
<div className="mt-8 max-w-3xl space-y-4">
<div className="mt-10 max-w-3xl space-y-6">
{about.paragraphs.map((text, i) => (
<Reveal key={i}>
<p className="body-text text-lg leading-relaxed">{text}</p>
<div className="flex gap-4">
<Heart
size={20}
className="mt-1 shrink-0 fill-rose-500/20 text-rose-500 dark:fill-rose-500/10 dark:text-rose-400"
/>
<p className="body-text text-lg leading-relaxed">{text}</p>
</div>
</Reveal>
))}
</div>

View File

@@ -1,7 +1,8 @@
"use client";
import { useState } from "react";
import { Flame, Sparkles, Wind, Zap, Star, Monitor } from "lucide-react";
import Image from "next/image";
import { Flame, Sparkles, Wind, Zap, Star, Monitor, ArrowRight } from "lucide-react";
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
@@ -9,12 +10,12 @@ import { ClassModal } from "@/components/ui/ClassModal";
import type { ClassItem } from "@/types";
const iconMap: Record<string, React.ReactNode> = {
flame: <Flame size={32} />,
sparkles: <Sparkles size={32} />,
wind: <Wind size={32} />,
zap: <Zap size={32} />,
star: <Star size={32} />,
monitor: <Monitor size={32} />,
flame: <Flame size={20} />,
sparkles: <Sparkles size={20} />,
wind: <Wind size={20} />,
zap: <Zap size={20} />,
star: <Star size={20} />,
monitor: <Monitor size={20} />,
};
export function Classes() {
@@ -22,22 +23,57 @@ export function Classes() {
const [selectedClass, setSelectedClass] = useState<ClassItem | null>(null);
return (
<section id="classes" className="surface-muted section-padding">
<section id="classes" className="relative section-padding bg-neutral-100 dark:bg-[#0a0a0a]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container">
<Reveal>
<SectionHeading>{classes.title}</SectionHeading>
</Reveal>
<div className="mt-12 grid gap-6 sm:grid-cols-2">
<div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{classes.items.map((item) => (
<Reveal key={item.name} className="h-full">
<div
className="card h-full flex flex-col cursor-pointer transition-transform hover:scale-[1.02]"
className="group relative h-full min-h-[280px] cursor-pointer overflow-hidden rounded-2xl"
onClick={() => setSelectedClass(item)}
>
<div className="heading-text">{iconMap[item.icon]}</div>
<h3 className="heading-text mt-4 text-xl font-semibold">{item.name}</h3>
<p className="body-text mt-2">{item.description}</p>
{/* Background image */}
{item.images && item.images[0] && (
<Image
src={item.images[0]}
alt={item.name}
fill
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/>
)}
{/* Dark gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-black/10 transition-all duration-500 group-hover:from-black/95 group-hover:via-black/50" />
{/* Rose tint on hover */}
<div className="absolute inset-0 bg-rose-900/0 transition-all duration-500 group-hover:bg-rose-900/10" />
{/* Content */}
<div className="relative flex h-full flex-col justify-end p-6">
{/* Icon badge */}
<div className="mb-3 inline-flex h-9 w-9 items-center justify-center rounded-lg bg-white/10 text-white backdrop-blur-sm transition-all duration-300 group-hover:bg-rose-500/20 group-hover:text-rose-300">
{iconMap[item.icon]}
</div>
<h3 className="text-xl font-semibold text-white">
{item.name}
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-white/60 line-clamp-2">
{item.description}
</p>
{/* Hover arrow */}
<div className="mt-3 flex items-center gap-1.5 text-sm font-medium text-rose-400 opacity-0 translate-y-2 transition-all duration-300 group-hover:opacity-100 group-hover:translate-y-0">
<span>Подробнее</span>
<ArrowRight size={14} />
</div>
</div>
</div>
</Reveal>
))}

View File

@@ -8,51 +8,65 @@ export function Contact() {
const { contact } = siteContent;
return (
<section id="contact" className="surface-base section-padding">
<section id="contact" className="relative section-padding bg-neutral-50 dark:bg-[#050505]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container grid items-start gap-12 lg:grid-cols-2">
<Reveal>
<SectionHeading>{contact.title}</SectionHeading>
<div className="mt-12 space-y-6">
<div className="mt-10 space-y-5">
{contact.addresses.map((address, i) => (
<div key={i} className="contact-item">
<MapPin size={20} className="contact-icon" />
<div key={i} className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<MapPin size={18} />
</div>
<p className="body-text">{address}</p>
</div>
))}
<div className="contact-item">
<Phone size={20} className="contact-icon" />
<a href={`tel:${contact.phone}`} className="nav-link text-base">
<div className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<Phone size={18} />
</div>
<a
href={`tel:${contact.phone}`}
className="text-neutral-600 transition-colors hover:text-rose-600 dark:text-neutral-400 dark:hover:text-rose-400"
>
{contact.phone}
</a>
</div>
<div className="contact-item">
<Clock size={20} className="contact-icon" />
<div className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<Clock size={18} />
</div>
<p className="body-text">{contact.workingHours}</p>
</div>
<div className="theme-border contact-item border-t pt-6">
<Instagram size={20} className="contact-icon" />
<a
href={contact.instagram}
target="_blank"
rel="noopener noreferrer"
className="nav-link text-base"
>
{BRAND.instagramHandle}
</a>
<div className="border-t border-neutral-200 pt-5 dark:border-white/[0.06]">
<div className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<Instagram size={18} />
</div>
<a
href={contact.instagram}
target="_blank"
rel="noopener noreferrer"
className="text-neutral-600 transition-colors hover:text-rose-600 dark:text-neutral-400 dark:hover:text-rose-400"
>
{BRAND.instagramHandle}
</a>
</div>
</div>
</div>
</Reveal>
<Reveal>
<div className="theme-border overflow-hidden rounded-2xl border">
<div className="overflow-hidden rounded-2xl border border-neutral-200 shadow-sm dark:border-white/[0.06] dark:shadow-[0_0_30px_rgba(225,29,72,0.05)]">
<iframe
src={contact.mapEmbedUrl}
width="100%"
height="350"
height="380"
style={{ border: 0 }}
allowFullScreen
loading="lazy"

View File

@@ -1,35 +1,99 @@
"use client";
import Image from "next/image";
import { siteContent } from "@/data/content";
import { BRAND } from "@/lib/constants";
import { Button } from "@/components/ui/Button";
import { FloatingHearts } from "@/components/ui/FloatingHearts";
import { ChevronDown } from "lucide-react";
export function Hero() {
const { hero } = siteContent;
return (
<section className="surface-base section-container flex min-h-svh items-center justify-center">
<div className="text-center">
<Image
src="/images/logo.png"
alt={BRAND.name}
width={280}
height={280}
priority
unoptimized
className="hero-logo mx-auto mb-8 dark:invert"
/>
<section className="relative flex min-h-svh items-center justify-center overflow-hidden bg-[#050505]">
{/* Animated gradient background */}
<div className="hero-bg-gradient absolute inset-0" />
{/* Glow orbs */}
<div
className="hero-glow-orb"
style={{
width: "500px",
height: "500px",
top: "-10%",
left: "50%",
transform: "translateX(-50%)",
background: "radial-gradient(circle, rgba(225, 29, 72, 0.12), transparent 70%)",
}}
/>
<div
className="hero-glow-orb"
style={{
width: "300px",
height: "300px",
bottom: "10%",
right: "10%",
background: "radial-gradient(circle, rgba(225, 29, 72, 0.08), transparent 70%)",
animationDelay: "3s",
}}
/>
{/* Floating hearts */}
<FloatingHearts />
{/* Content */}
<div className="section-container relative z-10 text-center">
<div className="hero-logo relative mx-auto mb-10 h-[220px] w-[220px]">
{/* Outer ambient glow */}
<div className="absolute -inset-16 rounded-full bg-rose-500/8 blur-[60px]" />
{/* Rose disc — makes black heart visible as silhouette */}
<div
className="absolute inset-2 rounded-full"
style={{
background: "radial-gradient(circle, rgba(225,29,72,0.45) 0%, rgba(225,29,72,0.18) 45%, transparent 70%)",
}}
/>
<Image
src="/images/logo.png"
alt={BRAND.name}
width={220}
height={220}
priority
unoptimized
className="relative"
style={{
filter:
"drop-shadow(0 0 6px rgba(225,29,72,0.5)) drop-shadow(0 0 20px rgba(225,29,72,0.25))",
}}
/>
</div>
<h1 className="hero-title font-display text-5xl font-bold tracking-tight sm:text-6xl lg:text-8xl">
{hero.headline}
<span className="gradient-text">{hero.headline}</span>
</h1>
<p className="hero-subtitle body-text mx-auto mt-6 max-w-md text-lg sm:text-xl">
<p className="hero-subtitle mx-auto mt-6 max-w-lg text-lg text-neutral-400 sm:text-xl">
{hero.subheadline}
</p>
<div className="hero-cta mt-10">
<div className="hero-cta mt-12">
<Button href={hero.ctaHref} size="lg">
{hero.ctaText}
</Button>
</div>
</div>
{/* Scroll indicator */}
<div className="hero-cta absolute bottom-8 left-1/2 -translate-x-1/2">
<a
href="#about"
className="flex flex-col items-center gap-1 text-neutral-600 transition-colors hover:text-rose-400"
>
<span className="text-xs uppercase tracking-widest">Scroll</span>
<ChevronDown size={20} className="animate-bounce" />
</a>
</div>
</section>
);
}

View File

@@ -1,8 +1,8 @@
"use client";
import { useState } from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import Image from "next/image";
import { Instagram } from "lucide-react";
import { Instagram, ChevronLeft, ChevronRight } from "lucide-react";
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
@@ -12,51 +12,179 @@ import type { TeamMember } from "@/types";
export function Team() {
const { team } = siteContent;
const [selectedMember, setSelectedMember] = useState<TeamMember | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollTimer = useRef<ReturnType<typeof setTimeout>>(null);
const isDragging = useRef(false);
const dragStartX = useRef(0);
const dragScrollLeft = useRef(0);
const dragMoved = useRef(false);
// Render 3 copies: [clone] [original] [clone]
const tripled = [...team.members, ...team.members, ...team.members];
// On mount, jump to the middle set (no animation)
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
requestAnimationFrame(() => {
const cardWidth = el.scrollWidth / 3;
el.scrollLeft = cardWidth;
});
}, []);
// When scroll settles, check if we need to loop
const handleScroll = useCallback(() => {
if (scrollTimer.current) clearTimeout(scrollTimer.current);
scrollTimer.current = setTimeout(() => {
const el = scrollRef.current;
if (!el) return;
const oneSetWidth = el.scrollWidth / 3;
if (el.scrollLeft < oneSetWidth * 0.3) {
el.style.scrollBehavior = "auto";
el.scrollLeft += oneSetWidth;
el.style.scrollBehavior = "";
}
if (el.scrollLeft > oneSetWidth * 1.7) {
el.style.scrollBehavior = "auto";
el.scrollLeft -= oneSetWidth;
el.style.scrollBehavior = "";
}
}, 100);
}, []);
// Mouse drag handlers
function handleMouseDown(e: React.MouseEvent) {
const el = scrollRef.current;
if (!el) return;
isDragging.current = true;
dragMoved.current = false;
dragStartX.current = e.pageX;
dragScrollLeft.current = el.scrollLeft;
el.style.scrollBehavior = "auto";
el.style.scrollSnapType = "none";
el.style.cursor = "grabbing";
}
function handleMouseMove(e: React.MouseEvent) {
if (!isDragging.current || !scrollRef.current) return;
e.preventDefault();
const dx = e.pageX - dragStartX.current;
if (Math.abs(dx) > 3) dragMoved.current = true;
scrollRef.current.scrollLeft = dragScrollLeft.current - dx;
}
function handleMouseUp() {
if (!isDragging.current || !scrollRef.current) return;
isDragging.current = false;
scrollRef.current.style.scrollBehavior = "";
scrollRef.current.style.scrollSnapType = "";
scrollRef.current.style.cursor = "";
}
function handleCardClick(member: TeamMember) {
// Don't open modal if user was dragging
if (dragMoved.current) return;
setSelectedMember(member);
}
function scroll(direction: "left" | "right") {
if (!scrollRef.current) return;
const amount = scrollRef.current.offsetWidth * 0.7;
scrollRef.current.scrollBy({
left: direction === "left" ? -amount : amount,
behavior: "smooth",
});
}
return (
<section id="team" className="surface-base section-padding">
<section id="team" className="relative section-padding bg-neutral-50 dark:bg-[#050505]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container">
<Reveal>
<SectionHeading>{team.title}</SectionHeading>
</Reveal>
</div>
<div className="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{team.members.map((member, i) => (
<Reveal key={i}>
{/* Carousel wrapper */}
<Reveal>
<div className="relative mt-10">
{/* Scroll container */}
<div
ref={scrollRef}
onScroll={handleScroll}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
className="flex cursor-grab gap-4 overflow-x-auto px-6 pb-4 sm:px-8 scroll-smooth snap-x snap-mandatory select-none lg:px-[max(2rem,calc((100vw-72rem)/2+2rem))]"
style={{ scrollbarWidth: "none" }}
>
{tripled.map((member, i) => (
<div
className="card flex h-full cursor-pointer flex-col items-center text-center transition-transform hover:scale-[1.02]"
onClick={() => setSelectedMember(member)}
key={`${i}-${member.name}`}
className="group relative w-[220px] shrink-0 cursor-pointer snap-start overflow-hidden rounded-2xl sm:w-[260px]"
onClick={() => handleCardClick(member)}
>
<div className="mx-auto h-32 w-32 overflow-hidden rounded-full">
{/* Photo */}
<div className="aspect-[3/4] w-full overflow-hidden">
<Image
src={member.image}
alt={member.name}
width={128}
height={128}
className="h-full w-full object-cover"
width={260}
height={347}
className="h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/>
</div>
<h3 className="heading-text mt-4 text-lg font-semibold">{member.name}</h3>
{member.instagram && (
<span
className="nav-link mt-1 inline-flex gap-1.5 text-sm"
onClick={(e) => e.stopPropagation()}
>
<Instagram size={14} className="shrink-0 mt-[3px]" />
<a
href={member.instagram}
target="_blank"
rel="noopener noreferrer"
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-80 transition-opacity duration-500 group-hover:opacity-100" />
{/* Rose glow on hover */}
<div className="absolute inset-0 bg-gradient-to-t from-rose-900/20 to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
{/* Content */}
<div className="absolute bottom-0 left-0 right-0 p-4 translate-y-1 transition-transform duration-500 group-hover:translate-y-0">
<h3 className="text-base font-semibold text-white sm:text-lg">
{member.name}
</h3>
{member.instagram && (
<span
className="mt-1 inline-flex items-center gap-1.5 text-xs text-white/60 transition-colors hover:text-rose-400 sm:text-sm"
onClick={(e) => e.stopPropagation()}
>
{member.instagram.split("/").filter(Boolean).pop()}
</a>
</span>
)}
<Instagram size={12} className="shrink-0" />
<a
href={member.instagram}
target="_blank"
rel="noopener noreferrer"
>
{member.instagram.split("/").filter(Boolean).pop()}
</a>
</span>
)}
</div>
</div>
</Reveal>
))}
))}
</div>
{/* Side navigation arrows */}
<button
onClick={() => scroll("left")}
className="absolute left-2 top-1/2 -translate-y-1/2 hidden h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white/80 backdrop-blur-sm transition-all hover:bg-rose-500/30 hover:text-white sm:flex"
aria-label="Назад"
>
<ChevronLeft size={22} />
</button>
<button
onClick={() => scroll("right")}
className="absolute right-2 top-1/2 -translate-y-1/2 hidden h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white/80 backdrop-blur-sm transition-all hover:bg-rose-500/30 hover:text-white sm:flex"
aria-label="Вперёд"
>
<ChevronRight size={22} />
</button>
</div>
</div>
</Reveal>
<TeamMemberModal
member={selectedMember}