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:
@@ -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"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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} />
|
||||
Позвонить
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
14
src/components/ui/IconBadge.tsx
Normal file
14
src/components/ui/IconBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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" : ""
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -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]"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user