refactor: centralize gold tokens, extract sub-components, clean up unused code

- Replace hardcoded hex colors with gold/gold-light/gold-dark Tailwind tokens
- Extract Schedule into DayCard, ScheduleFilters, MobileSchedule sub-components
- Extract Team into TeamCarousel, TeamMemberInfo sub-components
- Add UI_CONFIG for centralized magic numbers (timings, thresholds)
- Add reusable IconBadge component, simplify Contact section
- Convert Pricing clickable divs to semantic buttons for a11y
- Remove unused SocialLinks, btn-outline, btn-ghost, nav-link CSS classes
- Fix React setState-during-render error in TeamCarousel (deferred update pattern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 14:57:39 +03:00
parent 08e4af1d55
commit d5afaf92ba
31 changed files with 784 additions and 691 deletions

View File

@@ -2,13 +2,14 @@
import { useState, useEffect } from "react";
import { ChevronUp } from "lucide-react";
import { UI_CONFIG } from "@/lib/config";
export function BackToTop() {
const [visible, setVisible] = useState(false);
useEffect(() => {
function handleScroll() {
setVisible(window.scrollY > 600);
setVisible(window.scrollY > UI_CONFIG.scrollThresholds.backToTop);
}
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
@@ -18,7 +19,7 @@ export function BackToTop() {
<button
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
aria-label="Наверх"
className={`fixed bottom-6 right-6 z-40 flex h-10 w-10 items-center justify-center rounded-full border border-[#c9a96e]/30 bg-black/60 text-[#d4b87a] backdrop-blur-sm transition-all duration-300 hover:bg-[#c9a96e]/20 hover:border-[#c9a96e]/50 ${
className={`fixed bottom-6 right-6 z-40 flex h-10 w-10 items-center justify-center rounded-full border border-gold/30 bg-black/60 text-gold-light backdrop-blur-sm transition-all duration-300 hover:bg-gold/20 hover:border-gold/50 ${
visible ? "translate-y-0 opacity-100" : "translate-y-4 opacity-0 pointer-events-none"
}`}
>

View File

@@ -118,7 +118,7 @@ export function BookingModal({ open, onClose }: BookingModalProps) {
</p>
<button
onClick={handleClose}
className="mt-6 rounded-full bg-[#c9a96e] px-6 py-2.5 text-sm font-semibold text-black transition-all hover:bg-[#d4b87a] cursor-pointer"
className="mt-6 rounded-full bg-gold px-6 py-2.5 text-sm font-semibold text-black transition-all hover:bg-gold-light cursor-pointer"
>
Закрыть
</button>
@@ -142,7 +142,7 @@ export function BookingModal({ open, onClose }: BookingModalProps) {
onChange={(e) => setName(e.target.value)}
placeholder="Ваше имя"
required
className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-[#c9a96e]/40 focus:bg-white/[0.06]"
className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]"
/>
</div>
<div>
@@ -152,13 +152,13 @@ export function BookingModal({ open, onClose }: BookingModalProps) {
onChange={(e) => handlePhoneChange(e.target.value)}
placeholder="+375 (__) ___-__-__"
required
className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-[#c9a96e]/40 focus:bg-white/[0.06]"
className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]"
/>
</div>
<button
type="submit"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-[#c9a96e] py-3 text-sm font-semibold text-black transition-all hover:bg-[#d4b87a] hover:shadow-lg hover:shadow-[#c9a96e]/20 cursor-pointer"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-gold py-3 text-sm font-semibold text-black transition-all hover:bg-gold-light hover:shadow-lg hover:shadow-gold/20 cursor-pointer"
>
<Send size={15} />
Отправить в Instagram
@@ -178,14 +178,14 @@ export function BookingModal({ open, onClose }: BookingModalProps) {
href={contact.instagram}
target="_blank"
rel="noopener noreferrer"
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.03] py-3 text-sm font-medium text-neutral-300 transition-all hover:border-[#c9a96e]/30 hover:text-[#d4b87a] cursor-pointer"
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.03] py-3 text-sm font-medium text-neutral-300 transition-all hover:border-gold/30 hover:text-gold-light cursor-pointer"
>
<Instagram size={16} />
Instagram
</a>
<a
href={`tel:${contact.phone.replace(/\s/g, "")}`}
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.03] py-3 text-sm font-medium text-neutral-300 transition-all hover:border-[#c9a96e]/30 hover:text-[#d4b87a] cursor-pointer"
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.03] py-3 text-sm font-medium text-neutral-300 transition-all hover:border-gold/30 hover:text-gold-light cursor-pointer"
>
<Phone size={16} />
Позвонить

View File

@@ -2,7 +2,6 @@ import Link from "next/link";
interface ButtonProps {
href?: string;
variant?: "primary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
className?: string;
@@ -17,13 +16,12 @@ const sizes = {
export function Button({
href,
variant = "primary",
size = "md",
children,
className = "",
onClick,
}: ButtonProps) {
const classes = `btn-${variant} ${sizes[size]} ${className}`;
const classes = `btn-primary ${sizes[size]} ${className}`;
if (href) {
return (

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { UI_CONFIG } from "@/lib/config";
interface Heart {
id: number;
@@ -15,7 +16,7 @@ export function FloatingHearts() {
const [hearts, setHearts] = useState<Heart[]>([]);
useEffect(() => {
const generated: Heart[] = Array.from({ length: 12 }, (_, i) => ({
const generated: Heart[] = Array.from({ length: UI_CONFIG.team.floatingHeartsCount }, (_, i) => ({
id: i,
left: Math.random() * 100,
size: 8 + Math.random() * 16,
@@ -33,7 +34,7 @@ export function FloatingHearts() {
{hearts.map((heart) => (
<div
key={heart.id}
className="absolute text-[#c9a96e]"
className="absolute text-gold"
style={{
left: `${heart.left}%`,
bottom: "-20px",

View File

@@ -0,0 +1,14 @@
interface IconBadgeProps {
children: React.ReactNode;
className?: string;
}
export function IconBadge({ children, className = "" }: IconBadgeProps) {
return (
<div
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gold/10 text-gold-dark transition-colors group-hover:bg-gold/15 dark:text-gold-light ${className}`}
>
{children}
</div>
);
}

View File

@@ -13,7 +13,7 @@ export function SectionHeading({ children, className = "", centered = false }: S
{children}
</h2>
<span
className={`mt-4 block h-[1px] w-20 bg-gradient-to-r from-[#c9a96e] to-transparent ${
className={`mt-4 block h-[1px] w-20 bg-gradient-to-r from-gold to-transparent ${
centered ? "mx-auto" : ""
}`}
/>

View File

@@ -1,6 +1,7 @@
"use client";
import { useRef, useEffect, useState, useCallback } from "react";
import { UI_CONFIG } from "@/lib/config";
interface ShowcaseLayoutProps<T> {
items: T[];
@@ -34,7 +35,7 @@ export function ShowcaseLayout<T>({
const timeout = setTimeout(() => {
setDisplayIndex(activeIndex);
setFading(false);
}, 250);
}, UI_CONFIG.showcase.fadeMs);
return () => clearTimeout(timeout);
}, [activeIndex, displayIndex]);
@@ -85,8 +86,7 @@ export function ShowcaseLayout<T>({
const dy = e.changedTouches[0].clientY - touchStart.current.y;
touchStart.current = null;
// Only trigger if horizontal swipe is dominant and > 50px
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5) {
if (Math.abs(dx) > UI_CONFIG.showcase.swipeThreshold && Math.abs(dx) > Math.abs(dy) * 1.5) {
if (dx < 0 && activeIndex < items.length - 1) {
onSelect(activeIndex + 1);
} else if (dx > 0 && activeIndex > 0) {
@@ -131,7 +131,7 @@ export function ShowcaseLayout<T>({
{/* Counter */}
{counter && (
<div className="mt-3 flex items-center justify-center gap-2 lg:justify-start">
<span className="text-xs font-medium tabular-nums text-[#c9a96e]">
<span className="text-xs font-medium tabular-nums text-gold">
{String(activeIndex + 1).padStart(2, "0")}
</span>
<span className="h-[1px] w-8 bg-white/10" />
@@ -155,7 +155,7 @@ export function ShowcaseLayout<T>({
onClick={() => onSelect(i)}
className={`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-gold/60 bg-gold/10 dark:bg-gold/5"
: "border-transparent bg-neutral-100 hover:bg-neutral-200 dark:bg-white/[0.03] dark:hover:bg-white/[0.06]"
}`}
>

View File

@@ -1,31 +0,0 @@
import { Instagram } from "lucide-react";
interface SocialLinksProps {
instagram?: string;
instagramHandle?: string;
className?: string;
iconSize?: number;
}
export function SocialLinks({
instagram,
instagramHandle,
className = "",
iconSize = 24,
}: SocialLinksProps) {
return (
<div className={`flex items-center gap-4 ${className}`}>
{instagram && (
<a
href={instagram}
target="_blank"
rel="noopener noreferrer"
className="social-icon flex items-center gap-2"
>
<Instagram size={iconSize} />
{instagramHandle && <span className="text-sm font-medium">{instagramHandle}</span>}
</a>
)}
</div>
);
}