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:
@@ -16,6 +16,17 @@ html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ===== Selection ===== */
|
||||
|
||||
::selection {
|
||||
background-color: rgba(225, 29, 72, 0.3);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
@@ -32,6 +43,5 @@ html {
|
||||
/* ===== Focus ===== */
|
||||
|
||||
:focus-visible {
|
||||
@apply outline-2 outline-offset-2 outline-neutral-900;
|
||||
@apply dark:outline-white;
|
||||
@apply outline-2 outline-offset-2 outline-rose-500;
|
||||
}
|
||||
|
||||
@@ -26,31 +26,18 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
const themeScript = `
|
||||
(function() {
|
||||
var stored = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (stored === 'dark' || (!stored && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="ru" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
</head>
|
||||
<html lang="ru" className="dark">
|
||||
<body
|
||||
className={`${inter.variable} ${oswald.variable} surface-base font-sans antialiased`}
|
||||
className={`${inter.variable} ${oswald.variable} bg-[#050505] text-neutral-50 font-sans antialiased`}
|
||||
>
|
||||
<Header />
|
||||
<main className="pt-16">{children}</main>
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@keyframes hero-fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transform: translateY(32px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@@ -14,11 +14,59 @@
|
||||
@keyframes hero-fade-in-scale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
transform: scale(0.8);
|
||||
filter: blur(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
opacity: 0.4;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heart-float {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(0) scale(0.5);
|
||||
}
|
||||
10% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-100vh) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,22 +74,95 @@
|
||||
|
||||
.hero-logo {
|
||||
opacity: 0;
|
||||
animation: hero-fade-in-scale 1s ease-out 0.1s forwards;
|
||||
animation: hero-fade-in-scale 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
opacity: 0;
|
||||
animation: hero-fade-in-up 0.8s ease-out 0.4s forwards;
|
||||
animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
opacity: 0;
|
||||
animation: hero-fade-in-up 0.8s ease-out 0.7s forwards;
|
||||
animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 0.8s forwards;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
opacity: 0;
|
||||
animation: hero-fade-in-up 0.8s ease-out 1s forwards;
|
||||
animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 1.1s forwards;
|
||||
}
|
||||
|
||||
/* ===== Hero Background ===== */
|
||||
|
||||
.hero-bg-gradient {
|
||||
background: radial-gradient(ellipse 80% 60% at 50% -20%, rgba(225, 29, 72, 0.15), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 80% 50%, rgba(225, 29, 72, 0.08), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 20% 80%, rgba(225, 29, 72, 0.06), transparent);
|
||||
}
|
||||
|
||||
.hero-glow-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
animation: pulse-glow 6s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== Gradient Text ===== */
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #fff 0%, #e11d48 50%, #fff 100%);
|
||||
background-size: 200% 200%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: gradient-shift 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Light mode gradient text */
|
||||
.gradient-text-light {
|
||||
background: linear-gradient(135deg, #171717 0%, #e11d48 50%, #171717 100%);
|
||||
background-size: 200% 200%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: gradient-shift 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ===== Animated Border ===== */
|
||||
|
||||
.animated-border {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.animated-border::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, rgba(225, 29, 72, 0.3), transparent 40%, transparent 60%, rgba(225, 29, 72, 0.15));
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.animated-border:hover::before {
|
||||
opacity: 1;
|
||||
background: linear-gradient(135deg, rgba(225, 29, 72, 0.6), transparent 40%, transparent 60%, rgba(225, 29, 72, 0.4));
|
||||
}
|
||||
|
||||
/* ===== Glow Effect ===== */
|
||||
|
||||
.glow-hover {
|
||||
transition: box-shadow 0.5s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.glow-hover:hover {
|
||||
box-shadow: 0 0 30px rgba(225, 29, 72, 0.1), 0 0 60px rgba(225, 29, 72, 0.05);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* ===== Scroll Reveal ===== */
|
||||
@@ -49,7 +170,7 @@
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.7s ease-out, transform 0.7s ease-out;
|
||||
transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.reveal.visible {
|
||||
@@ -62,11 +183,11 @@
|
||||
@keyframes modal-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,11 +201,18 @@
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
animation: modal-overlay-in 0.2s ease-out;
|
||||
animation: modal-overlay-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
animation: modal-fade-in 0.3s ease-out;
|
||||
animation: modal-fade-in 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* ===== Section Divider ===== */
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(225, 29, 72, 0.3), transparent);
|
||||
}
|
||||
|
||||
/* ===== Reduced Motion ===== */
|
||||
@@ -96,6 +224,7 @@
|
||||
.hero-cta {
|
||||
animation: none !important;
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
.reveal {
|
||||
@@ -108,4 +237,17 @@
|
||||
.modal-content {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.gradient-text,
|
||||
.gradient-text-light {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.hero-glow-orb {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.glow-hover:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,57 @@
|
||||
/* ===== Navigation ===== */
|
||||
|
||||
.nav-link {
|
||||
@apply text-sm font-medium text-neutral-600 transition-colors duration-200;
|
||||
@apply text-sm font-medium transition-all duration-300;
|
||||
@apply text-neutral-500;
|
||||
@apply hover:text-neutral-900;
|
||||
@apply dark:text-neutral-400 dark:hover:text-white;
|
||||
}
|
||||
|
||||
.nav-link-active {
|
||||
@apply text-rose-600;
|
||||
@apply dark:text-rose-400;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
@apply text-neutral-500 transition-colors duration-200;
|
||||
@apply hover:text-neutral-900;
|
||||
@apply dark:text-neutral-400 dark:hover:text-white;
|
||||
@apply text-neutral-400 transition-all duration-300;
|
||||
@apply hover:text-rose-600;
|
||||
@apply dark:text-neutral-500 dark:hover:text-rose-400;
|
||||
}
|
||||
|
||||
/* ===== Cards ===== */
|
||||
|
||||
.card {
|
||||
@apply rounded-2xl border p-6 transition-all duration-200 cursor-pointer;
|
||||
@apply border-neutral-200 bg-neutral-50;
|
||||
@apply hover:border-neutral-400 hover:shadow-lg;
|
||||
@apply dark:border-neutral-800 dark:bg-neutral-900;
|
||||
@apply dark:hover:border-neutral-600;
|
||||
@apply rounded-2xl border p-6 transition-all duration-500 cursor-pointer;
|
||||
@apply border-neutral-200 bg-white;
|
||||
@apply hover:border-rose-200 hover:shadow-lg;
|
||||
@apply dark:border-white/[0.06] dark:bg-white/[0.02];
|
||||
@apply dark:hover:border-rose-500/20 dark:hover:bg-white/[0.04];
|
||||
@apply dark:hover:shadow-[0_0_30px_rgba(225,29,72,0.08)];
|
||||
}
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center font-medium rounded-full transition-colors duration-200 cursor-pointer;
|
||||
@apply bg-neutral-900 text-white;
|
||||
@apply hover:bg-neutral-700;
|
||||
@apply dark:bg-white dark:text-neutral-900;
|
||||
@apply dark:hover:bg-neutral-200;
|
||||
@apply inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 cursor-pointer;
|
||||
@apply bg-rose-600 text-white;
|
||||
@apply hover:bg-rose-500 hover:shadow-[0_0_30px_rgba(225,29,72,0.4)];
|
||||
@apply dark:bg-rose-600 dark:text-white;
|
||||
@apply dark:hover:bg-rose-500 dark:hover:shadow-[0_0_30px_rgba(225,29,72,0.4)];
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply inline-flex items-center justify-center font-medium rounded-full transition-colors duration-200 cursor-pointer;
|
||||
@apply border border-neutral-900 text-neutral-900;
|
||||
@apply hover:bg-neutral-900 hover:text-white;
|
||||
@apply dark:border-white dark:text-white;
|
||||
@apply dark:hover:bg-white dark:hover:text-neutral-900;
|
||||
@apply inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 cursor-pointer;
|
||||
@apply border border-rose-600 text-rose-600;
|
||||
@apply hover:bg-rose-600 hover:text-white;
|
||||
@apply dark:border-rose-500 dark:text-rose-400;
|
||||
@apply dark:hover:bg-rose-500 dark:hover:text-white;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply inline-flex items-center justify-center font-medium rounded-full transition-colors duration-200 cursor-pointer;
|
||||
@apply inline-flex items-center justify-center font-medium rounded-full transition-all duration-300 cursor-pointer;
|
||||
@apply text-neutral-600;
|
||||
@apply hover:text-neutral-900;
|
||||
@apply dark:text-neutral-400 dark:hover:text-white;
|
||||
@apply hover:text-rose-600;
|
||||
@apply dark:text-neutral-400 dark:hover:text-rose-400;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar ===== */
|
||||
@@ -74,6 +81,6 @@
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
@apply shrink-0 text-neutral-900;
|
||||
@apply dark:text-neutral-50;
|
||||
@apply shrink-0 text-rose-600;
|
||||
@apply dark:text-rose-400;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
/* ===== Surfaces ===== */
|
||||
|
||||
.surface-base {
|
||||
@apply bg-white text-neutral-900;
|
||||
@apply dark:bg-neutral-950 dark:text-neutral-50;
|
||||
@apply bg-neutral-50 text-neutral-900;
|
||||
@apply dark:bg-[#050505] dark:text-neutral-50;
|
||||
}
|
||||
|
||||
.surface-muted {
|
||||
@apply bg-neutral-100;
|
||||
@apply dark:bg-neutral-900;
|
||||
@apply dark:bg-[#0a0a0a];
|
||||
}
|
||||
|
||||
.surface-glass {
|
||||
@apply bg-white/80 backdrop-blur-md;
|
||||
@apply dark:bg-neutral-950/80;
|
||||
@apply bg-white/70 backdrop-blur-xl;
|
||||
@apply dark:bg-black/40 dark:backdrop-blur-xl;
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
@apply bg-white/80 backdrop-blur-sm;
|
||||
@apply dark:bg-white/[0.03] dark:backdrop-blur-sm;
|
||||
}
|
||||
|
||||
/* ===== Borders ===== */
|
||||
|
||||
.theme-border {
|
||||
@apply border-neutral-200;
|
||||
@apply dark:border-neutral-800;
|
||||
@apply dark:border-white/[0.06];
|
||||
}
|
||||
|
||||
/* ===== Text ===== */
|
||||
|
||||
.heading-text {
|
||||
@apply text-neutral-900;
|
||||
@apply dark:text-neutral-50;
|
||||
@apply dark:text-white;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
@@ -36,13 +41,18 @@
|
||||
|
||||
.muted-text {
|
||||
@apply text-neutral-500;
|
||||
@apply dark:text-neutral-400;
|
||||
@apply dark:text-neutral-500;
|
||||
}
|
||||
|
||||
.accent-text {
|
||||
@apply text-rose-600;
|
||||
@apply dark:text-rose-400;
|
||||
}
|
||||
|
||||
/* ===== Layout ===== */
|
||||
|
||||
.section-padding {
|
||||
@apply py-10 sm:py-14;
|
||||
@apply py-16 sm:py-24;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { BRAND } from "@/lib/constants";
|
||||
import { siteContent } from "@/data/content";
|
||||
import { SocialLinks } from "@/components/ui/SocialLinks";
|
||||
import { Heart } from "lucide-react";
|
||||
|
||||
export function Footer() {
|
||||
const { contact } = siteContent;
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="surface-muted theme-border border-t">
|
||||
<div className="section-container flex flex-col items-center gap-4 py-8 sm:flex-row sm:justify-between">
|
||||
<p className="muted-text text-sm">
|
||||
<footer className="relative border-t border-neutral-200 bg-neutral-100 dark:border-white/[0.06] dark:bg-[#050505]">
|
||||
<div className="section-divider absolute top-0 left-0 right-0" />
|
||||
<div className="section-container flex flex-col items-center gap-4 py-10 sm:flex-row sm:justify-between">
|
||||
<p className="text-sm text-neutral-500">
|
||||
© {year} {BRAND.name}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 text-sm text-neutral-500">
|
||||
<span>Made with</span>
|
||||
<Heart size={14} className="fill-rose-500 text-rose-500" />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -3,65 +3,97 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { BRAND, NAV_LINKS } from "@/lib/constants";
|
||||
import { ThemeToggle } from "@/components/ui/ThemeToggle";
|
||||
|
||||
export function Header() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function handleScroll() {
|
||||
setScrolled(window.scrollY > 20);
|
||||
}
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className="surface-glass theme-border fixed top-0 z-50 w-full border-b">
|
||||
<div className="flex h-16 items-center justify-between px-6 sm:px-8">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt={BRAND.name}
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
className="dark:invert"
|
||||
/>
|
||||
<span className="font-display text-lg font-bold tracking-tight">
|
||||
<header
|
||||
className={`fixed top-0 z-50 w-full transition-all duration-500 ${
|
||||
scrolled
|
||||
? "border-b border-white/[0.06] bg-black/40 shadow-none backdrop-blur-xl"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6 sm:px-8">
|
||||
<Link href="/" className="group flex items-center gap-2.5">
|
||||
<div className="relative flex h-8 w-8 items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 rounded-full transition-all duration-300 group-hover:scale-125"
|
||||
style={{
|
||||
background: "radial-gradient(circle, rgba(225,29,72,0.5) 0%, rgba(225,29,72,0.15) 50%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt={BRAND.name}
|
||||
width={24}
|
||||
height={24}
|
||||
unoptimized
|
||||
className="relative transition-transform duration-300 group-hover:scale-110"
|
||||
style={{
|
||||
filter: "drop-shadow(0 0 3px rgba(225,29,72,0.5))",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-display text-lg font-bold tracking-tight text-white">
|
||||
{BRAND.shortName}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-8 md:flex">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a key={link.href} href={link.href} className="nav-link">
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="relative py-1 text-sm font-medium text-neutral-400 transition-all duration-300 after:absolute after:bottom-0 after:left-0 after:h-[2px] after:w-0 after:bg-rose-500 after:transition-all after:duration-300 hover:text-white hover:after:w-full"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
aria-label="Меню"
|
||||
className="body-text rounded-md p-2"
|
||||
className="rounded-lg p-2 text-neutral-400 transition-colors hover:text-white"
|
||||
>
|
||||
{menuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{menuOpen && (
|
||||
<nav className="surface-base theme-border border-t px-6 py-4 sm:px-8 md:hidden">
|
||||
{/* Mobile menu */}
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 md:hidden ${
|
||||
menuOpen ? "max-h-80 opacity-100" : "max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<nav className="border-t border-white/[0.06] bg-black/40 px-6 py-4 backdrop-blur-xl sm:px-8">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="nav-link block py-3"
|
||||
className="block py-3 text-base text-neutral-400 transition-colors hover:text-white"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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