Compare commits

...

2 Commits

Author SHA1 Message Date
0ed0a91161 feat: showcase layout, photo filter, team specializations, scroll UX
- Replace modals with ShowcaseLayout for Team and Classes sections
- Add warm photo filter matching dark/gold color scheme
- Replace generic "Тренер" with actual specializations per member
- Fix heart logo color animation loop (seamless repeat)
- Style scrollbar with gold theme, pause auto-rotation on hover
- Auto-scroll only when active item is out of view

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:23:11 +03:00
a75922c730 feat: Instagram data sync, gold accent, SVG logo, FAQ & Pricing sections
- Sync all content from Instagram: fix addresses, trainer names, add 5 new
  trainers, remove 2 inactive, update class descriptions
- Add FAQ section (11 Q&A items) and Pricing section (tabs: subscriptions,
  rental, rules)
- Redesign with editorial magazine feel: centered headings, generous spacing,
  section glow effects, glassmorphism cards
- Migrate entire accent palette from rose to warm gold (#c9a96e)
- Replace low-res PNG logo with vector SVG traced via potrace — crisp at any
  size, animated gradient (black↔gold), heartbeat pulse animation
- Make header brand name gold

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:45:50 +03:00
27 changed files with 975 additions and 588 deletions

3
public/images/logo.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234 192" fill="currentColor" fill-rule="evenodd">
<path d="M118.02,188.43 C118.04,184.10 120.51,173.30 122.96,166.79 C126.11,158.42 133.55,147.62 144.55,135.42 C165.53,112.15 170.96,101.38 170.99,82.98 C171.00,72.35 168.51,62.96 162.47,50.94 C160.00,46.02 157.78,42.00 157.53,42.00 C157.29,42.00 158.24,45.04 159.64,48.75 C163.04,57.78 165.96,71.24 165.96,78.00 C165.97,85.89 163.51,95.22 159.27,103.40 C156.31,109.09 152.52,113.57 140.17,126.00 C131.69,134.53 123.42,143.44 121.79,145.81 C116.23,153.88 110.87,167.99 109.81,177.28 C109.51,179.99 109.37,179.90 105.02,174.28 C102.55,171.10 98.74,166.54 96.55,164.15 L92.56,159.80 L95.53,157.70 C100.61,154.12 105.90,148.12 108.76,142.70 C111.22,138.02 111.50,136.50 111.47,127.50 C111.45,118.43 111.02,116.15 106.86,103.00 C101.17,85.06 99.60,76.75 100.25,68.17 C100.75,61.51 104.60,48.83 107.29,45.00 C108.57,43.16 108.76,43.69 109.31,50.84 C110.42,65.22 115.99,75.08 126.37,81.04 C133.31,85.02 133.82,84.76 128.92,79.75 C124.20,74.93 119.44,65.68 118.16,58.84 C116.46,49.75 119.09,39.24 125.73,28.59 L128.79,23.69 L130.02,29.59 C130.69,32.84 133.23,39.92 135.65,45.33 C143.15,62.02 144.36,69.90 141.53,83.29 C140.04,90.28 134.00,104.04 127.66,114.86 C125.68,118.24 124.39,120.97 124.78,120.93 C125.18,120.90 128.41,117.53 131.97,113.46 C145.06,98.47 150.50,85.84 150.46,70.50 C150.43,59.80 149.70,57.36 141.46,40.79 C137.98,33.80 134.83,26.02 134.45,23.49 C133.85,19.50 134.07,18.56 136.10,16.40 C139.50,12.77 147.93,7.39 153.86,5.06 L159.00,3.03 L159.00,9.32 C159.00,18.37 162.11,24.01 172.06,33.00 C176.46,36.97 180.72,41.50 181.53,43.06 C183.67,47.20 183.39,56.94 180.95,63.13 C178.14,70.25 180.87,67.95 184.97,59.74 C190.78,48.12 188.70,39.73 177.15,28.15 C173.28,24.27 169.43,20.06 168.61,18.80 C166.51,15.58 164.79,7.21 165.54,3.83 C166.16,1.00 166.17,1.00 174.33,1.01 C178.82,1.02 184.15,1.29 186.17,1.63 C189.69,2.21 189.81,2.37 189.29,5.62 C188.42,10.94 190.83,20.71 195.10,29.20 C197.26,33.49 199.19,37.00 199.40,37.00 C199.62,37.00 198.67,33.51 197.31,29.25 C195.35,23.12 194.91,19.90 195.17,13.83 C195.36,9.61 195.85,5.81 196.28,5.39 C197.35,4.31 205.52,8.43 211.67,13.15 C218.45,18.35 221.00,23.72 220.99,32.74 C220.98,36.46 220.28,41.98 219.42,45.00 C218.57,48.02 217.63,51.40 217.34,52.50 C216.30,56.51 222.34,45.32 224.52,39.20 C225.76,35.73 227.01,32.66 227.29,32.38 C227.57,32.09 228.79,34.48 229.99,37.68 C238.21,59.57 232.78,83.80 215.76,101.15 C209.43,107.60 207.42,108.54 209.17,104.25 C210.91,100.00 210.37,85.54 208.08,74.64 C206.93,69.22 205.95,62.24 205.90,59.14 C205.80,53.85 205.74,53.72 204.93,57.00 C204.45,58.92 204.13,68.15 204.22,77.50 C204.37,93.11 204.20,94.91 202.15,99.45 C198.99,106.51 192.06,115.46 190.76,114.16 C188.49,111.89 189.93,84.88 192.72,77.19 C194.09,73.45 189.30,79.05 186.68,84.26 C182.02,93.55 180.69,101.03 181.41,113.97 L182.05,125.50 L169.94,135.00 C153.90,147.58 132.06,170.01 124.13,182.05 C118.83,190.09 118.00,190.96 118.02,188.43 Z M83.09,150.59 C78.00,145.44 77.78,144.99 78.44,141.34 C78.82,139.23 81.24,133.00 83.81,127.50 C88.47,117.57 88.50,117.43 88.49,107.00 C88.47,99.38 87.96,94.99 86.59,91.00 C84.28,84.21 77.06,69.61 76.36,70.30 C76.08,70.58 76.56,72.19 77.43,73.87 C79.91,78.65 82.99,92.88 82.99,99.57 C83.00,108.39 80.69,114.86 73.96,124.82 C70.68,129.67 68.00,134.17 68.00,134.82 C68.00,135.47 67.62,136.00 67.16,136.00 C66.07,136.00 57.00,128.93 57.00,128.07 C57.00,127.71 59.03,123.47 61.50,118.66 C66.60,108.75 67.24,103.18 64.48,92.59 C62.01,83.09 61.32,83.22 61.40,93.17 C61.45,100.19 61.02,103.39 59.69,106.10 C57.49,110.57 48.29,121.00 46.56,121.00 C44.40,121.00 39.79,109.24 39.24,102.34 C38.56,93.90 40.48,89.09 48.68,78.77 C62.32,61.60 65.53,49.22 60.98,31.41 C58.70,22.51 54.61,13.20 50.08,6.62 C47.54,2.92 47.30,2.10 48.60,1.60 C50.50,0.87 66.31,0.80 68.17,1.51 C69.18,1.90 69.42,4.19 69.15,10.95 C68.88,18.05 69.27,21.45 71.06,27.48 C72.30,31.66 73.77,35.36 74.33,35.70 C74.97,36.10 75.06,35.62 74.57,34.41 C74.15,33.36 73.57,28.88 73.27,24.45 C72.70,15.76 74.85,5.38 77.44,4.39 C79.59,3.56 92.09,10.37 97.73,15.45 C100.57,18.00 103.83,21.61 104.99,23.48 L107.09,26.88 L103.66,31.69 C94.93,43.97 91.54,55.17 91.64,71.50 C91.72,84.83 92.69,89.79 99.08,109.50 C105.86,130.41 104.79,139.90 94.39,151.01 C91.83,153.75 89.44,156.00 89.08,156.00 C88.72,156.00 86.03,153.57 83.09,150.59 Z M29.50,109.90 C26.20,107.67 21.64,104.05 19.38,101.84 L15.26,97.83 L17.24,92.67 C19.86,85.83 19.20,74.50 15.57,64.04 C12.16,54.24 10.98,53.26 12.75,61.71 C14.48,69.97 13.94,81.02 11.53,86.50 L9.77,90.50 L6.92,84.50 C2.82,75.84 1.00,67.75 1.00,58.18 C1.00,42.04 6.09,29.69 17.39,18.40 C23.48,12.31 32.07,6.09 30.82,8.67 C30.59,9.13 28.88,12.62 27.02,16.44 C21.43,27.90 22.74,38.16 31.08,48.28 C35.14,53.21 36.55,53.01 33.93,47.87 C31.25,42.61 30.40,34.47 31.90,28.31 C33.17,23.08 42.81,3.00 44.05,3.00 C44.47,3.00 46.18,6.79 47.86,11.42 C58.07,39.63 56.90,53.23 42.67,72.14 C31.96,86.38 29.60,96.21 33.92,108.52 C34.98,111.54 35.77,113.99 35.67,113.97 C35.58,113.96 32.80,112.12 29.50,109.90 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -8,6 +8,9 @@
@theme inline { @theme inline {
--font-display: var(--font-oswald); --font-display: var(--font-oswald);
--font-sans: var(--font-inter); --font-sans: var(--font-inter);
--color-gold: #c9a96e;
--color-gold-light: #d4b87a;
--color-gold-dark: #a08050;
} }
/* ===== Base ===== */ /* ===== Base ===== */
@@ -23,7 +26,7 @@ body {
/* ===== Selection ===== */ /* ===== Selection ===== */
::selection { ::selection {
background-color: rgba(225, 29, 72, 0.3); background-color: rgba(201, 169, 110, 0.3);
color: inherit; color: inherit;
} }
@@ -43,5 +46,5 @@ body {
/* ===== Focus ===== */ /* ===== Focus ===== */
:focus-visible { :focus-visible {
@apply outline-2 outline-offset-2 outline-rose-500; @apply outline-2 outline-offset-2 outline-[#c9a96e];
} }

View File

@@ -2,6 +2,8 @@ import { Hero } from "@/components/sections/Hero";
import { Team } from "@/components/sections/Team"; import { Team } from "@/components/sections/Team";
import { About } from "@/components/sections/About"; import { About } from "@/components/sections/About";
import { Classes } from "@/components/sections/Classes"; import { Classes } from "@/components/sections/Classes";
import { Pricing } from "@/components/sections/Pricing";
import { FAQ } from "@/components/sections/FAQ";
import { Contact } from "@/components/sections/Contact"; import { Contact } from "@/components/sections/Contact";
export default function HomePage() { export default function HomePage() {
@@ -11,6 +13,8 @@ export default function HomePage() {
<About /> <About />
<Team /> <Team />
<Classes /> <Classes />
<Pricing />
<FAQ />
<Contact /> <Contact />
</> </>
); );

View File

@@ -53,6 +53,24 @@
} }
} }
@keyframes heartbeat {
0%, 100% {
transform: scale(1);
}
15% {
transform: scale(1.08);
}
30% {
transform: scale(1);
}
45% {
transform: scale(1.05);
}
60% {
transform: scale(1);
}
}
@keyframes heart-float { @keyframes heart-float {
0% { 0% {
opacity: 0; opacity: 0;
@@ -77,6 +95,10 @@
animation: hero-fade-in-scale 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards; animation: hero-fade-in-scale 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
} }
.hero-logo-heartbeat {
animation: heartbeat 2.5s ease-in-out 1.5s infinite;
}
.hero-title { .hero-title {
opacity: 0; opacity: 0;
animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards; animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards;
@@ -95,9 +117,9 @@
/* ===== Hero Background ===== */ /* ===== Hero Background ===== */
.hero-bg-gradient { .hero-bg-gradient {
background: radial-gradient(ellipse 80% 60% at 50% -20%, rgba(225, 29, 72, 0.15), transparent), background: radial-gradient(ellipse 80% 60% at 50% -20%, rgba(201, 169, 110, 0.12), transparent),
radial-gradient(ellipse 60% 40% at 80% 50%, rgba(225, 29, 72, 0.08), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(201, 169, 110, 0.06), transparent),
radial-gradient(ellipse 60% 40% at 20% 80%, rgba(225, 29, 72, 0.06), transparent); radial-gradient(ellipse 60% 40% at 20% 80%, rgba(201, 169, 110, 0.04), transparent);
} }
.hero-glow-orb { .hero-glow-orb {
@@ -111,7 +133,7 @@
/* ===== Gradient Text ===== */ /* ===== Gradient Text ===== */
.gradient-text { .gradient-text {
background: linear-gradient(135deg, #fff 0%, #e11d48 50%, #fff 100%); background: linear-gradient(135deg, #fff 0%, #c9a96e 50%, #fff 100%);
background-size: 200% 200%; background-size: 200% 200%;
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
@@ -121,7 +143,7 @@
/* Light mode gradient text */ /* Light mode gradient text */
.gradient-text-light { .gradient-text-light {
background: linear-gradient(135deg, #171717 0%, #e11d48 50%, #171717 100%); background: linear-gradient(135deg, #171717 0%, #c9a96e 50%, #171717 100%);
background-size: 200% 200%; background-size: 200% 200%;
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
@@ -141,7 +163,7 @@
inset: 0; inset: 0;
border-radius: inherit; border-radius: inherit;
padding: 1px; padding: 1px;
background: linear-gradient(135deg, rgba(225, 29, 72, 0.3), transparent 40%, transparent 60%, rgba(225, 29, 72, 0.15)); background: linear-gradient(135deg, rgba(201, 169, 110, 0.3), transparent 40%, transparent 60%, rgba(201, 169, 110, 0.15));
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude; mask-composite: exclude;
pointer-events: none; pointer-events: none;
@@ -151,7 +173,7 @@
.animated-border:hover::before { .animated-border:hover::before {
opacity: 1; opacity: 1;
background: linear-gradient(135deg, rgba(225, 29, 72, 0.6), transparent 40%, transparent 60%, rgba(225, 29, 72, 0.4)); background: linear-gradient(135deg, rgba(201, 169, 110, 0.6), transparent 40%, transparent 60%, rgba(201, 169, 110, 0.4));
} }
/* ===== Glow Effect ===== */ /* ===== Glow Effect ===== */
@@ -161,7 +183,7 @@
} }
.glow-hover:hover { .glow-hover:hover {
box-shadow: 0 0 30px rgba(225, 29, 72, 0.1), 0 0 60px rgba(225, 29, 72, 0.05); box-shadow: 0 0 30px rgba(201, 169, 110, 0.1), 0 0 60px rgba(201, 169, 110, 0.05);
transform: translateY(-4px); transform: translateY(-4px);
} }
@@ -178,6 +200,38 @@
transform: translateY(0); transform: translateY(0);
} }
/* ===== Showcase ===== */
@keyframes showcase-detail-enter {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes showcase-image-enter {
from {
opacity: 0;
transform: scale(1.03);
}
to {
opacity: 1;
transform: scale(1);
}
}
.showcase-detail-enter {
animation: showcase-detail-enter 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.showcase-detail-enter img {
animation: showcase-image-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
/* ===== Modal ===== */ /* ===== Modal ===== */
@keyframes modal-fade-in { @keyframes modal-fade-in {
@@ -212,7 +266,7 @@
.section-divider { .section-divider {
height: 1px; height: 1px;
background: linear-gradient(90deg, transparent, rgba(225, 29, 72, 0.3), transparent); background: linear-gradient(90deg, transparent, rgba(201, 169, 110, 0.15), transparent);
} }
/* ===== Reduced Motion ===== */ /* ===== Reduced Motion ===== */
@@ -243,7 +297,13 @@
animation: none !important; animation: none !important;
} }
.hero-glow-orb { .hero-glow-orb,
.hero-logo-heartbeat {
animation: none !important;
}
.showcase-detail-enter,
.showcase-detail-enter img {
animation: none !important; animation: none !important;
} }

View File

@@ -8,14 +8,14 @@
} }
.nav-link-active { .nav-link-active {
@apply text-rose-600; @apply text-[#a08050];
@apply dark:text-rose-400; @apply dark:text-[#d4b87a];
} }
.social-icon { .social-icon {
@apply text-neutral-400 transition-all duration-300; @apply text-neutral-400 transition-all duration-300;
@apply hover:text-rose-600; @apply hover:text-[#a08050];
@apply dark:text-neutral-500 dark:hover:text-rose-400; @apply dark:text-neutral-500 dark:hover:text-[#d4b87a];
} }
/* ===== Cards ===== */ /* ===== Cards ===== */
@@ -23,35 +23,35 @@
.card { .card {
@apply rounded-2xl border p-6 transition-all duration-500 cursor-pointer; @apply rounded-2xl border p-6 transition-all duration-500 cursor-pointer;
@apply border-neutral-200 bg-white; @apply border-neutral-200 bg-white;
@apply hover:border-rose-200 hover:shadow-lg; @apply hover:border-[#c9a96e]/30 hover:shadow-lg;
@apply dark:border-white/[0.06] dark:bg-white/[0.02]; @apply dark:border-white/[0.08] dark:bg-[#111];
@apply dark:hover:border-rose-500/20 dark:hover:bg-white/[0.04]; @apply dark:hover:border-[#c9a96e]/25 dark:hover:bg-[#151515];
@apply dark:hover:shadow-[0_0_30px_rgba(225,29,72,0.08)]; @apply dark:hover:shadow-[0_0_30px_rgba(201,169,110,0.06)];
} }
/* ===== Buttons ===== */ /* ===== Buttons ===== */
.btn-primary { .btn-primary {
@apply inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 cursor-pointer; @apply inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 cursor-pointer;
@apply bg-rose-600 text-white; @apply bg-[#c9a96e] text-black;
@apply hover:bg-rose-500 hover:shadow-[0_0_30px_rgba(225,29,72,0.4)]; @apply hover:bg-[#d4b87a] hover:shadow-[0_0_30px_rgba(201,169,110,0.35)];
@apply dark:bg-rose-600 dark:text-white; @apply dark:bg-[#c9a96e] dark:text-black;
@apply dark:hover:bg-rose-500 dark:hover:shadow-[0_0_30px_rgba(225,29,72,0.4)]; @apply dark:hover:bg-[#d4b87a] dark:hover:shadow-[0_0_30px_rgba(201,169,110,0.35)];
} }
.btn-outline { .btn-outline {
@apply inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 cursor-pointer; @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 border border-[#c9a96e] text-[#a08050];
@apply hover:bg-rose-600 hover:text-white; @apply hover:bg-[#c9a96e] hover:text-black;
@apply dark:border-rose-500 dark:text-rose-400; @apply dark:border-[#c9a96e] dark:text-[#d4b87a];
@apply dark:hover:bg-rose-500 dark:hover:text-white; @apply dark:hover:bg-[#c9a96e] dark:hover:text-black;
} }
.btn-ghost { .btn-ghost {
@apply inline-flex items-center justify-center font-medium rounded-full transition-all duration-300 cursor-pointer; @apply inline-flex items-center justify-center font-medium rounded-full transition-all duration-300 cursor-pointer;
@apply text-neutral-600; @apply text-neutral-600;
@apply hover:text-rose-600; @apply hover:text-[#a08050];
@apply dark:text-neutral-400 dark:hover:text-rose-400; @apply dark:text-neutral-400 dark:hover:text-[#d4b87a];
} }
/* ===== Scrollbar ===== */ /* ===== Scrollbar ===== */
@@ -81,6 +81,6 @@
} }
.contact-icon { .contact-icon {
@apply shrink-0 text-rose-600; @apply shrink-0 text-[#a08050];
@apply dark:text-rose-400; @apply dark:text-[#d4b87a];
} }

View File

@@ -2,12 +2,12 @@
.surface-base { .surface-base {
@apply bg-neutral-50 text-neutral-900; @apply bg-neutral-50 text-neutral-900;
@apply dark:bg-[#050505] dark:text-neutral-50; @apply dark:bg-[#050505] dark:text-neutral-100;
} }
.surface-muted { .surface-muted {
@apply bg-neutral-100; @apply bg-neutral-100;
@apply dark:bg-[#0a0a0a]; @apply dark:bg-[#080808];
} }
.surface-glass { .surface-glass {
@@ -17,14 +17,14 @@
.surface-card { .surface-card {
@apply bg-white/80 backdrop-blur-sm; @apply bg-white/80 backdrop-blur-sm;
@apply dark:bg-white/[0.03] dark:backdrop-blur-sm; @apply dark:bg-[#111] dark:backdrop-blur-sm;
} }
/* ===== Borders ===== */ /* ===== Borders ===== */
.theme-border { .theme-border {
@apply border-neutral-200; @apply border-neutral-200;
@apply dark:border-white/[0.06]; @apply dark:border-white/[0.08];
} }
/* ===== Text ===== */ /* ===== Text ===== */
@@ -36,7 +36,7 @@
.body-text { .body-text {
@apply text-neutral-600; @apply text-neutral-600;
@apply dark:text-neutral-400; @apply dark:text-neutral-300;
} }
.muted-text { .muted-text {
@@ -45,16 +45,81 @@
} }
.accent-text { .accent-text {
@apply text-rose-600; @apply text-[#a08050];
@apply dark:text-rose-400; @apply dark:text-[#d4b87a];
} }
/* ===== Layout ===== */ /* ===== Layout ===== */
.section-padding { .section-padding {
@apply py-16 sm:py-24; @apply py-20 sm:py-32;
} }
.section-container { .section-container {
@apply mx-auto max-w-6xl px-6 sm:px-8; @apply mx-auto max-w-6xl px-6 sm:px-8;
} }
/* ===== Section Glow Backgrounds ===== */
.section-glow {
position: relative;
}
.section-glow::before {
content: "";
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 600px;
height: 400px;
background: radial-gradient(ellipse, rgba(201, 169, 110, 0.05), transparent 70%);
pointer-events: none;
}
/* ===== Glass Card ===== */
.glass-card {
@apply rounded-2xl border backdrop-blur-sm transition-all duration-300;
@apply border-neutral-200/80 bg-white/90;
@apply dark:border-white/[0.06] dark:bg-white/[0.04];
}
.glass-card:hover {
@apply dark:border-[#c9a96e]/15 dark:bg-white/[0.06];
}
/* ===== Photo Filter ===== */
.photo-filter {
filter: saturate(0.7) sepia(0.15) brightness(0.95) contrast(1.05);
}
:is(.dark) .photo-filter {
filter: saturate(0.6) sepia(0.2) brightness(0.9) contrast(1.1);
}
/* ===== Custom Scrollbar ===== */
.styled-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(201, 169, 110, 0.25) transparent;
}
.styled-scrollbar::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.styled-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.styled-scrollbar::-webkit-scrollbar-thumb {
background: rgba(201, 169, 110, 0.25);
border-radius: 4px;
}
.styled-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(201, 169, 110, 0.4);
}

View File

@@ -5,7 +5,7 @@ export function Footer() {
const year = new Date().getFullYear(); const year = new Date().getFullYear();
return ( return (
<footer className="relative border-t border-neutral-200 bg-neutral-100 dark:border-white/[0.06] dark:bg-[#050505]"> <footer className="relative border-t border-neutral-200 bg-neutral-100 dark:border-white/[0.08] dark:bg-[#050505]">
<div className="section-divider absolute top-0 left-0 right-0" /> <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"> <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"> <p className="text-sm text-neutral-500">
@@ -13,7 +13,7 @@ export function Footer() {
</p> </p>
<div className="flex items-center gap-1.5 text-sm text-neutral-500"> <div className="flex items-center gap-1.5 text-sm text-neutral-500">
<span>Made with</span> <span>Made with</span>
<Heart size={14} className="fill-rose-500 text-rose-500" /> <Heart size={14} className="fill-[#c9a96e] text-[#c9a96e]" />
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { Menu, X } from "lucide-react"; import { Menu, X } from "lucide-react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { BRAND, NAV_LINKS } from "@/lib/constants"; import { BRAND, NAV_LINKS } from "@/lib/constants";
import { HeroLogo } from "@/components/ui/HeroLogo";
export function Header() { export function Header() {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
@@ -32,22 +32,15 @@ export function Header() {
<div <div
className="absolute inset-0 rounded-full transition-all duration-300 group-hover:scale-125" className="absolute inset-0 rounded-full transition-all duration-300 group-hover:scale-125"
style={{ style={{
background: "radial-gradient(circle, rgba(225,29,72,0.5) 0%, rgba(225,29,72,0.15) 50%, transparent 70%)", background: "radial-gradient(circle, rgba(201,169,110,0.5) 0%, rgba(201,169,110,0.15) 50%, transparent 70%)",
}} }}
/> />
<Image <HeroLogo
src="/images/logo.png" size={24}
alt={BRAND.name} className="relative text-black transition-transform duration-300 drop-shadow-[0_0_3px_rgba(201,169,110,0.5)] group-hover:scale-110"
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> </div>
<span className="font-display text-lg font-bold tracking-tight text-white"> <span className="font-display text-lg font-bold tracking-tight text-[#c9a96e]">
{BRAND.shortName} {BRAND.shortName}
</span> </span>
</Link> </Link>
@@ -57,7 +50,7 @@ export function Header() {
<a <a
key={link.href} key={link.href}
href={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" 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-[#c9a96e] after:transition-all after:duration-300 hover:text-white hover:after:w-full"
> >
{link.label} {link.label}
</a> </a>

View File

@@ -1,29 +1,24 @@
import { siteContent } from "@/data/content"; import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading"; import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal"; import { Reveal } from "@/components/ui/Reveal";
import { Heart } from "lucide-react";
export function About() { export function About() {
const { about } = siteContent; const { about } = siteContent;
return ( return (
<section id="about" className="relative section-padding bg-neutral-100 dark:bg-[#0a0a0a]"> <section id="about" className="section-glow relative section-padding bg-neutral-100 dark:bg-[#080808]">
<div className="section-divider absolute top-0 left-0 right-0" /> <div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container"> <div className="section-container">
<Reveal> <Reveal>
<SectionHeading>{about.title}</SectionHeading> <SectionHeading centered>{about.title}</SectionHeading>
</Reveal> </Reveal>
<div className="mt-10 max-w-3xl space-y-6"> <div className="mt-14 mx-auto max-w-2xl space-y-8 text-center">
{about.paragraphs.map((text, i) => ( {about.paragraphs.map((text, i) => (
<Reveal key={i}> <Reveal key={i}>
<div className="flex gap-4"> <p className="text-xl leading-relaxed text-neutral-600 dark:text-neutral-300 sm:text-2xl">
<Heart {text}
size={20} </p>
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> </Reveal>
))} ))}
</div> </div>

View File

@@ -1,12 +1,12 @@
"use client"; "use client";
import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { Flame, Sparkles, Wind, Zap, Star, Monitor, ArrowRight } from "lucide-react"; import { Flame, Sparkles, Wind, Zap, Star, Monitor } from "lucide-react";
import { siteContent } from "@/data/content"; import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading"; import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal"; import { Reveal } from "@/components/ui/Reveal";
import { ClassModal } from "@/components/ui/ClassModal"; import { ShowcaseLayout } from "@/components/ui/ShowcaseLayout";
import { useShowcaseRotation } from "@/hooks/useShowcaseRotation";
import type { ClassItem } from "@/types"; import type { ClassItem } from "@/types";
const iconMap: Record<string, React.ReactNode> = { const iconMap: Record<string, React.ReactNode> = {
@@ -20,70 +20,92 @@ const iconMap: Record<string, React.ReactNode> = {
export function Classes() { export function Classes() {
const { classes } = siteContent; const { classes } = siteContent;
const [selectedClass, setSelectedClass] = useState<ClassItem | null>(null); const { activeIndex, select, setHovering } = useShowcaseRotation({
totalItems: classes.items.length,
autoPlayInterval: 5000,
});
return ( return (
<section id="classes" className="relative section-padding bg-neutral-100 dark:bg-[#0a0a0a]"> <section id="classes" className="section-glow relative section-padding bg-neutral-100 dark:bg-[#080808]">
<div className="section-divider absolute top-0 left-0 right-0" /> <div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container"> <div className="section-container">
<Reveal> <Reveal>
<SectionHeading>{classes.title}</SectionHeading> <SectionHeading centered>{classes.title}</SectionHeading>
</Reveal> </Reveal>
<div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="mt-14">
{classes.items.map((item) => ( <Reveal>
<Reveal key={item.name} className="h-full"> <ShowcaseLayout<ClassItem>
<div items={classes.items}
className="group relative h-full min-h-[280px] cursor-pointer overflow-hidden rounded-2xl" activeIndex={activeIndex}
onClick={() => setSelectedClass(item)} onSelect={select}
> onHoverChange={setHovering}
{/* Background image */} renderDetail={(item) => (
<div>
{/* Hero image */}
{item.images && item.images[0] && ( {item.images && item.images[0] && (
<div className="relative aspect-[16/9] w-full overflow-hidden rounded-2xl">
<Image <Image
src={item.images[0]} src={item.images[0]}
alt={item.name} alt={item.name}
fill fill
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105" className="object-cover photo-filter"
/> />
)} {/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
{/* Dark gradient overlay */} {/* Icon + name 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" /> <div className="absolute bottom-0 left-0 right-0 p-6">
<div className="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-lg bg-[#c9a96e]/20 text-[#d4b87a] backdrop-blur-sm">
{/* 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]} {iconMap[item.icon]}
</div> </div>
<h3 className="text-2xl font-bold text-white">
<h3 className="text-xl font-semibold text-white">
{item.name} {item.name}
</h3> </h3>
</div>
</div>
)}
<p className="mt-1.5 text-sm leading-relaxed text-white/60 line-clamp-2"> {/* Description */}
{item.detailedDescription && (
<div className="mt-5 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 whitespace-pre-line">
{item.detailedDescription}
</div>
)}
</div>
)}
renderSelectorItem={(item, _i, isActive) => (
<div className="flex items-center gap-3 p-3">
{/* Icon */}
<div
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-colors ${
isActive
? "bg-[#c9a96e]/20 text-[#d4b87a]"
: "bg-neutral-200/50 text-neutral-500 dark:bg-white/[0.06] dark:text-neutral-400"
}`}
>
{iconMap[item.icon]}
</div>
<div className="min-w-0">
<p
className={`text-sm font-semibold truncate transition-colors ${
isActive
? "text-[#c9a96e]"
: "text-neutral-700 dark:text-neutral-300"
}`}
>
{item.name}
</p>
<p className="text-xs text-neutral-500 dark:text-neutral-500 truncate">
{item.description} {item.description}
</p> </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> </div>
</div> )}
</Reveal>
))}
</div>
</div>
<ClassModal
classItem={selectedClass}
onClose={() => setSelectedClass(null)}
/> />
</Reveal>
</div>
</div>
</section> </section>
); );
} }

View File

@@ -17,7 +17,7 @@ export function Contact() {
<div className="mt-10 space-y-5"> <div className="mt-10 space-y-5">
{contact.addresses.map((address, i) => ( {contact.addresses.map((address, i) => (
<div key={i} className="group flex items-center gap-4"> <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"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#c9a96e]/10 text-[#a08050] transition-colors group-hover:bg-[#c9a96e]/15 dark:bg-[#c9a96e]/10 dark:text-[#d4b87a] dark:group-hover:bg-[#c9a96e]/15">
<MapPin size={18} /> <MapPin size={18} />
</div> </div>
<p className="body-text">{address}</p> <p className="body-text">{address}</p>
@@ -25,34 +25,34 @@ export function Contact() {
))} ))}
<div className="group flex items-center gap-4"> <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"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#c9a96e]/10 text-[#a08050] transition-colors group-hover:bg-[#c9a96e]/15 dark:bg-[#c9a96e]/10 dark:text-[#d4b87a] dark:group-hover:bg-[#c9a96e]/15">
<Phone size={18} /> <Phone size={18} />
</div> </div>
<a <a
href={`tel:${contact.phone}`} href={`tel:${contact.phone}`}
className="text-neutral-600 transition-colors hover:text-rose-600 dark:text-neutral-400 dark:hover:text-rose-400" className="text-neutral-600 transition-colors hover:text-[#a08050] dark:text-neutral-300 dark:hover:text-[#d4b87a]"
> >
{contact.phone} {contact.phone}
</a> </a>
</div> </div>
<div className="group flex items-center gap-4"> <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"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#c9a96e]/10 text-[#a08050] transition-colors group-hover:bg-[#c9a96e]/15 dark:bg-[#c9a96e]/10 dark:text-[#d4b87a] dark:group-hover:bg-[#c9a96e]/15">
<Clock size={18} /> <Clock size={18} />
</div> </div>
<p className="body-text">{contact.workingHours}</p> <p className="body-text">{contact.workingHours}</p>
</div> </div>
<div className="border-t border-neutral-200 pt-5 dark:border-white/[0.06]"> <div className="border-t border-neutral-200 pt-5 dark:border-white/[0.08]">
<div className="group flex items-center gap-4"> <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"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#c9a96e]/10 text-[#a08050] transition-colors group-hover:bg-[#c9a96e]/15 dark:bg-[#c9a96e]/10 dark:text-[#d4b87a] dark:group-hover:bg-[#c9a96e]/15">
<Instagram size={18} /> <Instagram size={18} />
</div> </div>
<a <a
href={contact.instagram} href={contact.instagram}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-neutral-600 transition-colors hover:text-rose-600 dark:text-neutral-400 dark:hover:text-rose-400" className="text-neutral-600 transition-colors hover:text-[#a08050] dark:text-neutral-300 dark:hover:text-[#d4b87a]"
> >
{BRAND.instagramHandle} {BRAND.instagramHandle}
</a> </a>
@@ -62,7 +62,7 @@ export function Contact() {
</Reveal> </Reveal>
<Reveal> <Reveal>
<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)]"> <div className="overflow-hidden rounded-2xl border border-neutral-200 shadow-sm dark:border-white/[0.08] dark:shadow-[0_0_30px_rgba(201,169,110,0.05)]">
<iframe <iframe
src={contact.mapEmbedUrl} src={contact.mapEmbedUrl}
width="100%" width="100%"

View File

@@ -0,0 +1,66 @@
"use client";
import { useState } from "react";
import { Plus, Minus } from "lucide-react";
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
export function FAQ() {
const { faq } = siteContent;
const [openIndex, setOpenIndex] = useState<number | null>(null);
function toggle(index: number) {
setOpenIndex(openIndex === index ? null : index);
}
return (
<section id="faq" className="section-glow relative section-padding bg-neutral-100 dark:bg-[#080808]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container">
<Reveal>
<SectionHeading centered>{faq.title}</SectionHeading>
</Reveal>
<div className="mx-auto mt-14 max-w-3xl">
{faq.items.map((item, i) => (
<Reveal key={i}>
<div
className={`border-b border-neutral-200 dark:border-white/[0.06] ${
i === 0 ? "border-t" : ""
}`}
>
<button
onClick={() => toggle(i)}
className="flex w-full items-center justify-between gap-6 py-6 text-left transition-colors"
>
<span className="text-base font-medium text-neutral-900 dark:text-white sm:text-lg">
{item.question}
</span>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-100 transition-all duration-300 dark:bg-white/[0.06]">
{openIndex === i ? (
<Minus size={16} className="text-[#c9a96e]" />
) : (
<Plus size={16} className="text-neutral-400 dark:text-neutral-500" />
)}
</span>
</button>
<div
className={`grid transition-all duration-300 ease-out ${
openIndex === i ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
}`}
>
<div className="overflow-hidden">
<div className="pb-6 pr-14 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 sm:text-base whitespace-pre-line">
{item.answer}
</div>
</div>
</div>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,10 +1,9 @@
"use client"; "use client";
import Image from "next/image";
import { siteContent } from "@/data/content"; import { siteContent } from "@/data/content";
import { BRAND } from "@/lib/constants";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { FloatingHearts } from "@/components/ui/FloatingHearts"; import { FloatingHearts } from "@/components/ui/FloatingHearts";
import { HeroLogo } from "@/components/ui/HeroLogo";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
export function Hero() { export function Hero() {
@@ -24,7 +23,7 @@ export function Hero() {
top: "-10%", top: "-10%",
left: "50%", left: "50%",
transform: "translateX(-50%)", transform: "translateX(-50%)",
background: "radial-gradient(circle, rgba(225, 29, 72, 0.12), transparent 70%)", background: "radial-gradient(circle, rgba(201, 169, 110, 0.12), transparent 70%)",
}} }}
/> />
<div <div
@@ -34,7 +33,7 @@ export function Hero() {
height: "300px", height: "300px",
bottom: "10%", bottom: "10%",
right: "10%", right: "10%",
background: "radial-gradient(circle, rgba(225, 29, 72, 0.08), transparent 70%)", background: "radial-gradient(circle, rgba(201, 169, 110, 0.08), transparent 70%)",
animationDelay: "3s", animationDelay: "3s",
}} }}
/> />
@@ -44,30 +43,17 @@ export function Hero() {
{/* Content */} {/* Content */}
<div className="section-container relative z-10 text-center"> <div className="section-container relative z-10 text-center">
<div className="hero-logo relative mx-auto mb-10 h-[220px] w-[220px]"> <div className="hero-logo relative mx-auto mb-10 flex items-center justify-center" style={{ width: 220, height: 181 }}>
{/* Outer ambient glow */} {/* Soft ambient glow behind heart */}
<div className="absolute -inset-16 rounded-full bg-rose-500/8 blur-[60px]" /> <div className="absolute -inset-10 rounded-full blur-[80px]" style={{ background: "radial-gradient(circle, rgba(201,169,110,0.15), transparent 70%)" }} />
{/* Rose disc — makes black heart visible as silhouette */} <div className="hero-logo-heartbeat relative">
<div <HeroLogo
className="absolute inset-2 rounded-full" size={220}
style={{ animated
background: "radial-gradient(circle, rgba(225,29,72,0.45) 0%, rgba(225,29,72,0.18) 45%, transparent 70%)", className="drop-shadow-[0_0_8px_rgba(201,169,110,0.35)] drop-shadow-[0_0_30px_rgba(201,169,110,0.15)]"
}}
/>
<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> </div>
</div>
<h1 className="hero-title font-display text-5xl font-bold tracking-tight sm:text-6xl lg:text-8xl"> <h1 className="hero-title font-display text-5xl font-bold tracking-tight sm:text-6xl lg:text-8xl">
<span className="gradient-text">{hero.headline}</span> <span className="gradient-text">{hero.headline}</span>
@@ -88,7 +74,7 @@ export function Hero() {
<div className="hero-cta absolute bottom-8 left-1/2 -translate-x-1/2"> <div className="hero-cta absolute bottom-8 left-1/2 -translate-x-1/2">
<a <a
href="#about" href="#about"
className="flex flex-col items-center gap-1 text-neutral-600 transition-colors hover:text-rose-400" className="flex flex-col items-center gap-1 text-neutral-600 transition-colors hover:text-[#d4b87a]"
> >
<span className="text-xs uppercase tracking-widest">Scroll</span> <span className="text-xs uppercase tracking-widest">Scroll</span>
<ChevronDown size={20} className="animate-bounce" /> <ChevronDown size={20} className="animate-bounce" />

View File

@@ -0,0 +1,139 @@
"use client";
import { useState } from "react";
import { CreditCard, Building2, ScrollText } from "lucide-react";
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
type Tab = "prices" | "rental" | "rules";
export function Pricing() {
const { pricing } = siteContent;
const [activeTab, setActiveTab] = useState<Tab>("prices");
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: "prices", label: "Абонементы", icon: <CreditCard size={16} /> },
{ id: "rental", label: "Аренда зала", icon: <Building2 size={16} /> },
{ id: "rules", label: "Правила", icon: <ScrollText size={16} /> },
];
return (
<section id="pricing" className="section-glow 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 centered>{pricing.title}</SectionHeading>
</Reveal>
{/* Tabs */}
<Reveal>
<div className="mt-12 flex flex-wrap justify-center gap-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`inline-flex items-center gap-2 rounded-full px-6 py-2.5 text-sm font-medium transition-all duration-300 ${
activeTab === tab.id
? "bg-[#c9a96e] text-black shadow-lg shadow-[#c9a96e]/25"
: "bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-white/[0.06] dark:text-neutral-300 dark:hover:bg-white/[0.1]"
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
</Reveal>
{/* Prices tab */}
{activeTab === "prices" && (
<Reveal>
<div className="mx-auto mt-10 max-w-2xl">
<p className="mb-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
{pricing.subtitle}
</p>
<div className="overflow-hidden rounded-2xl border border-neutral-200 dark:border-white/[0.06]">
{pricing.items.map((item, i) => (
<div
key={i}
className={`group flex items-center justify-between gap-4 px-6 py-5 transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.03] ${
i > 0 ? "border-t border-neutral-100 dark:border-white/[0.04]" : ""
}`}
>
<div>
<p className="font-medium text-neutral-900 dark:text-white">
{item.name}
</p>
{item.note && (
<p className="mt-0.5 text-sm text-neutral-500 dark:text-neutral-400">
{item.note}
</p>
)}
</div>
<span className="shrink-0 font-display text-xl font-bold text-[#a08050] dark:text-[#d4b87a]">
{item.price}
</span>
</div>
))}
</div>
</div>
</Reveal>
)}
{/* Rental tab */}
{activeTab === "rental" && (
<Reveal>
<div className="mx-auto mt-10 max-w-2xl">
<div className="overflow-hidden rounded-2xl border border-neutral-200 dark:border-white/[0.06]">
{pricing.rentalItems.map((item, i) => (
<div
key={i}
className={`group flex items-center justify-between gap-4 px-6 py-5 transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.03] ${
i > 0 ? "border-t border-neutral-100 dark:border-white/[0.04]" : ""
}`}
>
<div>
<p className="font-medium text-neutral-900 dark:text-white">
{item.name}
</p>
{item.note && (
<p className="mt-0.5 text-sm text-neutral-500 dark:text-neutral-400">
{item.note}
</p>
)}
</div>
<span className="shrink-0 font-display text-xl font-bold text-[#a08050] dark:text-[#d4b87a]">
{item.price}
</span>
</div>
))}
</div>
</div>
</Reveal>
)}
{/* Rules tab */}
{activeTab === "rules" && (
<Reveal>
<div className="mx-auto mt-10 max-w-2xl space-y-4">
{pricing.rules.map((rule, i) => (
<div
key={i}
className="flex gap-4"
>
<span className="mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#c9a96e]/10 text-xs font-bold text-[#a08050] dark:bg-[#c9a96e]/10 dark:text-[#d4b87a]">
{i + 1}
</span>
<p className="text-sm leading-relaxed text-neutral-700 dark:text-neutral-300 sm:text-base">
{rule}
</p>
</div>
))}
</div>
</Reveal>
)}
</div>
</section>
);
}

View File

@@ -1,195 +1,108 @@
"use client"; "use client";
import { useState, useRef, useEffect, useCallback } from "react";
import Image from "next/image"; import Image from "next/image";
import { Instagram, ChevronLeft, ChevronRight } from "lucide-react"; import { Instagram } from "lucide-react";
import { siteContent } from "@/data/content"; import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading"; import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal"; import { Reveal } from "@/components/ui/Reveal";
import { TeamMemberModal } from "@/components/ui/TeamMemberModal"; import { ShowcaseLayout } from "@/components/ui/ShowcaseLayout";
import { useShowcaseRotation } from "@/hooks/useShowcaseRotation";
import type { TeamMember } from "@/types"; import type { TeamMember } from "@/types";
export function Team() { export function Team() {
const { team } = siteContent; const { team } = siteContent;
const [selectedMember, setSelectedMember] = useState<TeamMember | null>(null); const { activeIndex, select, setHovering } = useShowcaseRotation({
const scrollRef = useRef<HTMLDivElement>(null); totalItems: team.members.length,
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 ( return (
<section id="team" className="relative section-padding bg-neutral-50 dark:bg-[#050505]"> <section id="team" className="section-glow relative section-padding bg-neutral-50 dark:bg-[#050505]">
<div className="section-divider absolute top-0 left-0 right-0" /> <div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container"> <div className="section-container">
<Reveal> <Reveal>
<SectionHeading>{team.title}</SectionHeading> <SectionHeading centered>{team.title}</SectionHeading>
</Reveal> </Reveal>
</div>
{/* Carousel wrapper */} <div className="mt-10">
<Reveal> <Reveal>
<div className="relative mt-10"> <ShowcaseLayout<TeamMember>
{/* Scroll container */} items={team.members}
<div activeIndex={activeIndex}
ref={scrollRef} onSelect={select}
onScroll={handleScroll} onHoverChange={setHovering}
onMouseDown={handleMouseDown} renderDetail={(member) => (
onMouseMove={handleMouseMove} <div className="relative aspect-[3/4] max-h-[600px] w-full overflow-hidden rounded-2xl">
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
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)}
>
{/* Photo */}
<div className="aspect-[3/4] w-full overflow-hidden">
<Image <Image
src={member.image} src={member.image}
alt={member.name} alt={member.name}
width={260} fill
height={347} className="object-cover photo-filter"
className="h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/> />
</div>
{/* Gradient overlay */} {/* 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" /> <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
{/* Rose glow on hover */} {/* Text over photo */}
<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" /> <div className="absolute bottom-0 left-0 right-0 p-6 sm:p-8">
<h3 className="text-2xl font-bold text-white sm:text-3xl">
{/* 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} {member.name}
</h3> </h3>
<p className="mt-1 text-sm font-medium text-[#d4b87a]">
{member.role}
</p>
{member.instagram && ( {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()}
>
<Instagram size={12} className="shrink-0" />
<a <a
href={member.instagram} href={member.instagram}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="mt-2 inline-flex items-center gap-1.5 text-sm text-white/60 transition-colors hover:text-[#d4b87a]"
> >
<Instagram size={14} />
{member.instagram.split("/").filter(Boolean).pop()} {member.instagram.split("/").filter(Boolean).pop()}
</a> </a>
</span> )}
{member.description && (
<p className="mt-4 text-sm leading-relaxed text-white/70 line-clamp-4 sm:line-clamp-6">
{member.description}
</p>
)} )}
</div> </div>
</div> </div>
))} )}
</div> renderSelectorItem={(member, _i, isActive) => (
<div className="flex items-center gap-3 p-2.5 lg:p-3">
{/* Side navigation arrows */} {/* Thumbnail */}
<button <div className="relative h-12 w-12 shrink-0 overflow-hidden rounded-lg lg:h-14 lg:w-14">
onClick={() => scroll("left")} <Image
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" src={member.image}
aria-label="Назад" alt={member.name}
> fill
<ChevronLeft size={22} /> className="object-cover photo-filter"
</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>
</Reveal>
<TeamMemberModal
member={selectedMember}
onClose={() => setSelectedMember(null)}
/> />
</div>
<div className="min-w-0">
<p
className={`text-sm font-semibold truncate transition-colors ${
isActive
? "text-[#c9a96e]"
: "text-neutral-700 dark:text-neutral-300"
}`}
>
{member.name}
</p>
<p className="text-xs text-neutral-500 dark:text-neutral-500 truncate">
{member.role}
</p>
</div>
</div>
)}
/>
</Reveal>
</div>
</div>
</section> </section>
); );
} }

View File

@@ -1,119 +0,0 @@
"use client";
import { useEffect } from "react";
import Image from "next/image";
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={20} />,
sparkles: <Sparkles size={20} />,
wind: <Wind size={20} />,
zap: <Zap size={20} />,
star: <Star size={20} />,
monitor: <Monitor size={20} />,
};
interface ClassModalProps {
classItem: ClassItem | null;
onClose: () => void;
}
export function ClassModal({ classItem, onClose }: ClassModalProps) {
useEffect(() => {
if (!classItem) return;
document.body.style.overflow = "hidden";
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.body.style.overflow = "";
document.removeEventListener("keydown", handleKeyDown);
};
}, [classItem, onClose]);
if (!classItem) return null;
const heroImage = classItem.images?.[0];
return (
<div
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 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()}
>
{/* 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" />
{/* 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="p-6 text-sm leading-relaxed whitespace-pre-line text-neutral-600 dark:text-neutral-400">
{classItem.detailedDescription}
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -33,7 +33,7 @@ export function FloatingHearts() {
{hearts.map((heart) => ( {hearts.map((heart) => (
<div <div
key={heart.id} key={heart.id}
className="absolute text-rose-500" className="absolute text-[#c9a96e]"
style={{ style={{
left: `${heart.left}%`, left: `${heart.left}%`,
bottom: "-20px", bottom: "-20px",

View File

@@ -0,0 +1,45 @@
interface HeroLogoProps {
className?: string;
size?: number;
animated?: boolean;
}
export function HeroLogo({ className = "", size = 220, animated = false }: HeroLogoProps) {
const h = Math.round(size * (192 / 234));
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 234 192"
width={size}
height={h}
className={className}
role="img"
aria-label="Black Heart logo"
>
{animated && (
<defs>
<linearGradient id="heart-gradient" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#000">
<animate attributeName="stop-color" values="#000;#c9a96e;#000;#000;#000" dur="6s" repeatCount="indefinite" />
</stop>
<stop offset="35%" stopColor="#000">
<animate attributeName="stop-color" values="#000;#000;#c9a96e;#000;#000" dur="6s" repeatCount="indefinite" />
</stop>
<stop offset="65%" stopColor="#000">
<animate attributeName="stop-color" values="#000;#000;#000;#c9a96e;#000" dur="6s" repeatCount="indefinite" />
</stop>
<stop offset="100%" stopColor="#000">
<animate attributeName="stop-color" values="#000;#000;#000;#c9a96e;#000" dur="6s" repeatCount="indefinite" />
</stop>
</linearGradient>
</defs>
)}
<path
fill={animated ? "url(#heart-gradient)" : "currentColor"}
fillRule="evenodd"
d="M118.02,188.43 C118.04,184.10 120.51,173.30 122.96,166.79 C126.11,158.42 133.55,147.62 144.55,135.42 C165.53,112.15 170.96,101.38 170.99,82.98 C171.00,72.35 168.51,62.96 162.47,50.94 C160.00,46.02 157.78,42.00 157.53,42.00 C157.29,42.00 158.24,45.04 159.64,48.75 C163.04,57.78 165.96,71.24 165.96,78.00 C165.97,85.89 163.51,95.22 159.27,103.40 C156.31,109.09 152.52,113.57 140.17,126.00 C131.69,134.53 123.42,143.44 121.79,145.81 C116.23,153.88 110.87,167.99 109.81,177.28 C109.51,179.99 109.37,179.90 105.02,174.28 C102.55,171.10 98.74,166.54 96.55,164.15 L92.56,159.80 L95.53,157.70 C100.61,154.12 105.90,148.12 108.76,142.70 C111.22,138.02 111.50,136.50 111.47,127.50 C111.45,118.43 111.02,116.15 106.86,103.00 C101.17,85.06 99.60,76.75 100.25,68.17 C100.75,61.51 104.60,48.83 107.29,45.00 C108.57,43.16 108.76,43.69 109.31,50.84 C110.42,65.22 115.99,75.08 126.37,81.04 C133.31,85.02 133.82,84.76 128.92,79.75 C124.20,74.93 119.44,65.68 118.16,58.84 C116.46,49.75 119.09,39.24 125.73,28.59 L128.79,23.69 L130.02,29.59 C130.69,32.84 133.23,39.92 135.65,45.33 C143.15,62.02 144.36,69.90 141.53,83.29 C140.04,90.28 134.00,104.04 127.66,114.86 C125.68,118.24 124.39,120.97 124.78,120.93 C125.18,120.90 128.41,117.53 131.97,113.46 C145.06,98.47 150.50,85.84 150.46,70.50 C150.43,59.80 149.70,57.36 141.46,40.79 C137.98,33.80 134.83,26.02 134.45,23.49 C133.85,19.50 134.07,18.56 136.10,16.40 C139.50,12.77 147.93,7.39 153.86,5.06 L159.00,3.03 L159.00,9.32 C159.00,18.37 162.11,24.01 172.06,33.00 C176.46,36.97 180.72,41.50 181.53,43.06 C183.67,47.20 183.39,56.94 180.95,63.13 C178.14,70.25 180.87,67.95 184.97,59.74 C190.78,48.12 188.70,39.73 177.15,28.15 C173.28,24.27 169.43,20.06 168.61,18.80 C166.51,15.58 164.79,7.21 165.54,3.83 C166.16,1.00 166.17,1.00 174.33,1.01 C178.82,1.02 184.15,1.29 186.17,1.63 C189.69,2.21 189.81,2.37 189.29,5.62 C188.42,10.94 190.83,20.71 195.10,29.20 C197.26,33.49 199.19,37.00 199.40,37.00 C199.62,37.00 198.67,33.51 197.31,29.25 C195.35,23.12 194.91,19.90 195.17,13.83 C195.36,9.61 195.85,5.81 196.28,5.39 C197.35,4.31 205.52,8.43 211.67,13.15 C218.45,18.35 221.00,23.72 220.99,32.74 C220.98,36.46 220.28,41.98 219.42,45.00 C218.57,48.02 217.63,51.40 217.34,52.50 C216.30,56.51 222.34,45.32 224.52,39.20 C225.76,35.73 227.01,32.66 227.29,32.38 C227.57,32.09 228.79,34.48 229.99,37.68 C238.21,59.57 232.78,83.80 215.76,101.15 C209.43,107.60 207.42,108.54 209.17,104.25 C210.91,100.00 210.37,85.54 208.08,74.64 C206.93,69.22 205.95,62.24 205.90,59.14 C205.80,53.85 205.74,53.72 204.93,57.00 C204.45,58.92 204.13,68.15 204.22,77.50 C204.37,93.11 204.20,94.91 202.15,99.45 C198.99,106.51 192.06,115.46 190.76,114.16 C188.49,111.89 189.93,84.88 192.72,77.19 C194.09,73.45 189.30,79.05 186.68,84.26 C182.02,93.55 180.69,101.03 181.41,113.97 L182.05,125.50 L169.94,135.00 C153.90,147.58 132.06,170.01 124.13,182.05 C118.83,190.09 118.00,190.96 118.02,188.43 Z M83.09,150.59 C78.00,145.44 77.78,144.99 78.44,141.34 C78.82,139.23 81.24,133.00 83.81,127.50 C88.47,117.57 88.50,117.43 88.49,107.00 C88.47,99.38 87.96,94.99 86.59,91.00 C84.28,84.21 77.06,69.61 76.36,70.30 C76.08,70.58 76.56,72.19 77.43,73.87 C79.91,78.65 82.99,92.88 82.99,99.57 C83.00,108.39 80.69,114.86 73.96,124.82 C70.68,129.67 68.00,134.17 68.00,134.82 C68.00,135.47 67.62,136.00 67.16,136.00 C66.07,136.00 57.00,128.93 57.00,128.07 C57.00,127.71 59.03,123.47 61.50,118.66 C66.60,108.75 67.24,103.18 64.48,92.59 C62.01,83.09 61.32,83.22 61.40,93.17 C61.45,100.19 61.02,103.39 59.69,106.10 C57.49,110.57 48.29,121.00 46.56,121.00 C44.40,121.00 39.79,109.24 39.24,102.34 C38.56,93.90 40.48,89.09 48.68,78.77 C62.32,61.60 65.53,49.22 60.98,31.41 C58.70,22.51 54.61,13.20 50.08,6.62 C47.54,2.92 47.30,2.10 48.60,1.60 C50.50,0.87 66.31,0.80 68.17,1.51 C69.18,1.90 69.42,4.19 69.15,10.95 C68.88,18.05 69.27,21.45 71.06,27.48 C72.30,31.66 73.77,35.36 74.33,35.70 C74.97,36.10 75.06,35.62 74.57,34.41 C74.15,33.36 73.57,28.88 73.27,24.45 C72.70,15.76 74.85,5.38 77.44,4.39 C79.59,3.56 92.09,10.37 97.73,15.45 C100.57,18.00 103.83,21.61 104.99,23.48 L107.09,26.88 L103.66,31.69 C94.93,43.97 91.54,55.17 91.64,71.50 C91.72,84.83 92.69,89.79 99.08,109.50 C105.86,130.41 104.79,139.90 94.39,151.01 C91.83,153.75 89.44,156.00 89.08,156.00 C88.72,156.00 86.03,153.57 83.09,150.59 Z M29.50,109.90 C26.20,107.67 21.64,104.05 19.38,101.84 L15.26,97.83 L17.24,92.67 C19.86,85.83 19.20,74.50 15.57,64.04 C12.16,54.24 10.98,53.26 12.75,61.71 C14.48,69.97 13.94,81.02 11.53,86.50 L9.77,90.50 L6.92,84.50 C2.82,75.84 1.00,67.75 1.00,58.18 C1.00,42.04 6.09,29.69 17.39,18.40 C23.48,12.31 32.07,6.09 30.82,8.67 C30.59,9.13 28.88,12.62 27.02,16.44 C21.43,27.90 22.74,38.16 31.08,48.28 C35.14,53.21 36.55,53.01 33.93,47.87 C31.25,42.61 30.40,34.47 31.90,28.31 C33.17,23.08 42.81,3.00 44.05,3.00 C44.47,3.00 46.18,6.79 47.86,11.42 C58.07,39.63 56.90,53.23 42.67,72.14 C31.96,86.38 29.60,96.21 33.92,108.52 C34.98,111.54 35.77,113.99 35.67,113.97 C35.58,113.96 32.80,112.12 29.50,109.90 Z"
/>
</svg>
);
}

View File

@@ -1,15 +1,22 @@
interface SectionHeadingProps { interface SectionHeadingProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
centered?: boolean;
} }
export function SectionHeading({ children, className = "" }: SectionHeadingProps) { export function SectionHeading({ children, className = "", centered = false }: SectionHeadingProps) {
return ( return (
<div className={centered ? "text-center" : ""}>
<h2 <h2
className={`font-display text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl ${className}`} className={`font-display text-4xl font-bold uppercase tracking-wide sm:text-5xl lg:text-6xl heading-text ${className}`}
> >
{children} {children}
<span className="mt-3 block h-[2px] w-16 rounded-full bg-gradient-to-r from-rose-500 to-rose-500/0" />
</h2> </h2>
<span
className={`mt-4 block h-[1px] w-20 bg-gradient-to-r from-[#c9a96e] to-transparent ${
centered ? "mx-auto" : ""
}`}
/>
</div>
); );
} }

View File

@@ -0,0 +1,106 @@
"use client";
import { useRef, useEffect, useState } from "react";
interface ShowcaseLayoutProps<T> {
items: T[];
activeIndex: number;
onSelect: (index: number) => void;
onHoverChange?: (hovering: boolean) => void;
renderDetail: (item: T, index: number) => React.ReactNode;
renderSelectorItem: (item: T, index: number, isActive: boolean) => React.ReactNode;
}
export function ShowcaseLayout<T>({
items,
activeIndex,
onSelect,
onHoverChange,
renderDetail,
renderSelectorItem,
}: ShowcaseLayoutProps<T>) {
const selectorRef = useRef<HTMLDivElement>(null);
const activeItemRef = useRef<HTMLButtonElement>(null);
const [isUserInteracting, setIsUserInteracting] = useState(false);
// Auto-scroll selector only when item is out of view
useEffect(() => {
if (isUserInteracting) return;
const container = selectorRef.current;
const activeEl = activeItemRef.current;
if (!container || !activeEl) return;
const isHorizontal = window.innerWidth < 1024;
if (isHorizontal) {
const elLeft = activeEl.offsetLeft;
const elRight = elLeft + activeEl.offsetWidth;
const scrollLeft = container.scrollLeft;
const viewRight = scrollLeft + container.offsetWidth;
if (elLeft < scrollLeft || elRight > viewRight) {
const left = elLeft - container.offsetWidth / 2 + activeEl.offsetWidth / 2;
container.scrollTo({ left, behavior: "smooth" });
}
} else {
const elTop = activeEl.offsetTop;
const elBottom = elTop + activeEl.offsetHeight;
const scrollTop = container.scrollTop;
const viewBottom = scrollTop + container.offsetHeight;
if (elTop < scrollTop || elBottom > viewBottom) {
const top = elTop - container.offsetHeight / 2 + activeEl.offsetHeight / 2;
container.scrollTo({ top, behavior: "smooth" });
}
}
}, [activeIndex, isUserInteracting]);
function handleMouseEnter() {
setIsUserInteracting(true);
onHoverChange?.(true);
}
function handleMouseLeave() {
setIsUserInteracting(false);
onHoverChange?.(false);
}
return (
<div
className="flex flex-col gap-6 lg:flex-row lg:gap-8"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Detail area */}
<div className="lg:w-[60%]">
<div key={activeIndex} className="showcase-detail-enter">
{renderDetail(items[activeIndex], activeIndex)}
</div>
</div>
{/* Selector */}
<div className="lg:w-[40%]">
<div
ref={selectorRef}
className="styled-scrollbar flex gap-3 overflow-x-auto pb-2 lg:max-h-[600px] lg:flex-col lg:overflow-y-auto lg:overflow-x-visible lg:pb-0 lg:pr-1"
>
{items.map((item, i) => (
<button
key={i}
ref={i === activeIndex ? activeItemRef : null}
onClick={() => onSelect(i)}
className={`flex-shrink-0 cursor-pointer rounded-xl border-2 text-left transition-all duration-300 ${
i === activeIndex
? "border-[#c9a96e]/60 bg-[#c9a96e]/10 dark:bg-[#c9a96e]/5"
: "border-transparent bg-neutral-100 hover:bg-neutral-200 dark:bg-white/[0.03] dark:hover:bg-white/[0.06]"
}`}
>
{renderSelectorItem(item, i, i === activeIndex)}
</button>
))}
</div>
</div>
</div>
);
}

View File

@@ -1,91 +0,0 @@
"use client";
import { useEffect } from "react";
import Image from "next/image";
import { X, Instagram } from "lucide-react";
import type { TeamMember } from "@/types";
interface TeamMemberModalProps {
member: TeamMember | null;
onClose: () => void;
}
export function TeamMemberModal({ member, onClose }: TeamMemberModalProps) {
useEffect(() => {
if (!member) return;
document.body.style.overflow = "hidden";
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.body.style.overflow = "";
document.removeEventListener("keydown", handleKeyDown);
};
}, [member, onClose]);
if (!member) return null;
return (
<div
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 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()}
>
{/* 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" />
{/* 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>
{/* 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="mt-2 inline-flex items-center gap-2 text-sm text-white/70 transition-colors hover:text-rose-400"
>
<Instagram size={15} className="shrink-0" />
<span>{member.instagram.split("/").filter(Boolean).pop()}</span>
</a>
)}
</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>
);
}

View File

@@ -8,8 +8,7 @@ export function ThemeToggle() {
useEffect(() => { useEffect(() => {
const stored = localStorage.getItem("theme"); const stored = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; const isDark = stored !== "light";
const isDark = stored === "dark" || (!stored && prefersDark);
setDark(isDark); setDark(isDark);
document.documentElement.classList.toggle("dark", isDark); document.documentElement.classList.toggle("dark", isDark);
}, []); }, []);
@@ -25,9 +24,9 @@ export function ThemeToggle() {
<button <button
onClick={toggle} onClick={toggle}
aria-label="Переключить тему" aria-label="Переключить тему"
className="social-icon rounded-full p-2" className="rounded-full p-2 text-neutral-400 transition-all duration-300 hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-500 dark:hover:bg-white/[0.05] dark:hover:text-white"
> >
{dark ? <Sun size={20} /> : <Moon size={20} />} {dark ? <Sun size={18} /> : <Moon size={18} />}
</button> </button>
); );
} }

View File

@@ -24,8 +24,8 @@ export const siteContent: SiteContent = {
title: "Настоящие профи!", title: "Настоящие профи!",
members: [ members: [
{ {
name: "Виктор Артемов", name: "Виктор Артёмов",
role: "Тренер", role: "Pole Fitness · Exotic · Strip",
image: "/images/team/viktor-artyomov.webp", image: "/images/team/viktor-artyomov.webp",
instagram: "https://instagram.com/viktor.artyomov/", instagram: "https://instagram.com/viktor.artyomov/",
description: description:
@@ -33,103 +33,123 @@ export const siteContent: SiteContent = {
}, },
{ {
name: "Анна Тарыба", name: "Анна Тарыба",
role: "Тренер", role: "Exotic Pole Dance",
image: "/images/team/anna-taryba.webp", image: "/images/team/anna-taryba.webp",
instagram: "https://instagram.com/annataryba/", instagram: "https://instagram.com/annataryba/",
description: description:
"Я смогла в кратчайшие сроки достичь высочайших вершин в Exotic Pole Dance. Многократная призёрка чемпионатов в различных категориях. Основала свою команду ExoTeAM, где готовлю учениц к выходу на сцену. Люблю создавать хореографии в разных жанрах — от ярких и сложных до выразительных и плавных. Веду учеников от начального уровня до выступлений и медалей. Помогу освоить любые элементы и достичь идеальных линий!", "Мощь и сила в каждой связке. Мои акцентные хореографии созданы для продвинутого уровня, где вы сможете раскрыть свой потенциал и почувствовать себя настоящей королевой танца. Готовьтесь к интенсивному погружению в мир уверенных движений и сложных элементов, где каждое занятие — это новый вызов и триумф!",
}, },
{ {
name: "Анастасия Чалей", name: "Анастасия Чалей",
role: "Тренер", role: "Exotic Pole Dance",
image: "/images/team/anastasia-chaley.webp", image: "/images/team/anastasia-chaley.webp",
instagram: "https://instagram.com/nastya_chaley/", instagram: "https://instagram.com/nastya_chaley/",
description: description:
"Я тренер-хореограф по Exotic Pole Dance и Strip. Танцевала абсолютно разные стили — хип-хоп, джаз-фанк, вог, хаус, поппинг, крамп, дэнсхолл, тверк — поэтому мои хореографии не похожи одна на другую. Люблю как яркие и акцентные танцы, так и плавные и тягучие. Со мной вы сможете насладиться всеми сторонами своей личности. Призёрка множества чемпионатов. Приходите на занятия — танцы это радость!", "Вас ждут креативные хореографии, акцент на музыкальность и подачу, развитие уверенности и раскрытие вашей индивидуальности. Присоединяйтесь к тренировкам, где царит атмосфера радости и танцевального вдохновения! Мой вайб«танцы это радость».",
}, },
{ {
name: "Ольга Демидова", name: "Ольга Демидова",
role: "Тренер", role: "Pole Dance",
image: "/images/team/olga-demidova.webp", image: "/images/team/olga-demidova.webp",
instagram: "https://instagram.com/don_olga_red/", instagram: "https://instagram.com/don_olga_red/",
description: description:
начала заниматься Pole Dance 5 лет назад с нуля. За это время участвовала и становилась призёром чемпионатов по Pole Art, Pole Sport и Exotic Pole Dance. У меня крепкая трюковая база, я знаю, как повысить гибкость, и всему этому смогу научить вас на своих занятиях. Люблю свою работу и жду вас на тренировках!", вдохновляющий лидер, который открывает двери в мир удивительного Pole Dance. С каждым занятием помогаю своим ученикам преодолевать собственные границы и достигать результатов, которые казались недостижимыми.",
}, },
{ {
name: "Галина Савицкая", name: "Ирина Третьякович",
role: "Тренер", role: "Exotic Pole Dance",
image: "/images/team/galina-savitskaya.webp",
description:
"Безумно люблю растяжку и помогу полюбить её и вам! Использую упражнения ЛФК и точечные лайфхаки для удобных положений при растяжке ног, спины и плеч. Научу тянуться в паре и чувствовать безопасное расслабление и напряжение. 10 лет занимаюсь растяжкой в условиях пилонного спорта и танца.",
},
{
name: "Ирина Третьюкович",
role: "Тренер",
image: "/images/team/irina-tretyukovich.webp", image: "/images/team/irina-tretyukovich.webp",
instagram: "https://instagram.com/irkatretya/", instagram: "https://instagram.com/irkatretya/",
description: description:
"Я тренер по Exotic Pole Dance. За короткий период смогла выйти на профессиональный уровень и поучаствовать во многих чемпионатах, в том числе международных — конечно же, не без призовых мест! Моя сильная сторона — трюковые комбинации на пилоне и их использование в танцевальных связках. Если вам нужны сильные руки, красивое подтянутое тело, музыкальность и пластичность — буду ждать на своих тренировках!", "Вас ждёт калейдоскоп эмоций: от сексуальной связки до нежной лирики и даже мистического драйва. Мои хореографии всегда энергичны и непредсказуемы, пробуждают самые смелые ваши стороны. Приготовьтесь к скоростному погружению в мир танца, где каждое движение — это вызов и откровение!",
}, },
{ {
name: "Надежда Сыч", name: "Надежда Сыч",
role: "Тренер", role: "Exotic Pole Dance · Body Plastic",
image: "/images/team/nadezhda-sukh.webp", image: "/images/team/nadezhda-sukh.webp",
instagram: "https://instagram.com/nadja.dance/", instagram: "https://instagram.com/nadja.dance/",
description: description:
"Я обучаю партерной акробатике, балансам, трюковому пилону и сексуальным танцам. Занятия у меня — это волшебное путешествие, где вы научитесь основам акробатических элементов, разовьёте навыки в балансах и стойках, а флаги и трюковые комбинации с пилоном не будут казаться чем-то недостижимым. Вы раскроете свою индивидуальность через чувственные танцы, наполненные грацией и пластикой. Присоединяйтесь и окунитесь в мир, где танец становится искусством!", "Со мной вы научитесь кайфовать от себя и раскрывать свою сексуальность. Помогу развить силу, баланс и пластику, а главное — почувствовать себя желанной и привлекательной.",
}, },
{ {
name: "Ирина Карпусь", name: "Ирина Карпусь",
role: "Тренер", role: "Exotic Pole Dance",
image: "/images/team/irina-karpus.webp", image: "/images/team/irina-karpus.webp",
instagram: "https://instagram.com/karpus_iri/", instagram: "https://instagram.com/karpus_iri/",
description: description:
"Я пришла в Exotic Pole Dance относительно недавно и полюбила его навсегда. В танце люблю и стремлюсь к красивым линиям и элегантности, но никогда не забываю про силовую часть и трюки. На занятиях стараюсь найти к каждому индивидуальный подход, чтобы тренировка была комфортной и продуктивной. Помогу раскрыть ваши сильные стороны и полюбить танец. Буду ждать вас на занятиях!", "Я проводник в мир чувственного Exotic Pole Dance. Мои хореографии проникают в самое сердце, а занятия — идеальный старт для тех, кто хочет раскрыть свою женственность и уверенность в себе.",
}, },
{ {
name: "Юлия Книга", name: "Юлия Книга",
role: "Тренер", role: "Erotic Pole Dance",
image: "/images/team/yuliya-kniga.webp", image: "/images/team/yuliya-kniga.webp",
instagram: "https://instagram.com/knigynzel/", instagram: "https://instagram.com/knigynzel/",
description: description:
тренер по Exotic Pole Dance. В прошлом была танцовщицей эротического жанра, откуда и пошла моя любовь к танцам. Я точно знаю все техники раскрепощения, научу тебя быть плавной, музыкальной и сексуальной. Мои хореографии могут быть как быстрыми и динамичными с трюковыми элементами, так и медленными, томными и манящими. Помогу раскрыть тебя как танцора со всех сторон!", не просто инструктор, я настоящий вдохновитель и проводник в мир Erotic Pole Dance. Мои тренировки — это не просто набор упражнений, это целое искусство, в котором каждая из вас чувствует себя особенной и ценной.",
}, },
{ {
name: "Алена Чигилейчик", name: "Алёна Чигилейчик",
role: "Тренер", role: "Exotic Pole Dance",
image: "/images/team/elena-chigileychik.webp", image: "/images/team/elena-chigileychik.webp",
instagram: "https://instagram.com/alenachygi/", instagram: "https://instagram.com/alenachygi/",
description: description:
"За несколько лет я смогла самостоятельно обучиться Exotic Pole Dance и занять 3 место в категории профи. Имею отличную спортивную базу. Танцую в основном flow, но всегда ищу новое и меняю стили хореографий. Обожаю эмоциональную подачу и точность в движениях, ощущение каждого сантиметра тела и то, как музыка позволяет раскрываться в танце. Научу чувствовать себя с музыкой одним целым!", "Создаю атмосферу, где каждая деталь имеет значение. Мои занятия — это разнообразие стилей, где внимание уделяется каждому движению, а дружелюбная атмосфера помогает раскрыться и почувствовать себя уверенно.",
}, },
{ {
name: "Елена Тарасевич", name: "Елена Тарасевич",
role: "Тренер", role: "Body Plastic",
image: "/images/team/elena-tarasevic.webp", image: "/images/team/elena-tarasevic.webp",
instagram: "https://instagram.com/cerceia/", instagram: "https://instagram.com/cerceia/",
description: description:
"Я воздушный гимнаст, практик акройоги и тренер по стретчингу и Airyoga. В спорте и танцах более 15 лет, стаж тренера — около 9 лет. Многократный призёр соревнований по воздушно-спортивному эквилибру России, стран СНГ и международных фестивалей. Прошла обучение у чемпионки мира по воздушной гимнастике, цирковых акробатов, балерин и художественных гимнастов. За плечами более 30 семинаров по функциональной анатомии, биомеханике и йогатерапии.", "Ваш ключ к здоровому, гибкому и гармоничному телу. Знаю каждую связку, каждую клеточку вашего тела. Чувствую ваши ограничения, предугадываю ваши возможности и бережно веду вас к границам вашей гибкости.",
},
{
name: "Ольга Грабовец",
role: "Тренер",
image: "/images/team/olga-grabovets.webp",
instagram: "https://instagram.com/lo_woolf/",
description:
"Я амбассадор красивых линий и натянутых стоп! За 1,5 года выросла от новичка до тренера по Exotic Pole Dance. Многократный призёр чемпионатов. Для меня в танце очень важна музыкальность, и я стараюсь это почерпнуть у разных педагогов — не только наших, но и зарубежных.",
}, },
{ {
name: "Кристина Войтович", name: "Кристина Войтович",
role: "Тренер", role: "Exotic Pole Dance",
image: "/images/team/kristina-voytovich.webp", image: "/images/team/kristina-voytovich.webp",
instagram: "https://instagram.com/chris_voytovich/", instagram: "https://instagram.com/chris_voytovich/",
description: description:
"Я всегда мечтала заниматься Exotic Pole Dance и смогла не только осуществить свою мечту, но и стать тренером! Постоянно совершенствую навыки, посещаю интенсивы и мастер-классы, регулярно участвую в соревнованиях. Мой стиль преподавания объединяет элементы танца, стретчинга, акробатики и силовых упражнений. Стараюсь создать комфортную атмосферу, чтобы каждая ученица могла наслаждаться процессом обучения!", "В моих танцах кипит безумная смесь силы и чувственности. Обожаю переключаться между разными хореографиями: чувственными, дерзкими, меланхоличными, сексуальными... Каждая из них — это взрыв эмоций.",
},
{
name: "Екатерина Матлахова",
role: "Exotic · Pole Dance",
image: "/images/team/ekaterina-matlakhova.webp",
description:
"Создаю чувственные хореографии, где женственность расцветает в сексуальных движениях, изящных линиях и плавных переходах, подкреплённых эстетичными силовыми элементами. В моих танцах рождаются богини!",
},
{
name: "Лилия Огурцова",
role: "Exotic · Pole Dance",
image: "/images/team/liliya-ogurtsova.webp",
description:
"Я проведу вас в мир акцентных и чарующих хореографий. Мои занятия наполнены мистическим вайбом, драйвом и энергией. Уделяю особое внимание развитию силы, прокачке тела и чистоте движений, а также эмоциональной подаче в танце.",
},
{
name: "Наталья Анцух",
role: "Exotic Pole Dance",
image: "/images/team/natalya-antsukh.webp",
description:
"Каждое занятие — это праздник для тела и души, где стиль, грация и внутренняя сила объединяются воедино. Новичок или профессионал — я научу вас танцевать с уверенностью, раскрывать свою женственность и получать удовольствие от каждого движения.",
},
{
name: "Яна Артюкевич",
role: "Pole Dance",
image: "/images/team/yana-artyukevich.webp",
description:
"На моих занятиях вы научитесь красиво и уверенно владеть своим телом, освоите базовые трюки и элементы на пилоне — шаг за шагом, в уютной и вдохновляющей атмосфере. Укрепим мышцы, улучшим растяжку и осанку, а в процессе — почувствуете невероятную уверенность, сексуальность и внутреннюю силу.",
},
{
name: "Анжела Бобко",
role: "Pole Dance",
image: "/images/team/anzhela-bobko.webp",
description:
"Мой индивидуальный подход и внимательное отношение к каждому ученику создают атмосферу доверия и поддержки. Со мной вы не просто осваиваете технику — вы преодолеваете себя и становитесь лучшей версией себя.",
}, },
], ],
}, },
classes: { classes: {
title: "Скорее, мы ждём!", title: "Направления",
items: [ items: [
{ {
name: "Exotic Pole Dance", name: "Exotic Pole Dance",
@@ -137,34 +157,34 @@ export const siteContent: SiteContent = {
"Чувственная хореография с элементами pole dance в каблуках.", "Чувственная хореография с элементами pole dance в каблуках.",
icon: "sparkles", icon: "sparkles",
detailedDescription: detailedDescription:
"Чувственный, эстетичный, сексуальный вид танца. Он богат на плавные линии, манящие прогибы и развитие вашей женственности.\n\nВы получаете:\n— уверенность в себе,\n— красивую фигуру и развитие всех групп мышц,\n— раскрытие себя с новой стороны и возможность влюбиться заново,\n— вы учитесь наслаждаться собой.", "Стиль танца на пилоне, где акцент делается на чувственность, пластику. В Exotic Pole Dance используется обувь на высоких каблуках (стрипы), развивающий гибкость, силу, женственность и уверенность.\n\nВы получаете:\n— уверенность в себе,\n— красивую фигуру и развитие всех групп мышц,\n— раскрытие себя с новой стороны,\n— вы учитесь наслаждаться собой.",
images: ["/images/classes/exot.webp", "/images/classes/exot-w.webp"], images: ["/images/classes/exot.webp", "/images/classes/exot-w.webp"],
}, },
{ {
name: "Pole Dance", name: "Pole Dance",
description: description:
"Сила, грация и пластика на пилоне. Для любого уровня подготовки.", "Искусство на пилоне: акробатические трюки, силовые элементы и грация.",
icon: "flame", icon: "flame",
detailedDescription: detailedDescription:
"Пилон — это отличный тренажер для рук, ног, спины и пресса. Pole Dance учит красиво двигаться, улучшает растяжку, силовые показатели и выдержку.\n\nВы получите:\n— силу и грацию,\n— прекрасную растяжку,\n— правильную осанку,\n— прекрасное настроение.", "Вид искусства на пилоне, включающий акробатические трюки, силовые элементы и грациозные движения. Подходит для развития силы, выносливости и уровня технического мастерства.\n\nВы получите:\n— силу и грацию,\n— прекрасную растяжку,\n— правильную осанку,\n— прекрасное настроение.",
images: ["/images/classes/pole-dance.webp"], images: ["/images/classes/pole-dance.webp"],
}, },
{ {
name: "Body Plastic", name: "Body Plastic",
description: description:
"Танцевальное направление, раскрывающее женственность и пластику тела.", "Пластичность, гибкость и осознанность тела в каждом движении.",
icon: "wind", icon: "wind",
detailedDescription: detailedDescription:
"Растяжка — это искусство, которое позволяет вам не только улучшить гибкость, но и раскрыть истинную красоту вашего тела. Это больше, чем просто упражнения — это плавные движения, которые учат вас слушать своё тело и чувствовать его.\n\nЗанимаясь растяжкой, вы получите:\n— уверенность в себе,\n— красивую осанку и гибкость,\n— улучшение общего тонуса тела и расслабление мышц,\n— возможность открыть новые грани своей чувственности и женственности,\n— умение наслаждаться каждым движением и моментом.\n\nРастяжка помогает вам не только достигнуть физического совершенства, но и найти внутреннюю гармонию и любовь к себе.", "Тренировка, направленная на пластичность, гибкость и осознанность всего тела, помогает лучше управлять своим движением. Body Plastic объединяет растяжку, силу, контроль и пластичность, что помогает развивать тело гармонично и быстро.\n\nВместо односторонней растяжки он учит не только растягиваться, но и сохранять баланс, управлять каждым движением, что особенно важно для pole dance, акробатики и других тренировок.",
images: ["/images/classes/body-plastic.webp"], images: ["/images/classes/body-plastic.webp"],
}, },
{ {
name: "Партерная акробатика", name: "Трюковые комбинации с пилоном",
description: description:
"Акробатические элементы в партере для развития силы и гибкости.", "Яркие трюки, акробатические элементы и впечатляющие комбинации.",
icon: "zap", icon: "zap",
detailedDescription: detailedDescription:
"Партерная акробатика — это завораживающее сочетание силы, гибкости и грации, которое раскрывает безграничные возможности вашего тела. Этот вид искусства позволяет вам воплотить в жизнь самые смелые акробатические элементы, создавая уникальные и впечатляющие комбинации на полу.\n\nЗанимаясь партерной акробатикой, вы получите:\n— невероятную физическую силу и выносливость,\n— улучшение координации и равновесия,\n— развитие всех групп мышц и повышение гибкости,\n— возможность выразить себя через мощные и динамичные движения,\n— уверенность в своих возможностях и преодоление собственных границ.\n\nПартерная акробатика — это путь к совершенству тела и духа, который дарит ощущение полёта и свободы на земле.", "Направление с акцентом на выполнение трюков, акробатических элементов и их комбинаций. Идеально подходит для тех, кто хочет освоить яркие, эффектные трюки и создать впечатляющие комбинации для выступлений и личного развития.",
images: ["/images/classes/parter-1.webp", "/images/classes/parter-2.webp"], images: ["/images/classes/parter-1.webp", "/images/classes/parter-2.webp"],
}, },
{ {
@@ -173,7 +193,7 @@ export const siteContent: SiteContent = {
"Уникальные занятия с приглашёнными топовыми тренерами.", "Уникальные занятия с приглашёнными топовыми тренерами.",
icon: "star", icon: "star",
detailedDescription: detailedDescription:
"Мастер-классы — это уникальная возможность погрузиться в чувственный мир танца, где каждое движение наполнено грацией и страстью. Наши мастер-классы созданы для тех, кто хочет открыть в себе новые грани женственности и научиться выражать свои эмоции через танец.\n\nПриходя на наши мастер-классы, вы получите:\n— уверенность в себе и своих возможностях,\n— возможность раскрыть свою чувственность и сексуальность,\n— умение наслаждаться каждым моментом и каждым движением,\n— опыт от профессиональных тренеров, которые помогут вам достичь новых высот.\n\nНаши мастер-классы — это не просто тренировки, это путь к самопознанию и любви к своему телу. Присоединяйтесь к нам и откройте для себя мир танца, где каждый шаг приносит удовольствие и уверенность.", "Мастер-классы — это уникальная возможность погрузиться в чувственный мир танца, где каждое движение наполнено грацией и страстью. Наши мастер-классы созданы для тех, кто хочет открыть в себе новые грани женственности и научиться выражать свои эмоции через танец.\n\nПриходя на наши мастер-классы, вы получите:\n— уверенность в себе и своих возможностях,\n— возможность раскрыть свою чувственность и сексуальность,\n— умение наслаждаться каждым моментом и каждым движением,\n— опыт от профессиональных тренеров.",
images: ["/images/classes/master-class-1.webp", "/images/classes/master-class-2.webp", "/images/classes/master-class-3.webp"], images: ["/images/classes/master-class-1.webp", "/images/classes/master-class-2.webp", "/images/classes/master-class-3.webp"],
}, },
{ {
@@ -181,16 +201,117 @@ export const siteContent: SiteContent = {
description: "Тренировки в удобное время из любой точки мира.", description: "Тренировки в удобное время из любой точки мира.",
icon: "monitor", icon: "monitor",
detailedDescription: detailedDescription:
"Если вы находитесь не в Минске, у вас всё равно есть уникальная возможность тренироваться, расти и развиваться с нами! Мы предлагаем занятия онлайн по следующим направлениям: партерная акробатика, Pole Dance, Exotic Pole Dance, Exo-tricks, полёты.\n\nМы предлагаем два способа работы: самостоятельный и VIP. В самостоятельный тариф входит доступ к видеозаписям уроков по выбранному направлению, в VIP-тарифе вы также получите доступ к чату с куратором в Telegram, который подскажет и скорректирует в случае трудностей в процессе изучения материала.", "Если вы находитесь не в Минске, у вас всё равно есть уникальная возможность тренироваться, расти и развиваться с нами! Мы предлагаем занятия онлайн по следующим направлениям: партерная акробатика, Pole Dance, Exotic Pole Dance, Exo-tricks, полёты.\n\nМы предлагаем два способа работы: самостоятельный и VIP. В самостоятельный тариф входит доступ к видеозаписям уроков по выбранному направлению, в VIP-тарифе вы также получите доступ к чату с куратором в Telegram.",
images: ["/images/classes/online-classes.webp"], images: ["/images/classes/online-classes.webp"],
}, },
], ],
}, },
faq: {
title: "Частые вопросы",
items: [
{
question: "Что такое Exotic Pole Dance, Pole Dance и Body Plastic?",
answer:
"Exotic Pole Dance — стиль танца на пилоне, где акцент делается на чувственность, пластику. Используется обувь на высоких каблуках (стрипы), развивающий гибкость, силу, женственность и уверенность.\n\nPole Dance — вид искусства на пилоне, включающий акробатические трюки, силовые элементы и грациозные движения. Подходит для развития силы, выносливости и технического мастерства.\n\nBody Plastic — тренировка, направленная на пластичность, гибкость и осознанность всего тела, помогает лучше управлять своим движением.",
},
{
question: "Нужно ли иметь специальную подготовку, чтобы начать заниматься?",
answer:
"Нет, специальная подготовка не требуется. Уровень физической подготовки будет расти постепенно в процессе тренировок. Важно иметь желание и готовность к обучению.",
},
{
question: "Какая одежда нужна для занятий?",
answer:
"Pole Dance: важны шорты и топ, чтобы кожа на бёдрах и животе соприкасалась с пилоном для сцепления.\n\nExotic Pole Dance: на начальных этапах лучше шорты, можно леггинсы, топ/лиф, наколенники и желательно стрипы. На начальном этапе можно начинать без стрипов в носочках.",
},
{
question: "Какие группы по уровню существуют в вашей студии?",
answer:
"У нас есть группы для начинающих — «С нуля», где вы можете освоить базовые движения и технику. Также есть группы для продолжающих и для любого уровня подготовки — чтобы все могли развиваться и совершенствоваться в приятной и поддерживающей атмосфере.",
},
{
question: "Можно ли начать заниматься Exotic Pole Dance в любом возрасте?",
answer:
"Да, конечно! Возраст не имеет значения — этот вид спорта подходит для всех желающих развивать силу, гибкость и уверенность в себе. Единственное ограничение — от 18 лет.",
},
{
question: "Я чувствую себя скованно. Как раскрепоститься на тренировках Exotic Pole Dance?",
answer:
"Exotic Pole Dance — это про самовыражение и принятие себя. Не бойтесь проявлять свои эмоции, экспериментировать с движениями. Постепенно вы почувствуете себя увереннее и свободнее. Наши тренеры создают на занятиях комфортную и поддерживающую атмосферу.",
},
{
question: "Как быстро я смогу делать трюки на пилоне?",
answer:
"Это индивидуально и зависит от вашей физической подготовки, регулярности тренировок и способностей к обучению. Первые простые трюки обычно осваиваются в течение нескольких недель.",
},
{
question: "Body Plastic — это растяжка?",
answer:
"Body Plastic — это не только про растяжку. Body Plastic объединяет растяжку, силу, контроль и пластичность, что помогает развивать тело гармонично и быстро. Вместо односторонней растяжки он учит не только растягиваться, но и сохранять баланс, управлять каждым движением, что особенно важно для pole dance, акробатики и других тренировок.",
},
{
question: "Что включает направление «Трюковые комбинации с пилоном»?",
answer:
"Трюковые комбинации с пилоном — это направление с акцентом на выполнение трюков, акробатических элементов и их комбинаций. Это направление идеально подходит для тех, кто хочет освоить яркие, эффектные трюки и создать впечатляющие комбинации для выступлений и личного развития.",
},
{
question: "Сколько раз в неделю нужно заниматься?",
answer:
"Для новичков рекомендуется начинать с 23 раз в неделю. По мере развития физической формы и навыков можно увеличивать количество тренировок.",
},
{
question: "Участие в чемпионатах: обязательно ли это?",
answer:
"Нет, участие в чемпионатах — это не обязательно. Это скорее вопрос вашего личного желания и готовности. Если вы чувствуете в себе силы, мотивацию и хотите попробовать что-то новое, то не стесняйтесь сообщить об этом своему тренеру! Он поможет оценить ваши возможности и подготовиться к чемпионату наилучшим образом.",
},
],
},
pricing: {
title: "Стоимость",
subtitle: "Все абонементы идут с привязкой к группе, кроме безлимитного",
items: [
{ name: "Абонемент 8 × 90 мин", price: "175 BYN" },
{ name: "Абонемент 4 × 90 мин", price: "105 BYN" },
{ name: "Абонемент 8 × 60 мин", price: "145 BYN" },
{ name: "Абонемент 4 × 60 мин", price: "105 BYN" },
{ name: "Разовое занятие 1,5 часа", price: "30 BYN" },
{ name: "Разовое занятие 1 час", price: "25 BYN" },
{ name: "Пробное занятие", price: "25 BYN", note: "1,5 часа или 1 час" },
{
name: "Безлимитный абонемент",
price: "240 / 410 BYN",
note: "2 недели / месяц (обязательна предварительная запись)",
},
],
rentalTitle: "Аренда зала",
rentalItems: [
{ name: "С абонементом", price: "20 BYN", note: "+5 BYN за каждого доп. человека" },
{
name: "Без абонемента (Машерова 17/4, 6 этаж + Притыцкого 62/М)",
price: "35 BYN",
note: "+5 BYN за каждого доп. человека",
},
{
name: "Без абонемента (Машерова 17/4, 2 этаж)",
price: "25 BYN",
note: "+5 BYN за каждого доп. человека",
},
],
rules: [
"Абонемент является персональным и не подлежит передаче другим лицам.",
"Абонемент необходимо предъявлять администратору перед каждым занятием.",
"Оплата абонементов и разовых посещений производится до начала занятия.",
"Компенсация за пропущенные занятия не предусмотрена.",
"Срок действия абонемента — 4 недели.",
"Абонемент можно заморозить не более двух раз в год на срок до 2 недель (на время отпуска или командировки).",
"В случае болезни, подтверждённой больничным листом, возможно продление срока действия абонемента.",
],
},
contact: { contact: {
title: "Контакты", title: "Контакты",
addresses: [ addresses: [
"г. Минск, Матерова, 17к4", "г. Минск, Машерова, 17/4",
"г. Минск, Притыцкого, 62к1", "г. Минск, Притыцкого, 62/М",
], ],
phone: "+375 29 389-70-01", phone: "+375 29 389-70-01",
instagram: "https://instagram.com/blackheartdancehouse/", instagram: "https://instagram.com/blackheartdancehouse/",

View File

@@ -0,0 +1,45 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
interface UseShowcaseRotationOptions {
totalItems: number;
autoPlayInterval?: number;
pauseDuration?: number;
}
export function useShowcaseRotation({
totalItems,
autoPlayInterval = 4000,
pauseDuration = 10000,
}: UseShowcaseRotationOptions) {
const [activeIndex, setActiveIndex] = useState(0);
const pausedUntil = useRef(0);
const hoveringRef = useRef(false);
const select = useCallback(
(index: number) => {
setActiveIndex(index);
pausedUntil.current = Date.now() + pauseDuration;
},
[pauseDuration],
);
const setHovering = useCallback((hovering: boolean) => {
hoveringRef.current = hovering;
}, []);
useEffect(() => {
if (totalItems <= 1) return;
const id = setInterval(() => {
if (hoveringRef.current) return;
if (Date.now() < pausedUntil.current) return;
setActiveIndex((prev) => (prev + 1) % totalItems);
}, autoPlayInterval);
return () => clearInterval(id);
}, [totalItems, autoPlayInterval]);
return { activeIndex, select, setHovering };
}

View File

@@ -11,6 +11,8 @@ export const NAV_LINKS: NavLink[] = [
{ label: "О нас", href: "#about" }, { label: "О нас", href: "#about" },
{ label: "Команда", href: "#team" }, { label: "Команда", href: "#team" },
{ label: "Направления", href: "#classes" }, { label: "Направления", href: "#classes" },
{ label: "Стоимость", href: "#pricing" },
{ label: "FAQ", href: "#faq" },
{ label: "Контакты", href: "#contact" }, { label: "Контакты", href: "#contact" },
]; ];

View File

@@ -14,6 +14,17 @@ export interface TeamMember {
description?: string; description?: string;
} }
export interface FAQItem {
question: string;
answer: string;
}
export interface PricingItem {
name: string;
price: string;
note?: string;
}
export interface ContactInfo { export interface ContactInfo {
title: string; title: string;
addresses: string[]; addresses: string[];
@@ -46,5 +57,17 @@ export interface SiteContent {
title: string; title: string;
items: ClassItem[]; items: ClassItem[];
}; };
faq: {
title: string;
items: FAQItem[];
};
pricing: {
title: string;
subtitle: string;
items: PricingItem[];
rentalTitle: string;
rentalItems: PricingItem[];
rules: string[];
};
contact: ContactInfo; contact: ContactInfo;
} }

View File

@@ -1,2 +1,2 @@
export type { NavLink } from "./navigation"; export type { NavLink } from "./navigation";
export type { ClassItem, TeamMember, ContactInfo, SiteContent } from "./content"; export type { ClassItem, TeamMember, FAQItem, PricingItem, ContactInfo, SiteContent } from "./content";