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:
@@ -6,12 +6,12 @@ import { X, Flame, Sparkles, Wind, Zap, Star, Monitor } from "lucide-react";
|
||||
import type { ClassItem } from "@/types";
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
flame: <Flame size={40} />,
|
||||
sparkles: <Sparkles size={40} />,
|
||||
wind: <Wind size={40} />,
|
||||
zap: <Zap size={40} />,
|
||||
star: <Star size={40} />,
|
||||
monitor: <Monitor size={40} />,
|
||||
flame: <Flame size={20} />,
|
||||
sparkles: <Sparkles size={20} />,
|
||||
wind: <Wind size={20} />,
|
||||
zap: <Zap size={20} />,
|
||||
star: <Star size={20} />,
|
||||
monitor: <Monitor size={20} />,
|
||||
};
|
||||
|
||||
interface ClassModalProps {
|
||||
@@ -38,55 +38,80 @@ export function ClassModal({ classItem, onClose }: ClassModalProps) {
|
||||
|
||||
if (!classItem) return null;
|
||||
|
||||
const heroImage = classItem.images?.[0];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
||||
className="modal-overlay fixed inset-0 z-50 flex items-end justify-center bg-black/70 backdrop-blur-lg sm:items-center sm:p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="modal-content surface-base relative w-full max-w-md md:max-w-2xl max-h-[85vh] flex flex-col rounded-2xl shadow-xl"
|
||||
className="modal-content relative flex w-full max-h-[90vh] flex-col overflow-hidden rounded-t-3xl bg-white sm:max-w-2xl sm:rounded-3xl dark:bg-[#111]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="heading-text absolute right-4 top-4 z-10 rounded-full p-1 transition-opacity hover:opacity-70"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
{/* Hero image banner */}
|
||||
{heroImage && (
|
||||
<div className="relative h-52 w-full shrink-0 sm:h-64">
|
||||
<Image
|
||||
src={heroImage}
|
||||
alt={classItem.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
|
||||
|
||||
<div className="overflow-y-auto p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="heading-text shrink-0">{iconMap[classItem.icon]}</div>
|
||||
<h3 className="heading-text text-xl font-semibold">
|
||||
{classItem.name}
|
||||
</h3>
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-black/40 text-white/80 backdrop-blur-sm transition-all hover:bg-black/60 hover:text-white"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
{/* Title on image */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/15 text-white backdrop-blur-sm">
|
||||
{iconMap[classItem.icon]}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-white">
|
||||
{classItem.name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="overflow-y-auto">
|
||||
{/* Title fallback when no image */}
|
||||
{!heroImage && (
|
||||
<div className="flex items-center justify-between p-6 pb-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-rose-50 text-rose-600 dark:bg-rose-500/10 dark:text-rose-400">
|
||||
{iconMap[classItem.icon]}
|
||||
</div>
|
||||
<h3 className="heading-text text-xl font-bold">
|
||||
{classItem.name}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-full p-1.5 text-neutral-400 transition-all hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-500 dark:hover:bg-white/[0.05] dark:hover:text-white"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{classItem.detailedDescription && (
|
||||
<div className="body-text mt-4 text-sm leading-relaxed whitespace-pre-line">
|
||||
<div className="p-6 text-sm leading-relaxed whitespace-pre-line text-neutral-600 dark:text-neutral-400">
|
||||
{classItem.detailedDescription}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{classItem.images && classItem.images.length > 0 && (
|
||||
<div className={`mt-6 flex gap-3 pb-2 ${classItem.images.length === 1 ? "justify-center" : "overflow-x-auto"}`}>
|
||||
{classItem.images.map((src, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-48 w-72 shrink-0 overflow-hidden rounded-xl"
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={`${classItem.name} ${i + 1}`}
|
||||
width={288}
|
||||
height={192}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
50
src/components/ui/FloatingHearts.tsx
Normal file
50
src/components/ui/FloatingHearts.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Heart {
|
||||
id: number;
|
||||
left: number;
|
||||
size: number;
|
||||
delay: number;
|
||||
duration: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
export function FloatingHearts() {
|
||||
const [hearts, setHearts] = useState<Heart[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generated: Heart[] = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i,
|
||||
left: Math.random() * 100,
|
||||
size: 8 + Math.random() * 16,
|
||||
delay: Math.random() * 10,
|
||||
duration: 10 + Math.random() * 15,
|
||||
opacity: 0.03 + Math.random() * 0.08,
|
||||
}));
|
||||
setHearts(generated);
|
||||
}, []);
|
||||
|
||||
if (hearts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
{hearts.map((heart) => (
|
||||
<div
|
||||
key={heart.id}
|
||||
className="absolute text-rose-500"
|
||||
style={{
|
||||
left: `${heart.left}%`,
|
||||
bottom: "-20px",
|
||||
fontSize: `${heart.size}px`,
|
||||
opacity: heart.opacity,
|
||||
animation: `heart-float ${heart.duration}s ease-in ${heart.delay}s infinite`,
|
||||
}}
|
||||
>
|
||||
♥
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export function SectionHeading({ children, className = "" }: SectionHeadingProps
|
||||
className={`font-display text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl ${className}`}
|
||||
>
|
||||
{children}
|
||||
<span className="mt-2 block h-1 w-16 rounded bg-neutral-900 dark:bg-white" />
|
||||
<span className="mt-3 block h-[2px] w-16 rounded-full bg-gradient-to-r from-rose-500 to-rose-500/0" />
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,58 +31,60 @@ export function TeamMemberModal({ member, onClose }: TeamMemberModalProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
||||
className="modal-overlay fixed inset-0 z-50 flex items-end justify-center bg-black/70 backdrop-blur-lg sm:items-center sm:p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="modal-content surface-base relative w-full max-w-md md:max-w-2xl max-h-[85vh] flex flex-col rounded-2xl shadow-xl"
|
||||
className="modal-content relative flex w-full max-h-[90vh] flex-col overflow-hidden rounded-t-3xl bg-white sm:max-w-lg sm:rounded-3xl dark:bg-[#111]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="heading-text absolute right-4 top-4 z-10 rounded-full p-1 transition-opacity hover:opacity-70"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
{/* Hero photo */}
|
||||
<div className="relative h-72 w-full shrink-0 sm:h-80">
|
||||
<Image
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
{/* Gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
|
||||
|
||||
<div className="flex flex-col md:flex-row overflow-y-auto p-6 gap-6">
|
||||
<div className="flex flex-col items-center md:items-start shrink-0">
|
||||
<div className="h-40 w-40 md:h-48 md:w-48 overflow-hidden rounded-full ring-2 ring-rose-500/30">
|
||||
<Image
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-black/40 text-white/80 backdrop-blur-sm transition-all hover:bg-black/60 hover:text-white"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
<h3 className="heading-text mt-4 text-xl font-semibold text-center md:text-left w-full">
|
||||
{/* Name + Instagram on photo */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6">
|
||||
<h3 className="text-2xl font-bold text-white">
|
||||
{member.name}
|
||||
</h3>
|
||||
|
||||
{member.instagram && (
|
||||
<a
|
||||
href={member.instagram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="nav-link mt-1 inline-flex gap-1.5 text-sm"
|
||||
className="mt-2 inline-flex items-center gap-2 text-sm text-white/70 transition-colors hover:text-rose-400"
|
||||
>
|
||||
<Instagram size={14} className="shrink-0 mt-[3px]" />
|
||||
<Instagram size={15} className="shrink-0" />
|
||||
<span>{member.instagram.split("/").filter(Boolean).pop()}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{member.description && (
|
||||
<div className="md:border-l md:theme-border md:pl-6 flex items-center">
|
||||
<p className="body-text text-sm leading-relaxed">
|
||||
{member.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{member.description && (
|
||||
<div className="overflow-y-auto p-6">
|
||||
<p className="text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||
{member.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user