feat: comprehensive light theme support across entire site

- CSS foundation: theme-aware scrollbars, section glows, glass cards with
  gold shadows, stronger animated borders and glow effects for light mode
- Hero: consistent dark-video treatment for both themes, brighter gold
  gradient text, glowing CTA button
- Gradient text: auto-switch to warm gold tones on light backgrounds via
  html:not(.dark) selector
- Team profile: inverted ambient photo bg with white overlay for light,
  dark text/borders, gold-dark labels for contrast
- All sections: text-neutral-500→600 upgrades for WCAG AA contrast,
  gold shadow accents on cards (About, Pricing, FAQ, DayCard, News)
- Admin: replaced hardcoded #c9a96e with theme tokens, fixed select
  options, array editor borders, booking badges contrast
- Header: white text on transparent hero, dark text after scroll
- UI components: BackToTop, FloatingHearts, ShowcaseLayout tabs,
  SignupModal, NewsModal, GroupCard adapted for light backgrounds
- Updated CLAUDE.md to reflect dual theme support
This commit is contained in:
2026-04-10 21:30:56 +03:00
parent a587736dd3
commit 0e626451e7
36 changed files with 380 additions and 285 deletions
+15 -17
View File
@@ -190,15 +190,15 @@ export function ArrayEditor<T>({
<div
key={getStableKey(i)}
ref={(el) => { itemRefs.current[i] = el; }}
className={`rounded-lg border bg-neutral-900/50 mb-3 hover:border-white/25 hover:bg-neutral-800/50 focus-within:border-gold/50 focus-within:bg-neutral-800 transition-all ${
newItemIndex === i || droppedIndex === i ? "border-gold/40 ring-1 ring-gold/20" : "border-white/10"
className={`rounded-lg border bg-neutral-100/80 mb-3 hover:border-neutral-300 dark:hover:border-white/25 hover:bg-neutral-200/50 focus-within:border-gold/50 focus-within:bg-neutral-200 transition-all dark:bg-neutral-900/50 dark:hover:bg-neutral-800/50 dark:focus-within:bg-neutral-800 ${
newItemIndex === i || droppedIndex === i ? "border-gold/40 ring-1 ring-gold/20" : "border-neutral-200 dark:border-white/10"
} ${isHidden ? "hidden" : ""}`}
>
{inline ? (
/* Inline: grip + content + delete on one row */
<div className="flex items-start gap-1.5 p-1.5">
<div
className="cursor-grab active:cursor-grabbing rounded p-1 mt-1.5 text-neutral-500 hover:text-white transition-colors select-none shrink-0"
className="cursor-grab active:cursor-grabbing rounded p-1 mt-1.5 text-neutral-500 hover:text-neutral-900 transition-colors select-none shrink-0 dark:hover:text-white"
onMouseDown={(e) => handleGripMouseDown(e, i)}
aria-label="Перетащить для сортировки"
role="button"
@@ -222,7 +222,7 @@ export function ArrayEditor<T>({
<div className={`flex items-center justify-between gap-2 p-4 ${isCollapsed ? "" : "pb-0 mb-3"}`}>
<div className="flex items-center gap-2 flex-1 min-w-0">
<div
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-neutral-900 transition-colors select-none dark:hover:text-white"
onMouseDown={(e) => handleGripMouseDown(e, i)}
aria-label="Перетащить для сортировки"
role="button"
@@ -236,7 +236,7 @@ export function ArrayEditor<T>({
aria-expanded={!isCollapsed}
className="flex items-center gap-2 flex-1 min-w-0 text-left cursor-pointer group"
>
<span className="text-sm font-medium text-neutral-300 truncate group-hover:text-white transition-colors">{title}</span>
<span className="text-sm font-medium text-neutral-700 truncate group-hover:text-neutral-900 transition-colors dark:text-neutral-300 dark:group-hover:text-white">{title}</span>
{getItemBadge?.(item, i)}
<ChevronDown size={14} className={`text-neutral-500 transition-transform duration-200 shrink-0 ${isCollapsed ? "" : "rotate-180"}`} />
</button>
@@ -304,13 +304,11 @@ export function ArrayEditor<T>({
<div
key={getStableKey(i)}
ref={(el) => { itemRefs.current[i] = el; }}
className={`rounded-lg border bg-neutral-900/50 mb-3 transition-colors ${
"border-white/10"
}`}
className="rounded-lg border border-neutral-200 bg-neutral-100/80 mb-3 transition-colors dark:border-white/10 dark:bg-neutral-900/50"
>
{inline ? (
<div className="flex items-start gap-1.5 p-1.5">
<div className="cursor-grab active:cursor-grabbing rounded p-1 mt-1.5 text-neutral-500 hover:text-white transition-colors select-none shrink-0"
<div className="cursor-grab active:cursor-grabbing rounded p-1 mt-1.5 text-neutral-500 hover:text-neutral-900 transition-colors select-none shrink-0 dark:hover:text-white"
onMouseDown={(e) => handleGripMouseDown(e, i)} aria-label="Перетащить для сортировки" role="button">
<GripVertical size={14} />
</div>
@@ -327,7 +325,7 @@ export function ArrayEditor<T>({
<div className={`flex items-center justify-between gap-2 p-4 ${isCollapsed ? "" : "pb-0 mb-3"}`}>
<div className="flex items-center gap-2 flex-1 min-w-0">
<div
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-neutral-900 transition-colors select-none dark:hover:text-white"
onMouseDown={(e) => handleGripMouseDown(e, i)}
aria-label="Перетащить для сортировки"
role="button"
@@ -336,7 +334,7 @@ export function ArrayEditor<T>({
</div>
{collapsible && (
<button type="button" onClick={() => toggleCollapse(i)} className="flex items-center gap-2 flex-1 min-w-0 text-left cursor-pointer group">
<span className="text-sm font-medium text-neutral-300 truncate group-hover:text-white transition-colors">{title}</span>
<span className="text-sm font-medium text-neutral-700 truncate group-hover:text-neutral-900 transition-colors dark:text-neutral-300 dark:group-hover:text-white">{title}</span>
{getItemBadge?.(item, i)}
<ChevronDown size={14} className={`text-neutral-500 transition-transform duration-200 shrink-0 ${isCollapsed ? "" : "rotate-180"}`} />
</button>
@@ -384,14 +382,14 @@ export function ArrayEditor<T>({
<div>
{(label || (collapsible && items.length > 1)) && (
<div className="flex items-center justify-between mb-3">
{label ? <h3 className="text-sm font-medium text-neutral-300">{label}</h3> : <div />}
{label ? <h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{label}</h3> : <div />}
{collapsible && items.length > 1 && (() => {
const allCollapsed = collapsed.size >= items.length;
return (
<button
type="button"
onClick={() => allCollapsed ? setCollapsed(new Set()) : setCollapsed(new Set(items.map((_, i) => i)))}
className="rounded p-1 text-neutral-500 hover:text-white transition-colors"
className="rounded p-1 text-neutral-500 hover:text-neutral-900 transition-colors dark:hover:text-white"
title={allCollapsed ? "Развернуть все" : "Свернуть все"}
aria-label={allCollapsed ? "Развернуть все" : "Свернуть все"}
>
@@ -416,7 +414,7 @@ export function ArrayEditor<T>({
return next;
});
}}
className="mb-3 flex items-center gap-2 rounded-lg border border-dashed border-white/20 px-4 py-2.5 text-sm text-neutral-400 hover:text-white hover:border-white/40 transition-colors"
className="mb-3 flex items-center gap-2 rounded-lg border border-dashed border-neutral-300 px-4 py-2.5 text-sm text-neutral-500 hover:text-neutral-900 hover:border-neutral-400 transition-colors dark:border-white/20 dark:text-neutral-400 dark:hover:text-white dark:hover:border-white/40"
>
<Plus size={16} />
{addLabel}
@@ -436,7 +434,7 @@ export function ArrayEditor<T>({
setNewItemIndex(items.length);
setCollapsed(prev => { const next = new Set(prev); next.delete(items.length); return next; });
}}
className="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-white/20 px-4 py-2.5 text-sm text-neutral-400 hover:text-white hover:border-white/40 transition-colors"
className="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-neutral-300 px-4 py-2.5 text-sm text-neutral-500 hover:text-neutral-900 hover:border-neutral-400 transition-colors dark:border-white/20 dark:text-neutral-400 dark:hover:text-white dark:hover:border-white/40"
>
<Plus size={16} />
{addLabel}
@@ -455,9 +453,9 @@ export function ArrayEditor<T>({
height: dragSize.h,
}}
>
<div className="h-full rounded-lg border-2 border-gold/60 bg-neutral-900/95 shadow-2xl shadow-gold/20 flex items-center gap-3 px-4">
<div className="h-full rounded-lg border-2 border-gold/60 bg-white/95 shadow-2xl shadow-gold/20 flex items-center gap-3 px-4 dark:bg-neutral-900/95">
<GripVertical size={16} className="text-gold shrink-0" />
<span className="text-sm text-neutral-300">{collapsible && dragIndex !== null ? (getItemTitle?.(items[dragIndex], dragIndex) || "Перемещение...") : "Перемещение элемента..."}</span>
<span className="text-sm text-neutral-700 dark:text-neutral-300">{collapsible && dragIndex !== null ? (getItemTitle?.(items[dragIndex], dragIndex) || "Перемещение...") : "Перемещение элемента..."}</span>
</div>
</div>,
document.body
@@ -29,15 +29,15 @@ export function CollapsibleSection({
const toggle = onToggle ?? (() => setInternalOpen((v) => !v));
return (
<div className="rounded-xl border border-white/10 bg-neutral-900/30 overflow-hidden">
<div className="rounded-xl border border-neutral-200 bg-neutral-100/50 overflow-hidden dark:border-white/10 dark:bg-neutral-800/50">
<button
type="button"
onClick={toggle}
aria-expanded={open}
className="flex items-center justify-between w-full px-5 py-3.5 text-left cursor-pointer group hover:bg-white/[0.02] transition-colors"
className="flex items-center justify-between w-full px-5 py-3.5 text-left cursor-pointer group hover:bg-neutral-100 transition-colors dark:hover:bg-white/[0.02]"
>
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-neutral-200 group-hover:text-white transition-colors">
<h3 className="text-sm font-semibold text-neutral-700 group-hover:text-neutral-900 transition-colors dark:text-neutral-200 dark:group-hover:text-white">
{title}
</h3>
{count !== undefined && (
@@ -144,27 +144,27 @@ export function GenericBookingsList<T extends BaseBooking>({
const groupCounts = { new: 0, contacted: 0, confirmed: 0, declined: 0 };
for (const item of group.items) groupCounts[item.status] = (groupCounts[item.status] || 0) + 1;
return (
<div key={group.key} className={`rounded-xl border overflow-hidden ${group.isArchived ? "border-white/5 opacity-60" : "border-white/10"}`}>
<div key={group.key} className={`rounded-xl border overflow-hidden ${group.isArchived ? "border-neutral-100 opacity-60 dark:border-white/5" : "border-neutral-200 dark:border-white/10"}`}>
<button
onClick={() => setExpanded((p) => ({ ...p, [group.key]: !isOpen }))}
className={`w-full flex items-center gap-3 px-4 py-3 transition-colors text-left ${group.isArchived ? "bg-neutral-900/50 hover:bg-neutral-800/50" : "bg-neutral-900 hover:bg-neutral-800/80"}`}
className={`w-full flex items-center gap-3 px-4 py-3 transition-colors text-left ${group.isArchived ? "bg-neutral-100/50 hover:bg-neutral-200/50 dark:bg-neutral-900/50 dark:hover:bg-neutral-800/50" : "bg-neutral-50 hover:bg-neutral-200/80 dark:bg-neutral-900 dark:hover:bg-neutral-800/80"}`}
>
{isOpen ? <ChevronDown size={14} className="text-neutral-500 shrink-0" /> : <ChevronRight size={14} className="text-neutral-500 shrink-0" />}
{group.sublabel && (
<span className={`text-xs font-medium shrink-0 ${group.isArchived ? "text-neutral-500" : "text-gold"}`}>{group.sublabel}</span>
)}
<span className={`font-medium text-sm truncate ${group.isArchived ? "text-neutral-400" : "text-white"}`}>{group.label}</span>
<span className={`font-medium text-sm truncate ${group.isArchived ? "text-neutral-500 dark:text-neutral-400" : "text-neutral-900 dark:text-white"}`}>{group.label}</span>
{group.dateBadge && (
<span className={`text-[10px] rounded-full px-2 py-0.5 shrink-0 ${
group.isArchived ? "text-neutral-600 bg-neutral-800 line-through" : "text-gold bg-gold/10"
group.isArchived ? "text-neutral-600 bg-neutral-800 line-through" : "text-amber-700 dark:text-gold bg-gold/10"
}`}>
{group.dateBadge}
</span>
)}
{group.isArchived && (
<span className="text-[10px] text-neutral-600 bg-neutral-800 rounded-full px-2 py-0.5 shrink-0">архив</span>
<span className="text-[10px] text-neutral-500 bg-neutral-200 rounded-full px-2 py-0.5 shrink-0 dark:text-neutral-600 dark:bg-neutral-800">архив</span>
)}
<span className="text-[10px] text-neutral-500 bg-neutral-800 rounded-full px-2 py-0.5 shrink-0">{group.items.length} чел.</span>
<span className="text-[10px] text-neutral-500 bg-neutral-200 rounded-full px-2 py-0.5 shrink-0 dark:bg-neutral-800">{group.items.length} чел.</span>
{!group.isArchived && (
<div className="flex gap-2 ml-auto text-[10px]">
{groupCounts.new > 0 && <span className="text-gold">{groupCounts.new} новых</span>}
+42 -42
View File
@@ -161,43 +161,43 @@ function ConfirmModal({
if (!open) return null;
const selectClass = "w-full rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-sm text-white outline-none focus:border-gold/40 [color-scheme:dark] disabled:opacity-30 disabled:cursor-not-allowed";
const selectClass = "w-full rounded-lg border border-neutral-200 bg-neutral-100 px-3 py-2 text-sm text-neutral-900 outline-none focus:border-gold/40 [color-scheme:light] disabled:opacity-30 disabled:cursor-not-allowed dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-white dark:[color-scheme:dark]";
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" role="dialog" aria-modal="true" onClick={onClose}>
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
<div className="relative w-full max-w-sm rounded-2xl border border-white/[0.08] bg-[#0a0a0a] p-6 shadow-2xl" onClick={(e) => e.stopPropagation()}>
<button onClick={onClose} aria-label="Закрыть" className="absolute right-3 top-3 flex h-7 w-7 items-center justify-center rounded-full text-neutral-500 hover:bg-white/[0.06] hover:text-white">
<div className="relative w-full max-w-sm rounded-2xl border border-neutral-200 bg-white p-6 shadow-2xl dark:border-white/[0.08] dark:bg-[#0a0a0a]" onClick={(e) => e.stopPropagation()}>
<button onClick={onClose} aria-label="Закрыть" className="absolute right-3 top-3 flex h-7 w-7 items-center justify-center rounded-full text-neutral-500 hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-white/[0.06] dark:hover:text-white">
<X size={16} />
</button>
<h3 className="text-base font-bold text-white">Подтвердить запись</h3>
<p className="mt-1 text-xs text-neutral-400">{bookingName}</p>
<h3 className="text-base font-bold text-neutral-900 dark:text-white">Подтвердить запись</h3>
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{bookingName}</p>
<div className="mt-4 space-y-3">
<div>
<label className="text-[11px] font-medium text-neutral-400 mb-1 block">Зал</label>
<label className="text-[11px] font-medium text-neutral-500 mb-1 block dark:text-neutral-400">Зал</label>
<select value={hall} onChange={(e) => setHall(e.target.value)} className={selectClass}>
<option value="" className="bg-neutral-900">Выберите зал</option>
{halls.map((h) => <option key={h} value={h} className="bg-neutral-900">{h}</option>)}
<option value="" className="bg-white dark:bg-neutral-900">Выберите зал</option>
{halls.map((h) => <option key={h} value={h} className="bg-white dark:bg-neutral-900">{h}</option>)}
</select>
</div>
<div>
<label className="text-[11px] font-medium text-neutral-400 mb-1 block">Тренер</label>
<label className="text-[11px] font-medium text-neutral-500 mb-1 block dark:text-neutral-400">Тренер</label>
<select value={trainer} onChange={(e) => setTrainer(e.target.value)} disabled={!hall} className={selectClass}>
<option value="" className="bg-neutral-900">Выберите тренера</option>
{trainers.map((t) => <option key={t} value={t} className="bg-neutral-900">{t}</option>)}
<option value="" className="bg-white dark:bg-neutral-900">Выберите тренера</option>
{trainers.map((t) => <option key={t} value={t} className="bg-white dark:bg-neutral-900">{t}</option>)}
</select>
</div>
<div>
<label className="text-[11px] font-medium text-neutral-400 mb-1 block">Группа</label>
<label className="text-[11px] font-medium text-neutral-500 mb-1 block dark:text-neutral-400">Группа</label>
<select value={group} onChange={(e) => setGroup(e.target.value)} disabled={!trainer} className={selectClass}>
<option value="" className="bg-neutral-900">Выберите группу</option>
{groups.map((g) => <option key={g.value} value={g.value} className="bg-neutral-900">{g.label}</option>)}
<option value="" className="bg-white dark:bg-neutral-900">Выберите группу</option>
{groups.map((g) => <option key={g.value} value={g.value} className="bg-white dark:bg-neutral-900">{g.label}</option>)}
</select>
</div>
<div>
<label className="text-[11px] font-medium text-neutral-400 mb-1 block">Дата занятия</label>
<label className="text-[11px] font-medium text-neutral-500 mb-1 block dark:text-neutral-400">Дата занятия</label>
<input
type="date"
value={date}
@@ -212,14 +212,14 @@ function ConfirmModal({
)}
</div>
<div>
<label className="text-[11px] font-medium text-neutral-400 mb-1 block">Комментарий <span className="text-neutral-600">(необязательно)</span></label>
<label className="text-[11px] font-medium text-neutral-500 mb-1 block dark:text-neutral-400">Комментарий <span className="text-neutral-600">(необязательно)</span></label>
<input
type="text"
value={comment}
disabled={!group}
onChange={(e) => setComment(e.target.value)}
placeholder="Первое занятие, пробный"
className="w-full rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-sm text-white placeholder-neutral-500 outline-none focus:border-gold/40 disabled:opacity-30 disabled:cursor-not-allowed"
className="w-full rounded-lg border border-neutral-200 bg-neutral-100 px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 outline-none focus:border-gold/40 disabled:opacity-30 disabled:cursor-not-allowed dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-white dark:placeholder-neutral-500"
/>
</div>
</div>
@@ -318,8 +318,8 @@ function GroupBookingsTab({ filter, onDataChange }: { filter: BookingFilter; onD
onConfirm={(id) => setConfirmingId(id)}
renderExtra={(b) => (
<>
{b.groupInfo && <span className="text-xs text-neutral-400 bg-neutral-800 rounded-full px-2 py-0.5">{b.groupInfo}</span>}
{b.confirmedHall && <span className="text-[10px] text-neutral-500 bg-neutral-800 rounded-full px-2 py-0.5">{b.confirmedHall}</span>}
{b.groupInfo && <span className="text-xs text-neutral-500 bg-neutral-200 rounded-full px-2 py-0.5 dark:text-neutral-400 dark:bg-neutral-800">{b.groupInfo}</span>}
{b.confirmedHall && <span className="text-[10px] text-neutral-500 bg-neutral-200 rounded-full px-2 py-0.5 dark:bg-neutral-800">{b.confirmedHall}</span>}
{(b.confirmedGroup || b.confirmedDate) && (
<button
onClick={(e) => { e.stopPropagation(); setConfirmingId(b.id); }}
@@ -471,11 +471,11 @@ function RemindersTab() {
: currentStatus === "coming" ? "border-emerald-500/15 bg-emerald-500/[0.02]"
: currentStatus === "cancelled" ? "border-red-500/15 bg-red-500/[0.02] opacity-50"
: currentStatus === "pending" ? "border-amber-500/15 bg-amber-500/[0.02]"
: "border-white/5 bg-neutral-800/30"
: "border-neutral-200 bg-neutral-100/30 dark:border-white/5 dark:bg-neutral-800/30"
}`}
>
<div className="flex items-center gap-2 flex-wrap text-sm">
<span className="font-medium text-white">{item.name}</span>
<span className="font-medium text-neutral-900 dark:text-white">{item.name}</span>
{item.phone && (
<a href={`tel:${item.phone}`} className="inline-flex items-center gap-1 text-emerald-400 hover:text-emerald-300 text-xs">
<Phone size={10} />{item.phone}
@@ -542,11 +542,11 @@ function RemindersTab() {
const TypeIcon = typeConf.icon;
const egStats = countByStatus(eg.items);
return (
<div key={eg.label} className="rounded-xl border border-white/10 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 bg-neutral-900">
<div key={eg.label} className="rounded-xl border border-neutral-200 overflow-hidden dark:border-white/10">
<div className="flex items-center gap-2 px-4 py-2.5 bg-neutral-50 dark:bg-neutral-900">
<TypeIcon size={13} className={typeConf.color} />
<span className="text-sm font-medium text-white">{eg.label}{eg.items[0]?.eventHall ? ` · ${eg.items[0].eventHall}` : ""}</span>
<span className="text-[10px] text-neutral-500 bg-neutral-800 rounded-full px-2 py-0.5">{eg.items.length} чел.</span>
<span className="text-sm font-medium text-neutral-900 dark:text-white">{eg.label}{eg.items[0]?.eventHall ? ` · ${eg.items[0].eventHall}` : ""}</span>
<span className="text-[10px] text-neutral-500 bg-neutral-200 rounded-full px-2 py-0.5 dark:bg-neutral-800">{eg.items.length} чел.</span>
<div className="flex gap-2 ml-auto text-[10px]">
{egStats.coming > 0 && <span className="text-emerald-400">{egStats.coming} придёт</span>}
{egStats.cancelled > 0 && <span className="text-red-400">{egStats.cancelled} не придёт</span>}
@@ -672,15 +672,15 @@ function DashboardSummary({ refreshTrigger, onNavigate, onFilter, activeTab, act
if (c.tab === "reminders") {
const total = counts.remindersToday + counts.remindersTomorrow;
if (total === 0) return (
<div key={c.tab} className="rounded-xl border border-white/5 bg-neutral-900/50 p-3 opacity-40">
<div key={c.tab} className="rounded-xl border border-neutral-100 bg-neutral-50 p-3 opacity-40 dark:border-white/5 dark:bg-neutral-900/50">
<p className="text-xs text-neutral-500">{c.label}</p>
<p className="text-lg font-bold text-neutral-600 mt-1"></p>
<p className="text-lg font-bold text-neutral-400 mt-1 dark:text-neutral-600"></p>
</div>
);
return (
<button key={c.tab} onClick={() => onNavigate(c.tab)}
className={`rounded-xl border ${c.color} bg-neutral-900 p-3 text-left transition-all hover:bg-neutral-800/80 hover:scale-[1.02]`}>
<p className="text-xs text-neutral-400">{c.label}</p>
className={`rounded-xl border ${c.color} bg-neutral-50 p-3 text-left transition-all hover:bg-neutral-100 hover:scale-[1.02] dark:bg-neutral-900 dark:hover:bg-neutral-800/80`}>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{c.label}</p>
<div className="flex items-baseline gap-2 mt-1 flex-wrap">
{counts.remindersNotAsked > 0 && (
<span className="inline-flex items-baseline gap-1 cursor-pointer hover:underline decoration-neutral-500 underline-offset-2 transition-all"
@@ -718,20 +718,20 @@ function DashboardSummary({ refreshTrigger, onNavigate, onFilter, activeTab, act
const tc = c.counts!;
const total = tc.new + tc.contacted + tc.confirmed + tc.declined;
if (total === 0) return (
<div key={c.tab} className="rounded-xl border border-white/5 bg-neutral-900/50 p-3 opacity-40">
<div key={c.tab} className="rounded-xl border border-neutral-100 bg-neutral-50 p-3 opacity-40 dark:border-white/5 dark:bg-neutral-900/50">
<p className="text-xs text-neutral-500">{c.label}</p>
<p className="text-lg font-bold text-neutral-600 mt-1"></p>
<p className="text-lg font-bold text-neutral-400 mt-1 dark:text-neutral-600"></p>
</div>
);
const isActiveCard = activeTab === c.tab;
const hl = (status: BookingFilter) =>
isActiveCard && activeFilter === status
? "rounded-md bg-white/10 px-1.5 -mx-1.5 py-0.5 -my-0.5 ring-1 ring-white/20"
? "rounded-md bg-neutral-200 px-1.5 -mx-1.5 py-0.5 -my-0.5 ring-1 ring-neutral-300 dark:bg-white/10 dark:ring-white/20"
: "";
return (
<button key={c.tab} onClick={() => { onNavigate(c.tab); onFilter("all"); }}
className={`rounded-xl border ${c.color} bg-neutral-900 p-3 text-left transition-all hover:bg-neutral-800/80 hover:scale-[1.02]`}>
<p className="text-xs text-neutral-400">{c.label}</p>
className={`rounded-xl border ${c.color} bg-neutral-50 p-3 text-left transition-all hover:bg-neutral-100 hover:scale-[1.02] dark:bg-neutral-900 dark:hover:bg-neutral-800/80`}>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{c.label}</p>
<div className="flex items-baseline gap-2 mt-1 flex-wrap">
{tc.new > 0 && (
<>
@@ -897,7 +897,7 @@ function BookingsPageInner() {
<button
onClick={() => setHallFilter("all")}
className={`px-3 py-1.5 rounded-lg text-xs transition-colors ${
hallFilter === "all" ? "bg-gold/15 text-gold border border-gold/30" : "text-neutral-500 hover:text-white border border-transparent"
hallFilter === "all" ? "bg-gold/15 text-gold border border-gold/30" : "text-neutral-500 hover:text-neutral-900 border border-transparent dark:hover:text-white"
}`}
>
Все залы
@@ -907,7 +907,7 @@ function BookingsPageInner() {
key={hall}
onClick={() => setHallFilter(hallFilter === hall ? "all" : hall)}
className={`px-3 py-1.5 rounded-lg text-xs transition-colors ${
hallFilter === hall ? "bg-gold/15 text-gold border border-gold/30" : "text-neutral-500 hover:text-white border border-transparent"
hallFilter === hall ? "bg-gold/15 text-gold border border-gold/30" : "text-neutral-500 hover:text-neutral-900 border border-transparent dark:hover:text-white"
}`}
>
{hall}
@@ -929,10 +929,10 @@ function BookingsPageInner() {
<BookingCard key={`${r.type}-${r.id}`} status={r.status as BookingStatus}>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2 flex-wrap text-sm min-w-0">
<span className="text-[10px] text-neutral-500 bg-neutral-800 rounded-full px-2 py-0.5">{TYPE_LABELS[r.type] || r.type}</span>
<span className="font-medium text-white">{r.name}</span>
<span className="text-[10px] text-neutral-500 bg-neutral-200 rounded-full px-2 py-0.5 dark:bg-neutral-800">{TYPE_LABELS[r.type] || r.type}</span>
<span className="font-medium text-neutral-900 dark:text-white">{r.name}</span>
<ContactLinks phone={r.phone} instagram={r.instagram} telegram={r.telegram} />
{r.groupLabel && <span className="text-xs text-neutral-400 bg-neutral-800 rounded-full px-2 py-0.5">{r.groupLabel}</span>}
{r.groupLabel && <span className="text-xs text-neutral-500 bg-neutral-200 rounded-full px-2 py-0.5 dark:text-neutral-400 dark:bg-neutral-800">{r.groupLabel}</span>}
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="text-neutral-600 text-xs">{fmtDate(r.createdAt)}</span>
@@ -960,20 +960,20 @@ function BookingsPageInner() {
<select
value={tab}
onChange={(e) => setTab(e.target.value as Tab)}
className="w-full rounded-lg border border-white/10 bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white outline-none focus:border-gold/40 transition-colors [color-scheme:dark]"
className="w-full rounded-lg border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-900 outline-none focus:border-gold/40 transition-colors dark:border-white/10 dark:bg-neutral-900 dark:text-white dark:[color-scheme:dark]"
>
{TABS.map((t) => (
<option key={t.key} value={t.key}>{t.label}</option>
))}
</select>
</div>
<div className="mt-5 hidden sm:flex border-b border-white/10">
<div className="mt-5 hidden sm:flex border-b border-neutral-200 dark:border-white/10">
{TABS.map((t) => (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={`shrink-0 px-4 py-2.5 text-sm font-medium transition-colors relative whitespace-nowrap ${
tab === t.key ? "text-gold" : "text-neutral-400 hover:text-white"
tab === t.key ? "text-gold" : "text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white"
}`}
>
{t.label}
+5 -5
View File
@@ -67,7 +67,7 @@ function VideoSlot({
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-neutral-300">{label}</span>
{isCenter && (
<span className="inline-flex items-center gap-1 rounded-full bg-[#c9a96e]/15 px-2 py-0.5 text-[10px] font-medium text-[#c9a96e]">
<span className="inline-flex items-center gap-1 rounded-full bg-gold/15 px-2 py-0.5 text-[10px] font-medium text-gold">
<Smartphone size={10} />
мобильная версия
</span>
@@ -79,7 +79,7 @@ function VideoSlot({
{src ? (
<div
className={`group relative overflow-hidden rounded-lg border ${
isCenter ? "border-[#c9a96e]/40 ring-1 ring-[#c9a96e]/20" : "border-neutral-700"
isCenter ? "border-gold/40 ring-1 ring-[#c9a96e]/20" : "border-neutral-700"
}`}
onMouseEnter={() => videoRef.current?.play()}
onMouseLeave={() => { videoRef.current?.pause(); }}
@@ -104,7 +104,7 @@ function VideoSlot({
)}
</div>
{isCenter && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 flex items-center gap-1 rounded-full bg-[#c9a96e]/90 px-2 py-0.5 text-[10px] font-bold text-black">
<div className="absolute top-2 left-1/2 -translate-x-1/2 flex items-center gap-1 rounded-full bg-gold/90 px-2 py-0.5 text-[10px] font-bold text-black">
<Star size={10} fill="currentColor" />
MAIN
</div>
@@ -128,7 +128,7 @@ function VideoSlot({
disabled={uploading}
className={`flex aspect-[9/16] w-full items-center justify-center rounded-lg border-2 border-dashed transition-colors disabled:opacity-50 ${
isCenter
? "border-[#c9a96e]/30 text-[#c9a96e]/50 hover:border-[#c9a96e]/60 hover:text-[#c9a96e]"
? "border-gold/30 text-gold/50 hover:border-gold/60 hover:text-gold"
: "border-neutral-700 text-neutral-500 hover:border-neutral-500 hover:text-neutral-300"
}`}
>
@@ -164,7 +164,7 @@ function VideoSizeInfo({ totalSize, totalMb, rating }: { totalSize: number; tota
return (
<button
onClick={() => setOpen((v) => !v)}
className="w-full text-left rounded-lg bg-neutral-800/50 px-3 py-2 transition-colors hover:bg-neutral-800/80"
className="w-full text-left rounded-lg bg-neutral-100/80 px-3 py-2 transition-colors hover:bg-neutral-200/80 dark:bg-neutral-800/50 dark:hover:bg-neutral-800/80"
>
<div className="flex items-center justify-between">
<span className="text-xs text-neutral-400">Общий вес: <span className={`font-medium ${rating.color}`}>{formatFileSize(totalSize)}</span></span>
+23 -23
View File
@@ -63,7 +63,7 @@ function EventSettings({
onChange: (patch: Partial<OpenDayEvent>) => void;
}) {
return (
<div className="rounded-xl border border-white/10 bg-neutral-900 p-5 space-y-4">
<div className="rounded-xl border border-neutral-200 bg-white p-5 space-y-4 dark:border-white/10 dark:bg-neutral-900">
<h2 className="text-lg font-bold flex items-center gap-2">
<Calendar size={18} className="text-gold" />
Настройки мероприятия
@@ -71,16 +71,16 @@ function EventSettings({
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm text-neutral-400 mb-1.5">Название</label>
<label className="block text-sm text-neutral-500 mb-1.5 dark:text-neutral-400">Название</label>
<input
type="text"
value={event.title}
onChange={(e) => onChange({ title: e.target.value })}
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-white placeholder-neutral-500 outline-none focus:border-gold transition-colors"
className="w-full rounded-lg border border-neutral-200 bg-neutral-100 px-4 py-2.5 text-neutral-900 placeholder-neutral-400 outline-none focus:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500"
/>
</div>
<div>
<label className="block text-sm text-neutral-400 mb-1.5">Дата</label>
<label className="block text-sm text-neutral-500 mb-1.5 dark:text-neutral-400">Дата</label>
<input
type="date"
value={event.date}
@@ -89,10 +89,10 @@ function EventSettings({
const isPast = newDate && newDate < new Date().toISOString().slice(0, 10);
onChange({ date: newDate, ...(isPast || !newDate ? { active: false } : {}) });
}}
className={`w-full rounded-lg border bg-neutral-800 px-4 py-2.5 text-white outline-none transition-colors [color-scheme:dark] ${
className={`w-full rounded-lg border bg-neutral-100 px-4 py-2.5 text-neutral-900 outline-none transition-colors [color-scheme:light] dark:bg-neutral-800 dark:text-white dark:[color-scheme:dark] ${
event.date && event.date < new Date().toISOString().slice(0, 10)
? "border-amber-500/50"
: "border-white/10 focus:border-gold"
: "border-neutral-200 focus:border-gold dark:border-white/10"
}`}
/>
{!event.date && (
@@ -131,7 +131,7 @@ function EventSettings({
className={`flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all ${
event.discountPrice > 0
? "bg-gold/15 text-gold border border-gold/30"
: "bg-neutral-800 text-neutral-400 border border-white/10 hover:text-white"
: "bg-neutral-100 text-neutral-500 border border-neutral-200 hover:text-neutral-900 dark:bg-neutral-800 dark:text-neutral-400 dark:border-white/10 dark:hover:text-white"
}`}
>
<Sparkles size={14} />
@@ -147,12 +147,12 @@ function EventSettings({
/>
</div>
<div>
<label className="block text-sm text-neutral-400 mb-1.5">От N занятий</label>
<label className="block text-sm text-neutral-500 mb-1.5 dark:text-neutral-400">От N занятий</label>
<input
type="number"
value={event.discountThreshold || ""}
onChange={(e) => onChange({ discountThreshold: parseInt(e.target.value) || 0 })}
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-white outline-none focus:border-gold transition-colors"
className="w-full rounded-lg border border-neutral-200 bg-neutral-100 px-4 py-2.5 text-neutral-900 outline-none focus:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800 dark:text-white"
/>
</div>
</div>
@@ -178,8 +178,8 @@ function EventSettings({
event.active
? "bg-emerald-500/15 text-emerald-400 border border-emerald-500/30 cursor-pointer"
: !event.date || event.date < new Date().toISOString().slice(0, 10)
? "bg-neutral-800 text-neutral-500 border border-white/5 opacity-50 cursor-not-allowed"
: "bg-neutral-800 text-neutral-400 border border-white/10 cursor-pointer hover:border-gold/40 hover:text-gold hover:bg-gold/5"
? "bg-neutral-100 text-neutral-400 border border-neutral-200 opacity-50 cursor-not-allowed dark:bg-neutral-800 dark:text-neutral-500 dark:border-white/5"
: "bg-neutral-100 text-neutral-500 border border-neutral-200 cursor-pointer hover:border-gold/40 hover:text-gold hover:bg-gold/5 dark:bg-neutral-800 dark:text-neutral-400 dark:border-white/10"
}`}
>
{event.active ? (
@@ -242,7 +242,7 @@ function NewClassForm({
<SelectField label="" value={style} onChange={setStyle} options={styles.map((s) => ({ value: s, label: s }))} placeholder="Стиль..." />
<SelectField label="" value={trainer} onChange={setTrainer} options={trainers.map((t) => ({ value: t, label: t }))} placeholder="Тренер..." />
<div className="flex gap-2 justify-end mt-2">
<button onClick={onCancel} className="rounded-md border border-white/10 px-3 py-1 text-xs text-neutral-400 hover:text-white hover:border-white/25 transition-colors">Отмена</button>
<button onClick={onCancel} className="rounded-md border border-neutral-200 px-3 py-1 text-xs text-neutral-500 hover:text-neutral-900 hover:border-neutral-300 transition-colors dark:border-white/10 dark:text-neutral-400 dark:hover:text-white dark:hover:border-white/25">Отмена</button>
<button onClick={() => canSave && onSave({ trainer, style, endTime })} disabled={!canSave}
className="rounded-md bg-gold/20 border border-gold/30 px-3 py-1 text-xs font-medium text-gold hover:bg-gold/30 transition-colors disabled:opacity-30 disabled:cursor-not-allowed">Сохранить</button>
</div>
@@ -306,15 +306,15 @@ function ClassCell({
<div
className={`group relative p-2 rounded-lg cursor-pointer transition-all ${
cls.cancelled
? "bg-neutral-800/30 opacity-50"
? "bg-neutral-200/50 opacity-50 dark:bg-neutral-800/30"
: atRisk
? "bg-red-500/5 border border-red-500/20"
: "bg-gold/5 border border-gold/15 hover:border-gold/30"
: "bg-gold/10 border border-gold/25 dark:bg-gold/5 dark:border-gold/15 hover:border-gold/30"
}`}
onClick={() => onEdit(cls.id)}
>
<div className="flex items-center gap-1.5">
<span className="text-xs font-medium text-white truncate">{cls.style}</span>
<span className="text-xs font-medium text-neutral-900 truncate dark:text-white">{cls.style}</span>
<span className="text-[10px] text-neutral-500">{cls.startTime}{cls.endTime}</span>
</div>
<div className="text-[10px] text-neutral-400 truncate">{cls.trainer}</div>
@@ -468,7 +468,7 @@ function ScheduleGrid({
}
return (
<div ref={gridRef} className="rounded-xl border border-white/10 bg-neutral-900 p-5 space-y-3">
<div ref={gridRef} className="rounded-xl border border-neutral-200 bg-white p-5 space-y-3 dark:border-white/10 dark:bg-neutral-900">
<h2 className="text-lg font-bold">Расписание</h2>
{halls.length === 0 ? (
@@ -484,7 +484,7 @@ function ScheduleGrid({
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
selectedHall === hall
? "bg-gold/20 text-gold border border-gold/40"
: "bg-neutral-800 text-neutral-400 border border-white/10 hover:text-white"
: "bg-neutral-100 text-neutral-500 border border-neutral-200 hover:text-neutral-900 dark:bg-neutral-800 dark:text-neutral-400 dark:border-white/10 dark:hover:text-white"
}`}
>
{hall}
@@ -502,8 +502,8 @@ function ScheduleGrid({
{timeSlots.map((time) => {
const cls = hallClasses[time];
return (
<div key={time} className="flex items-start gap-3 border-t border-white/5 py-1.5">
<span className="text-xs text-neutral-500 w-12 pt-1.5 shrink-0">{time}</span>
<div key={time} className="flex items-start gap-3 border-t border-neutral-200 py-1.5 dark:border-white/5">
<span className="text-xs text-neutral-400 w-12 pt-1.5 shrink-0 dark:text-neutral-500">{time}</span>
<div className="flex-1">
{cls ? (
<ClassCell
@@ -528,7 +528,7 @@ function ScheduleGrid({
) : (
<button
onClick={() => { setCreatingTime(time); setEditingClassId(null); }}
className="w-full rounded-lg border border-dashed border-white/5 p-2 text-neutral-600 hover:text-gold hover:border-gold/20 transition-colors"
className="w-full rounded-lg border border-dashed border-neutral-200 p-2 text-neutral-400 hover:text-gold hover:border-gold/20 transition-colors dark:border-white/5 dark:text-neutral-600"
>
<Plus size={12} className="mx-auto" />
</button>
@@ -545,12 +545,12 @@ function ScheduleGrid({
{confirmAction && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={() => setConfirmAction(null)}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div className="relative w-full max-w-xs rounded-xl border border-white/[0.08] bg-[#141414] p-5 shadow-2xl" onClick={(e) => e.stopPropagation()}>
<p className="text-sm text-white text-center">{confirmAction.message}</p>
<div className="relative w-full max-w-xs rounded-xl border border-neutral-200 bg-white p-5 shadow-2xl dark:border-white/[0.08] dark:bg-[#141414]" onClick={(e) => e.stopPropagation()}>
<p className="text-sm text-neutral-900 text-center dark:text-white">{confirmAction.message}</p>
<div className="mt-4 flex gap-2 justify-center">
<button
onClick={() => setConfirmAction(null)}
className="rounded-lg border border-white/10 px-4 py-2 text-xs font-medium text-neutral-400 hover:text-white hover:border-white/25 transition-colors"
className="rounded-lg border border-neutral-200 px-4 py-2 text-xs font-medium text-neutral-500 hover:text-neutral-900 hover:border-neutral-300 transition-colors dark:border-white/10 dark:text-neutral-400 dark:hover:text-white dark:hover:border-white/25"
>
Нет
</button>