fix: remaining admin & layout light theme polish

- Admin forms, dialogs, and page editors: light-mode borders, text contrast
- YandexMap: theme-aware map styles
- Layout: theme init script adjustments
This commit is contained in:
2026-04-10 21:36:33 +03:00
parent 97663c514e
commit a080ef5a8e
17 changed files with 177 additions and 144 deletions
+5 -5
View File
@@ -52,13 +52,13 @@ export function ConfirmDialog({
> >
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" /> <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div <div
className="relative w-full max-w-sm rounded-2xl border border-white/[0.08] bg-neutral-900 p-6 shadow-2xl" 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-neutral-900"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<button <button
onClick={onCancel} onClick={onCancel}
aria-label="Закрыть" aria-label="Закрыть"
className="absolute right-3 top-3 rounded-full p-1 text-neutral-500 hover:text-white transition-colors" className="absolute right-3 top-3 rounded-full p-1 text-neutral-500 hover:text-neutral-900 transition-colors dark:hover:text-white"
> >
<X size={16} /> <X size={16} />
</button> </button>
@@ -70,8 +70,8 @@ export function ConfirmDialog({
</div> </div>
)} )}
<div> <div>
<h3 className="text-base font-bold text-white">{title}</h3> <h3 className="text-base font-bold text-neutral-900 dark:text-white">{title}</h3>
<p className="mt-1.5 text-sm text-neutral-400">{message}</p> <p className="mt-1.5 text-sm text-neutral-600 dark:text-neutral-400">{message}</p>
</div> </div>
</div> </div>
@@ -79,7 +79,7 @@ export function ConfirmDialog({
<button <button
ref={cancelRef} ref={cancelRef}
onClick={onCancel} onClick={onCancel}
className="rounded-lg px-4 py-2 text-sm font-medium text-neutral-300 hover:bg-white/[0.06] transition-colors cursor-pointer" className="rounded-lg px-4 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-100 transition-colors cursor-pointer dark:text-neutral-300 dark:hover:bg-white/[0.06]"
> >
{cancelLabel} {cancelLabel}
</button> </button>
+31 -31
View File
@@ -12,10 +12,10 @@ interface InputFieldProps {
type?: "text" | "url" | "tel"; type?: "text" | "url" | "tel";
} }
const baseInput = "w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-white placeholder-neutral-500 outline-none hover:border-gold/30 focus:border-gold transition-colors"; const baseInput = "w-full rounded-lg border border-neutral-200 bg-neutral-100 px-4 py-2.5 text-neutral-900 placeholder-neutral-400 outline-none hover:border-gold/30 focus:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500";
const textAreaInput = `${baseInput} resize-none overflow-hidden`; const textAreaInput = `${baseInput} resize-none overflow-hidden`;
const smallInput = "rounded-md border border-white/10 bg-neutral-800 px-2.5 py-1.5 text-sm text-white placeholder-neutral-600 outline-none hover:border-gold/30 focus:border-gold transition-colors"; const smallInput = "rounded-md border border-neutral-200 bg-neutral-100 px-2.5 py-1.5 text-sm text-neutral-900 placeholder-neutral-400 outline-none hover:border-gold/30 focus:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-600";
const dashedInput = "flex-1 rounded-lg border border-dashed border-white/10 bg-neutral-800/50 px-4 py-2 text-sm text-white placeholder-neutral-600 outline-none hover:border-gold/30 hover:placeholder-neutral-500 focus:border-gold/50 transition-colors"; const dashedInput = "flex-1 rounded-lg border border-dashed border-neutral-200 bg-neutral-100/50 px-4 py-2 text-sm text-neutral-900 placeholder-neutral-400 outline-none hover:border-gold/30 hover:placeholder-neutral-500 focus:border-gold/50 transition-colors dark:border-white/10 dark:bg-neutral-800/50 dark:text-white dark:placeholder-neutral-600";
const inputCls = baseInput; const inputCls = baseInput;
export function InputField({ export function InputField({
@@ -311,7 +311,7 @@ export function RichTextarea({
`rounded p-1.5 transition-colors ${ `rounded p-1.5 transition-colors ${
active active
? "text-gold bg-gold/15" ? "text-gold bg-gold/15"
: "text-neutral-500 hover:text-white hover:bg-white/10" : "text-neutral-500 hover:text-neutral-900 hover:bg-neutral-200 dark:hover:text-white dark:hover:bg-white/10"
}`; }`;
// Preview mode: show rendered markup // Preview mode: show rendered markup
@@ -324,9 +324,9 @@ export function RichTextarea({
setEditing(true); setEditing(true);
requestAnimationFrame(() => ref.current?.focus()); requestAnimationFrame(() => ref.current?.focus());
}} }}
className="group rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 cursor-text hover:border-gold/30 transition-colors relative" className="group rounded-lg border border-neutral-200 bg-neutral-100 px-4 py-2.5 cursor-text hover:border-gold/30 transition-colors relative dark:border-white/10 dark:bg-neutral-800"
> >
<div className="text-sm leading-relaxed text-neutral-300"> <div className="text-sm leading-relaxed text-neutral-700 dark:text-neutral-300">
{formatMarkup(value)} {formatMarkup(value)}
</div> </div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
@@ -343,9 +343,9 @@ export function RichTextarea({
return ( return (
<div> <div>
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label> <label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
<div className="rounded-lg border border-white/10 bg-neutral-800 overflow-hidden hover:border-gold/30 focus-within:border-gold transition-colors"> <div className="rounded-lg border border-neutral-200 bg-neutral-100 overflow-hidden hover:border-gold/30 focus-within:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800">
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center gap-0.5 px-2 py-1 border-b border-white/5"> <div className="flex items-center gap-0.5 px-2 py-1 border-b border-neutral-200 dark:border-white/5">
<button <button
type="button" type="button"
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
@@ -396,7 +396,7 @@ export function RichTextarea({
onBlur={() => setEditing(false)} onBlur={() => setEditing(false)}
placeholder={placeholder} placeholder={placeholder}
rows={rows} rows={rows}
className="w-full bg-transparent px-4 py-2.5 text-white placeholder-neutral-500 outline-none resize-none" className="w-full bg-transparent px-4 py-2.5 text-neutral-900 placeholder-neutral-400 outline-none resize-none dark:text-white dark:placeholder-neutral-500"
/> />
</div> </div>
</div> </div>
@@ -480,8 +480,8 @@ export function SelectField({
{label} {label}
{hint && ( {hint && (
<span className="group relative"> <span className="group relative">
<span className="flex h-4 w-4 items-center justify-center rounded-full border border-white/15 text-[10px] text-neutral-500 hover:text-white hover:border-white/30 transition-colors cursor-help">?</span> <span className="flex h-4 w-4 items-center justify-center rounded-full border border-neutral-300 text-[10px] text-neutral-500 hover:text-neutral-900 hover:border-neutral-400 transition-colors cursor-help dark:border-white/15 dark:hover:text-white dark:hover:border-white/30">?</span>
<span className="absolute left-6 top-1/2 -translate-y-1/2 z-50 w-52 rounded-lg border border-white/10 bg-neutral-800 px-3 py-2 text-[11px] leading-relaxed text-neutral-300 shadow-xl opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity"> <span className="absolute left-6 top-1/2 -translate-y-1/2 z-50 w-52 rounded-lg border border-neutral-200 bg-white px-3 py-2 text-[11px] leading-relaxed text-neutral-700 shadow-xl opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity dark:border-white/10 dark:bg-neutral-800 dark:text-neutral-300">
{hint} {hint}
</span> </span>
</span> </span>
@@ -500,9 +500,9 @@ export function SelectField({
aria-expanded={open} aria-expanded={open}
aria-haspopup="listbox" aria-haspopup="listbox"
placeholder={placeholder || "Выберите..."} placeholder={placeholder || "Выберите..."}
className={`w-full rounded-lg border bg-neutral-800 outline-none transition-colors ${ className={`w-full rounded-lg border bg-neutral-100 text-neutral-900 outline-none transition-colors dark:bg-neutral-800 dark:text-white ${
label ? "px-4 py-2.5" : "px-2 py-1 text-xs" label ? "px-4 py-2.5" : "px-2 py-1 text-xs"
} ${open ? "border-gold" : "border-white/10"} ${!open && value ? "text-white" : "text-white"} placeholder-neutral-500`} } ${open ? "border-gold" : "border-neutral-200 dark:border-white/10"} placeholder-neutral-400 dark:placeholder-neutral-500`}
/> />
) : ( ) : (
<button <button
@@ -511,16 +511,16 @@ export function SelectField({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
aria-expanded={open} aria-expanded={open}
aria-haspopup="listbox" aria-haspopup="listbox"
className={`w-full rounded-lg border bg-neutral-800 text-left outline-none transition-colors ${ className={`w-full rounded-lg border bg-neutral-100 text-left outline-none transition-colors dark:bg-neutral-800 ${
label ? "px-4 py-2.5" : "px-2 py-1 text-xs" label ? "px-4 py-2.5" : "px-2 py-1 text-xs"
} ${open ? "border-gold" : "border-white/10"} ${value ? "text-white" : "text-neutral-500"}`} } ${open ? "border-gold" : "border-neutral-200 dark:border-white/10"} ${value ? "text-neutral-900 dark:text-white" : "text-neutral-500"}`}
> >
{selectedLabel || placeholder || "Выберите..."} {selectedLabel || placeholder || "Выберите..."}
</button> </button>
)} )}
{open && ( {open && (
<div role="listbox" className="absolute z-50 mt-1 w-full rounded-lg border border-white/10 bg-neutral-800 shadow-xl overflow-hidden"> <div role="listbox" className="absolute z-50 mt-1 w-full rounded-lg border border-neutral-200 bg-white shadow-xl overflow-hidden dark:border-white/10 dark:bg-neutral-800">
<div className="max-h-48 overflow-y-auto"> <div className="max-h-48 overflow-y-auto">
{filtered.length === 0 && ( {filtered.length === 0 && (
<div className="px-4 py-2 text-sm text-neutral-500">Ничего не найдено</div> <div className="px-4 py-2 text-sm text-neutral-500">Ничего не найдено</div>
@@ -541,8 +541,8 @@ export function SelectField({
inputRef.current?.blur(); inputRef.current?.blur();
}} }}
className={`w-full px-4 py-2 text-left text-sm transition-colors ${ className={`w-full px-4 py-2 text-left text-sm transition-colors ${
idx === highlightIndex ? "bg-white/10" : "hover:bg-white/5" idx === highlightIndex ? "bg-neutral-100 dark:bg-white/10" : "hover:bg-neutral-50 dark:hover:bg-white/5"
} ${opt.value === value ? "text-gold bg-gold/5" : "text-white"}`} } ${opt.value === value ? "text-gold bg-gold/5" : "text-neutral-900 dark:text-white"}`}
> >
{opt.label} {opt.label}
</button> </button>
@@ -598,7 +598,7 @@ export function TimeRangeField({ label, value, onChange, onBlur }: TimeRangeFiel
value={start} value={start}
onChange={(e) => handleStartChange(e.target.value)} onChange={(e) => handleStartChange(e.target.value)}
onBlur={onBlur} onBlur={onBlur}
className="flex-1 rounded-lg border border-white/10 bg-neutral-800 px-3 py-2.5 text-white outline-none focus:border-gold transition-colors" className="flex-1 rounded-lg border border-neutral-200 bg-neutral-100 px-3 py-2.5 text-neutral-900 outline-none focus:border-gold transition-colors [color-scheme:light] dark:border-white/10 dark:bg-neutral-800 dark:text-white dark:[color-scheme:dark]"
/> />
<span className="text-neutral-500"></span> <span className="text-neutral-500"></span>
<input <input
@@ -606,7 +606,7 @@ export function TimeRangeField({ label, value, onChange, onBlur }: TimeRangeFiel
value={end} value={end}
onChange={(e) => handleEndChange(e.target.value)} onChange={(e) => handleEndChange(e.target.value)}
onBlur={onBlur} onBlur={onBlur}
className="flex-1 rounded-lg border border-white/10 bg-neutral-800 px-3 py-2.5 text-white outline-none focus:border-gold transition-colors" className="flex-1 rounded-lg border border-neutral-200 bg-neutral-100 px-3 py-2.5 text-neutral-900 outline-none focus:border-gold transition-colors [color-scheme:light] dark:border-white/10 dark:bg-neutral-800 dark:text-white dark:[color-scheme:dark]"
/> />
</div> </div>
</div> </div>
@@ -677,7 +677,7 @@ export function ListField({ label, items, onChange, placeholder }: ListFieldProp
type="text" type="text"
value={item} value={item}
onChange={(e) => update(i, e.target.value)} onChange={(e) => update(i, e.target.value)}
className="flex-1 rounded-lg border border-white/10 bg-neutral-800 px-4 py-2 text-sm text-white outline-none focus:border-gold transition-colors" className="flex-1 rounded-lg border border-neutral-200 bg-neutral-100 px-4 py-2 text-sm text-neutral-900 outline-none focus:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800 dark:text-white"
/> />
<button <button
type="button" type="button"
@@ -778,13 +778,13 @@ export function VictoryListField({ label, items, onChange, placeholder, onLinkVa
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label> <label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
<div className="space-y-2"> <div className="space-y-2">
{items.map((item, i) => ( {items.map((item, i) => (
<div key={i} className="rounded-lg border border-white/10 bg-neutral-800/50 p-2.5 space-y-1.5 transition-colors hover:border-gold/30 hover:bg-neutral-800/80 focus-within:border-gold/50 focus-within:bg-neutral-800"> <div key={i} className="rounded-lg border border-neutral-200 bg-neutral-100/80 p-2.5 space-y-1.5 transition-colors hover:border-gold/30 hover:bg-neutral-200/80 focus-within:border-gold/50 focus-within:bg-neutral-200 dark:border-white/10 dark:bg-neutral-800/50 dark:hover:bg-neutral-800/80 dark:focus-within:bg-neutral-800">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<input <input
type="text" type="text"
value={item.text} value={item.text}
onChange={(e) => updateText(i, e.target.value)} onChange={(e) => updateText(i, e.target.value)}
className="flex-1 rounded-md border border-white/10 bg-neutral-800 px-2.5 py-1.5 text-sm text-white outline-none focus:border-gold transition-colors" className="flex-1 rounded-md border border-neutral-200 bg-neutral-100 px-2.5 py-1.5 text-sm text-neutral-900 outline-none focus:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800 dark:text-white"
/> />
<button <button
type="button" type="button"
@@ -796,7 +796,7 @@ export function VictoryListField({ label, items, onChange, placeholder, onLinkVa
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{item.image ? ( {item.image ? (
<div className="flex items-center gap-1 rounded bg-neutral-700/50 px-1.5 py-0.5 text-[11px] text-neutral-300"> <div className="flex items-center gap-1 rounded bg-neutral-200 px-1.5 py-0.5 text-[11px] text-neutral-700 dark:bg-neutral-700/50 dark:text-neutral-300">
<ImageIcon size={10} className="text-gold" /> <ImageIcon size={10} className="text-gold" />
<span className="max-w-[80px] truncate">{item.image.split("/").pop()}</span> <span className="max-w-[80px] truncate">{item.image.split("/").pop()}</span>
<button type="button" onClick={() => removeImage(i)} className="text-neutral-500 hover:text-red-400"> <button type="button" onClick={() => removeImage(i)} className="text-neutral-500 hover:text-red-400">
@@ -886,8 +886,8 @@ export function ValidatedLinkField({ value, onChange, onValidate, validationKey,
validate(e.target.value); validate(e.target.value);
}} }}
placeholder={placeholder || "Ссылка..."} placeholder={placeholder || "Ссылка..."}
className={`w-full rounded-md border bg-neutral-800 px-2 py-1 text-xs text-white placeholder-neutral-600 outline-none transition-colors ${ className={`w-full rounded-md border bg-neutral-100 px-2 py-1 text-xs text-neutral-900 placeholder-neutral-400 outline-none transition-colors dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-600 ${
error ? "border-red-500/50" : "border-white/5 focus:border-gold/50" error ? "border-red-500/50" : "border-neutral-200 focus:border-gold/50 dark:border-white/5"
}`} }`}
/> />
{error && ( {error && (
@@ -965,8 +965,8 @@ export function AutocompleteMulti({
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label> <label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
<div <div
onClick={() => { setOpen(true); inputRef.current?.focus(); }} onClick={() => { setOpen(true); inputRef.current?.focus(); }}
className={`flex flex-wrap items-center gap-1.5 rounded-lg border bg-neutral-800 px-3 py-2 min-h-[42px] cursor-text transition-colors ${ className={`flex flex-wrap items-center gap-1.5 rounded-lg border bg-neutral-100 px-3 py-2 min-h-[42px] cursor-text transition-colors dark:bg-neutral-800 ${
open ? "border-gold" : "border-white/10 hover:border-gold/30" open ? "border-gold" : "border-neutral-200 hover:border-gold/30 dark:border-white/10"
}`} }`}
> >
{selected.map((item) => ( {selected.map((item) => (
@@ -985,14 +985,14 @@ export function AutocompleteMulti({
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={selected.length === 0 ? placeholder : ""} placeholder={selected.length === 0 ? placeholder : ""}
className="flex-1 min-w-[80px] bg-transparent text-sm text-white placeholder-neutral-500 outline-none" className="flex-1 min-w-[80px] bg-transparent text-sm text-neutral-900 placeholder-neutral-400 outline-none dark:text-white dark:placeholder-neutral-500"
/> />
</div> </div>
{open && filtered.length > 0 && ( {open && filtered.length > 0 && (
<div className="absolute z-50 mt-1 w-full rounded-lg border border-white/10 bg-neutral-800 shadow-xl overflow-hidden max-h-48 overflow-y-auto"> <div className="absolute z-50 mt-1 w-full rounded-lg border border-neutral-200 bg-white shadow-xl overflow-hidden max-h-48 overflow-y-auto dark:border-white/10 dark:bg-neutral-800">
{filtered.map((opt) => ( {filtered.map((opt) => (
<button key={opt} type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => addItem(opt)} <button key={opt} type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => addItem(opt)}
className="w-full px-4 py-2 text-left text-sm text-white hover:bg-white/5 transition-colors"> className="w-full px-4 py-2 text-left text-sm text-neutral-900 hover:bg-neutral-50 transition-colors dark:text-white dark:hover:bg-white/5">
{opt} {opt}
</button> </button>
))} ))}
+5 -5
View File
@@ -106,14 +106,14 @@ export function ImageCropField({
return ( return (
<div> <div>
<label className="block text-sm text-neutral-400 mb-1.5"> <label className="block text-sm text-neutral-500 mb-1.5 dark:text-neutral-400">
{label} <span className="text-neutral-600">(перетащите · Ctrl+колёсико для масштаба)</span> {label} <span className="text-neutral-400 dark:text-neutral-600">(перетащите · Ctrl+колёсико для масштаба)</span>
</label> </label>
{image ? ( {image ? (
<div className={`${maxWidth} space-y-2`}> <div className={`${maxWidth} space-y-2`}>
<div <div
ref={containerRef} ref={containerRef}
className={`relative ${aspect} overflow-hidden rounded-lg border border-white/10 cursor-grab active:cursor-grabbing select-none`} className={`relative ${aspect} overflow-hidden rounded-lg border border-neutral-200 cursor-grab active:cursor-grabbing select-none dark:border-white/10`}
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove} onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp} onPointerUp={handlePointerUp}
@@ -155,7 +155,7 @@ export function ImageCropField({
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="flex cursor-pointer items-center gap-1.5 rounded-md border border-white/10 px-2.5 py-1 text-xs text-neutral-400 hover:text-white hover:border-white/25 transition-colors"> <label className="flex cursor-pointer items-center gap-1.5 rounded-md border border-neutral-200 px-2.5 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">
{uploading ? <Loader2 size={12} className="animate-spin" /> : <Upload size={12} />} {uploading ? <Loader2 size={12} className="animate-spin" /> : <Upload size={12} />}
Заменить Заменить
<input type="file" accept="image/*" onChange={handleUpload} className="hidden" /> <input type="file" accept="image/*" onChange={handleUpload} className="hidden" />
@@ -170,7 +170,7 @@ export function ImageCropField({
</div> </div>
</div> </div>
) : ( ) : (
<label className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-dashed border-white/15 px-4 py-2.5 text-neutral-500 hover:border-gold/30 hover:text-neutral-300 transition-colors"> <label className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-dashed border-neutral-300 px-4 py-2.5 text-neutral-500 hover:border-gold/30 hover:text-neutral-700 transition-colors dark:border-white/15 dark:hover:text-neutral-300">
{uploading ? <Loader2 size={14} className="animate-spin" /> : <ImageIcon size={14} />} {uploading ? <Loader2 size={14} className="animate-spin" /> : <ImageIcon size={14} />}
<span className="text-xs">{uploading ? "Загрузка..." : "Загрузить фото"}</span> <span className="text-xs">{uploading ? "Загрузка..." : "Загрузить фото"}</span>
<input type="file" accept="image/*" onChange={handleUpload} className="hidden" /> <input type="file" accept="image/*" onChange={handleUpload} className="hidden" />
+3 -3
View File
@@ -12,8 +12,8 @@ export function PriceField({ label, value, onChange, placeholder = "0" }: PriceF
return ( return (
<div> <div>
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label> <label className="block text-sm text-neutral-500 mb-1.5 dark:text-neutral-400">{label}</label>
<div className="flex rounded-lg border border-white/10 bg-neutral-800 focus-within:border-gold transition-colors"> <div className="flex rounded-lg border border-neutral-200 bg-neutral-100 focus-within:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800">
<input <input
type="text" type="text"
inputMode="decimal" inputMode="decimal"
@@ -23,7 +23,7 @@ export function PriceField({ label, value, onChange, placeholder = "0" }: PriceF
onChange(v ? `${v} BYN` : ""); onChange(v ? `${v} BYN` : "");
}} }}
placeholder={placeholder} placeholder={placeholder}
className="flex-1 bg-transparent px-4 py-2.5 text-white placeholder-neutral-500 outline-none min-w-0" className="flex-1 bg-transparent px-4 py-2.5 text-neutral-900 placeholder-neutral-400 outline-none min-w-0 dark:text-white dark:placeholder-neutral-500"
/> />
<span className="flex items-center pr-4 text-sm font-medium text-gold select-none"> <span className="flex items-center pr-4 text-sm font-medium text-gold select-none">
BYN BYN
+1 -1
View File
@@ -29,7 +29,7 @@ export default function AboutEditorPage() {
value={text} value={text}
onChange={(e) => updateItem(e.target.value)} onChange={(e) => updateItem(e.target.value)}
rows={2} rows={2}
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-3 py-2 text-sm text-white placeholder-neutral-500 outline-none hover:border-gold/30 focus:border-gold transition-colors resize-none" 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 hover:border-gold/30 focus:border-gold transition-colors resize-none dark:border-white/10 dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500"
placeholder="Текст параграфа..." placeholder="Текст параграфа..."
/> />
)} )}
+14 -14
View File
@@ -65,7 +65,7 @@ function SearchSelect({ options, value, onChange, placeholder }: {
<div <div
onClick={() => { setOpen(true); setTimeout(() => inputRef.current?.focus(), 0); }} onClick={() => { setOpen(true); setTimeout(() => inputRef.current?.focus(), 0); }}
className={`flex items-center gap-2 w-full rounded-lg border px-3 py-2 text-sm cursor-text transition-colors ${ className={`flex items-center gap-2 w-full rounded-lg border px-3 py-2 text-sm cursor-text transition-colors ${
open ? "border-gold/40 bg-white/[0.06]" : "border-white/[0.08] bg-white/[0.04]" open ? "border-gold/40 bg-neutral-200/60 dark:bg-white/[0.06]" : "border-neutral-200 bg-neutral-100 dark:border-white/[0.08] dark:bg-white/[0.04]"
}`} }`}
> >
{open ? ( {open ? (
@@ -75,14 +75,14 @@ function SearchSelect({ options, value, onChange, placeholder }: {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder={selected ? selected.label : placeholder} placeholder={selected ? selected.label : placeholder}
className="flex-1 bg-transparent text-white placeholder-neutral-500 outline-none text-sm" className="flex-1 bg-transparent text-neutral-900 placeholder-neutral-400 outline-none text-sm dark:text-white dark:placeholder-neutral-500"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Escape") { setOpen(false); setSearch(""); } if (e.key === "Escape") { setOpen(false); setSearch(""); }
if (e.key === "Backspace" && !search && value) { onChange(""); } if (e.key === "Backspace" && !search && value) { onChange(""); }
}} }}
/> />
) : ( ) : (
<span className={`flex-1 truncate ${selected ? "text-white" : "text-neutral-500"}`}> <span className={`flex-1 truncate ${selected ? "text-neutral-900 dark:text-white" : "text-neutral-500"}`}>
{selected ? selected.label : placeholder} {selected ? selected.label : placeholder}
</span> </span>
)} )}
@@ -100,7 +100,7 @@ function SearchSelect({ options, value, onChange, placeholder }: {
</div> </div>
{open && ( {open && (
<div className="absolute z-20 mt-1 w-full rounded-lg border border-white/[0.08] shadow-xl overflow-hidden" style={{ backgroundColor: "#141414" }}> <div className="absolute z-20 mt-1 w-full rounded-lg border border-neutral-200 bg-white shadow-xl overflow-hidden dark:border-white/[0.08] dark:bg-[#141414]">
<div className="max-h-48 overflow-y-scroll admin-scrollbar"> <div className="max-h-48 overflow-y-scroll admin-scrollbar">
{filtered.length === 0 && ( {filtered.length === 0 && (
<p className="px-3 py-2 text-xs text-neutral-500">Ничего не найдено</p> <p className="px-3 py-2 text-xs text-neutral-500">Ничего не найдено</p>
@@ -112,7 +112,7 @@ function SearchSelect({ options, value, onChange, placeholder }: {
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={() => { onChange(o.value); setOpen(false); setSearch(""); }} onClick={() => { onChange(o.value); setOpen(false); setSearch(""); }}
className={`w-full px-3 py-2 text-left text-sm transition-colors ${ className={`w-full px-3 py-2 text-left text-sm transition-colors ${
o.value === value ? "bg-gold/10 text-gold" : "text-white hover:bg-white/[0.05]" o.value === value ? "bg-gold/10 text-gold" : "text-neutral-900 hover:bg-neutral-50 dark:text-white dark:hover:bg-white/[0.05]"
}`} }`}
> >
{o.label} {o.label}
@@ -288,7 +288,7 @@ export function AddBookingModal({
if (!open) return null; if (!open) return null;
const inputClass = "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 placeholder-neutral-500"; const inputClass = "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 placeholder-neutral-400 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-white dark:placeholder-neutral-500";
const canSubmit = name.trim() && phone.trim() && !saving const canSubmit = name.trim() && phone.trim() && !saving
&& (tab === "classes" || (tab === "events" && eventType === "master-class" && hasUpcomingMc) && (tab === "classes" || (tab === "events" && eventType === "master-class" && hasUpcomingMc)
@@ -297,21 +297,21 @@ export function AddBookingModal({
return createPortal( return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={onClose}> <div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={onClose}>
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" /> <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()}> <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} 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"> <button onClick={onClose} 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} /> <X size={16} />
</button> </button>
<h3 className="text-base font-bold text-white">Добавить запись</h3> <h3 className="text-base font-bold text-neutral-900 dark:text-white">Добавить запись</h3>
<p className="mt-1 text-xs text-neutral-400">Ручная запись (Instagram, звонок, лично)</p> <p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">Ручная запись (Instagram, звонок, лично)</p>
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-3">
{/* Type selector — single row */} {/* Type selector — single row */}
<div className="flex rounded-lg border border-white/[0.08] bg-white/[0.03] p-0.5"> <div className="flex rounded-lg border border-neutral-200 bg-neutral-100 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.03]">
<button <button
onClick={() => setTab("classes")} onClick={() => setTab("classes")}
className={`flex-1 rounded-md py-2 text-xs font-medium transition-all ${ className={`flex-1 rounded-md py-2 text-xs font-medium transition-all ${
tab === "classes" ? "bg-gold/20 text-gold shadow-sm" : "text-neutral-400 hover:text-white" tab === "classes" ? "bg-gold/20 text-gold shadow-sm" : "text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white"
}`} }`}
> >
Занятие Занятие
@@ -320,7 +320,7 @@ export function AddBookingModal({
<button <button
onClick={() => { setTab("events"); setEventType("master-class"); }} onClick={() => { setTab("events"); setEventType("master-class"); }}
className={`flex-1 rounded-md py-2 text-xs font-medium transition-all ${ className={`flex-1 rounded-md py-2 text-xs font-medium transition-all ${
tab === "events" && eventType === "master-class" ? "bg-purple-500/15 text-purple-400 shadow-sm" : "text-neutral-400 hover:text-white" tab === "events" && eventType === "master-class" ? "bg-purple-500/15 text-purple-400 shadow-sm" : "text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white"
}`} }`}
> >
Мастер-класс Мастер-класс
@@ -330,7 +330,7 @@ export function AddBookingModal({
<button <button
onClick={() => { setTab("events"); setEventType("open-day"); }} onClick={() => { setTab("events"); setEventType("open-day"); }}
className={`flex-1 rounded-md py-2 text-xs font-medium transition-all ${ className={`flex-1 rounded-md py-2 text-xs font-medium transition-all ${
tab === "events" && eventType === "open-day" ? "bg-blue-500/15 text-blue-400 shadow-sm" : "text-neutral-400 hover:text-white" tab === "events" && eventType === "open-day" ? "bg-blue-500/15 text-blue-400 shadow-sm" : "text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white"
}`} }`}
> >
Open Day Open Day
+2 -2
View File
@@ -43,10 +43,10 @@ export function SearchBar({
value={query} value={query}
onChange={(e) => handleChange(e.target.value)} onChange={(e) => handleChange(e.target.value)}
placeholder="Поиск по имени или телефону..." placeholder="Поиск по имени или телефону..."
className="w-full rounded-lg border border-white/[0.08] bg-white/[0.04] py-2 pl-9 pr-8 text-sm text-white placeholder-neutral-500 outline-none focus:border-gold/40" className="w-full rounded-lg border border-neutral-200 bg-neutral-100 py-2 pl-9 pr-8 text-sm text-neutral-900 placeholder-neutral-400 outline-none focus:border-gold/40 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-white dark:placeholder-neutral-500"
/> />
{query && ( {query && (
<button onClick={clear} className="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-500 hover:text-white"> <button onClick={clear} className="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-500 hover:text-neutral-900 dark:hover:text-white">
<X size={14} /> <X size={14} />
</button> </button>
)} )}
+6 -6
View File
@@ -109,8 +109,8 @@ function IconPicker({
setSearch(""); setSearch("");
setTimeout(() => inputRef.current?.focus(), 0); setTimeout(() => inputRef.current?.focus(), 0);
}} }}
className={`w-full flex items-center gap-2.5 rounded-lg border bg-neutral-800 px-4 py-2.5 text-left text-white outline-none transition-colors ${ className={`w-full flex items-center gap-2.5 rounded-lg border bg-neutral-100 px-4 py-2.5 text-left text-neutral-900 outline-none transition-colors dark:bg-neutral-800 dark:text-white ${
open ? "border-gold" : "border-white/10" open ? "border-gold" : "border-neutral-200 dark:border-white/10"
}`} }`}
> >
{SelectedIcon ? ( {SelectedIcon ? (
@@ -118,13 +118,13 @@ function IconPicker({
<SelectedIcon size={16} /> <SelectedIcon size={16} />
</span> </span>
) : ( ) : (
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-white/10 text-neutral-500">?</span> <span className="flex h-6 w-6 items-center justify-center rounded-md bg-neutral-200 text-neutral-500 dark:bg-white/10">?</span>
)} )}
<span className="text-sm">{selected?.label || value}</span> <span className="text-sm">{selected?.label || value}</span>
</button> </button>
{open && ( {open && (
<div className="absolute z-50 mt-1 w-full rounded-lg border border-white/10 bg-neutral-800 shadow-xl overflow-hidden"> <div className="absolute z-50 mt-1 w-full rounded-lg border border-neutral-200 bg-white shadow-xl overflow-hidden dark:border-white/10 dark:bg-neutral-800">
<div className="p-2 pb-0"> <div className="p-2 pb-0">
<input <input
ref={inputRef} ref={inputRef}
@@ -132,7 +132,7 @@ function IconPicker({
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="Поиск..." placeholder="Поиск..."
className="w-full rounded-md border border-white/10 bg-neutral-900 px-3 py-1.5 text-sm text-white outline-none focus:border-gold/50 placeholder:text-neutral-600" className="w-full rounded-md border border-neutral-200 bg-neutral-100 px-3 py-1.5 text-sm text-neutral-900 outline-none focus:border-gold/50 placeholder:text-neutral-400 dark:border-white/10 dark:bg-neutral-900 dark:text-white dark:placeholder:text-neutral-600"
/> />
</div> </div>
<div className="p-2 max-h-56 overflow-y-auto"> <div className="p-2 max-h-56 overflow-y-auto">
@@ -153,7 +153,7 @@ function IconPicker({
className={`flex flex-col items-center gap-0.5 rounded-lg p-2 transition-colors ${ className={`flex flex-col items-center gap-0.5 rounded-lg p-2 transition-colors ${
key === value key === value
? "bg-gold/20 text-gold-light" ? "bg-gold/20 text-gold-light"
: "text-neutral-400 hover:bg-white/5 hover:text-white" : "text-neutral-500 hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-white/5 dark:hover:text-white"
}`} }`}
> >
<Icon size={20} /> <Icon size={20} />
+6 -6
View File
@@ -41,8 +41,8 @@ function PhoneField({ value, onChange }: { value: string; onChange: (v: string)
value={value ?? ""} value={value ?? ""}
onChange={handleChange} onChange={handleChange}
placeholder="+375 (XX) XXX-XX-XX" placeholder="+375 (XX) XXX-XX-XX"
className={`w-full rounded-lg border bg-neutral-800 px-4 py-2.5 text-white placeholder-neutral-500 outline-none transition-colors ${ className={`w-full rounded-lg border bg-neutral-100 px-4 py-2.5 text-neutral-900 placeholder-neutral-400 outline-none transition-colors dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500 ${
value && !isComplete ? "border-red-500/50" : "border-white/10 focus:border-gold" value && !isComplete ? "border-red-500/50" : "border-neutral-200 focus:border-gold dark:border-white/10"
}`} }`}
/> />
{isComplete && ( {isComplete && (
@@ -106,12 +106,12 @@ function InstagramField({ value, onChange }: { value: string; onChange: (v: stri
validateUsername(username); validateUsername(username);
}} }}
placeholder="blackheartdancehouse" placeholder="blackheartdancehouse"
className={`w-full rounded-lg border bg-neutral-800 pl-8 pr-10 py-2.5 text-white placeholder-neutral-500 outline-none hover:border-gold/30 transition-colors ${ className={`w-full rounded-lg border bg-neutral-100 pl-8 pr-10 py-2.5 text-neutral-900 placeholder-neutral-400 outline-none hover:border-gold/30 transition-colors dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500 ${
status === "invalid" status === "invalid"
? "border-red-500 focus:border-red-500" ? "border-red-500 focus:border-red-500"
: status === "valid" : status === "valid"
? "border-green-500/50 focus:border-green-500" ? "border-green-500/50 focus:border-green-500"
: "border-white/10 focus:border-gold" : "border-neutral-200 focus:border-gold dark:border-white/10"
}`} }`}
/> />
<span className="absolute right-3 top-1/2 -translate-y-1/2"> <span className="absolute right-3 top-1/2 -translate-y-1/2">
@@ -164,7 +164,7 @@ function AddressList({ items, onChange }: { items: string[]; onChange: (items: s
type="text" type="text"
value={addr} value={addr}
onChange={(e) => update(i, e.target.value)} onChange={(e) => update(i, e.target.value)}
className="flex-1 rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-sm text-white outline-none focus:border-gold transition-colors" className="flex-1 rounded-lg border border-neutral-200 bg-neutral-100 px-4 py-2.5 text-sm text-neutral-900 outline-none focus:border-gold transition-colors dark:border-white/10 dark:bg-neutral-800 dark:text-white"
/> />
<button <button
type="button" type="button"
@@ -183,7 +183,7 @@ function AddressList({ items, onChange }: { items: string[]; onChange: (items: s
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }}
onBlur={add} onBlur={add}
placeholder="Добавить адрес..." placeholder="Добавить адрес..."
className="flex-1 rounded-lg border border-dashed border-white/15 bg-neutral-800/50 px-4 py-2.5 text-sm text-white placeholder-neutral-500 outline-none focus:border-gold/50 transition-colors" className="flex-1 rounded-lg border border-dashed border-neutral-300 bg-neutral-100/50 px-4 py-2.5 text-sm text-neutral-900 placeholder-neutral-400 outline-none focus:border-gold/50 transition-colors dark:border-white/15 dark:bg-neutral-800/50 dark:text-white dark:placeholder-neutral-500"
/> />
<button <button
type="button" type="button"
+17 -11
View File
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { adminFetch } from "@/lib/csrf"; import { adminFetch } from "@/lib/csrf";
import { ThemeToggle } from "@/components/ui/ThemeToggle";
import { import {
LayoutDashboard, LayoutDashboard,
Sparkles, Sparkles,
@@ -88,7 +89,7 @@ export default function AdminLayout({
} }
return ( return (
<div className="flex min-h-screen bg-neutral-950 text-white"> <div className="flex min-h-screen bg-neutral-50 text-neutral-900 dark:bg-neutral-950 dark:text-white">
{/* Mobile overlay */} {/* Mobile overlay */}
{sidebarOpen && ( {sidebarOpen && (
<div <div
@@ -99,18 +100,18 @@ export default function AdminLayout({
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-white/10 bg-neutral-900 transition-transform lg:sticky lg:top-0 lg:h-screen lg:translate-x-0 ${ className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-neutral-200 bg-white dark:border-white/10 dark:bg-neutral-900 transition-transform lg:sticky lg:top-0 lg:h-screen lg:translate-x-0 ${
sidebarOpen ? "translate-x-0" : "-translate-x-full" sidebarOpen ? "translate-x-0" : "-translate-x-full"
}`} }`}
> >
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4"> <div className="flex items-center justify-between border-b border-neutral-200 dark:border-white/10 px-5 py-4">
<Link href="/admin" className="text-lg font-bold"> <Link href="/admin" className="text-lg font-bold">
BLACK HEART BLACK HEART
</Link> </Link>
<button <button
onClick={() => setSidebarOpen(false)} onClick={() => setSidebarOpen(false)}
aria-label="Закрыть меню" aria-label="Закрыть меню"
className="lg:hidden text-neutral-400 hover:text-white" className="lg:hidden text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white"
> >
<X size={20} /> <X size={20} />
</button> </button>
@@ -128,7 +129,7 @@ export default function AdminLayout({
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors ${ className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors ${
active active
? "bg-gold/10 text-gold font-medium" ? "bg-gold/10 text-gold font-medium"
: "text-neutral-400 hover:text-white hover:bg-white/5" : "text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 dark:text-neutral-400 dark:hover:text-white dark:hover:bg-white/5"
}`} }`}
> >
<Icon size={18} /> <Icon size={18} />
@@ -143,18 +144,22 @@ export default function AdminLayout({
})} })}
</nav> </nav>
<div className="border-t border-white/10 p-3 space-y-1"> <div className="border-t border-neutral-200 dark:border-white/10 p-3 space-y-1">
<div className="flex items-center justify-between px-3 py-1">
<span className="text-xs text-neutral-400 dark:text-neutral-500">Тема</span>
<ThemeToggle />
</div>
<Link <Link
href="/" href="/"
target="_blank" target="_blank"
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-neutral-400 hover:text-white hover:bg-white/5 transition-colors" className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 dark:text-neutral-400 dark:hover:text-white dark:hover:bg-white/5 transition-colors"
> >
<ChevronLeft size={18} /> <ChevronLeft size={18} />
Открыть сайт Открыть сайт
</Link> </Link>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-neutral-400 hover:text-red-400 hover:bg-white/5 transition-colors" className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-neutral-500 hover:text-red-500 hover:bg-neutral-100 dark:text-neutral-400 dark:hover:text-red-400 dark:hover:bg-white/5 transition-colors"
> >
<LogOut size={18} /> <LogOut size={18} />
Выйти Выйти
@@ -165,16 +170,17 @@ export default function AdminLayout({
{/* Main content */} {/* Main content */}
<div className="flex-1 flex flex-col min-w-0"> <div className="flex-1 flex flex-col min-w-0">
{/* Top bar (mobile) */} {/* Top bar (mobile) */}
<header className="sticky top-0 z-30 flex items-center gap-3 border-b border-white/10 bg-neutral-950 px-4 py-3 lg:hidden"> <header className="sticky top-0 z-30 flex items-center gap-3 border-b border-neutral-200 bg-white dark:border-white/10 dark:bg-neutral-950 px-4 py-3 lg:hidden">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
aria-label="Открыть меню" aria-label="Открыть меню"
aria-expanded={sidebarOpen} aria-expanded={sidebarOpen}
className="text-neutral-400 hover:text-white" className="text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white"
> >
<Menu size={24} /> <Menu size={24} />
</button> </button>
<a href="/admin" className="font-bold hover:text-gold transition-colors">BLACK HEART</a> <a href="/admin" className="font-bold hover:text-gold transition-colors flex-1">BLACK HEART</a>
<ThemeToggle />
</header> </header>
<main className="flex-1 p-4 sm:p-6 lg:p-8">{children}</main> <main className="flex-1 p-4 sm:p-6 lg:p-8">{children}</main>
+7 -7
View File
@@ -36,18 +36,18 @@ export default function AdminLoginPage() {
} }
return ( return (
<div className="flex min-h-screen items-center justify-center bg-neutral-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-neutral-50 px-4 dark:bg-neutral-950">
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="w-full max-w-sm space-y-6 rounded-2xl border border-white/10 bg-neutral-900 p-8" className="w-full max-w-sm space-y-6 rounded-2xl border border-neutral-200 bg-white p-8 dark:border-white/10 dark:bg-neutral-900"
> >
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold text-white">BLACK HEART</h1> <h1 className="text-2xl font-bold text-neutral-900 dark:text-white">BLACK HEART</h1>
<p className="mt-1 text-sm text-neutral-400">Панель управления</p> <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">Панель управления</p>
</div> </div>
<div> <div>
<label htmlFor="password" className="block text-sm text-neutral-400 mb-2"> <label htmlFor="password" className="block text-sm text-neutral-500 mb-2 dark:text-neutral-400">
Пароль Пароль
</label> </label>
<div className="relative"> <div className="relative">
@@ -56,7 +56,7 @@ export default function AdminLoginPage() {
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-3 pr-11 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-3 pr-11 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"
placeholder="Введите пароль" placeholder="Введите пароль"
autoFocus autoFocus
aria-describedby={error ? "login-error" : undefined} aria-describedby={error ? "login-error" : undefined}
@@ -65,7 +65,7 @@ export default function AdminLoginPage() {
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? "Скрыть пароль" : "Показать пароль"} aria-label={showPassword ? "Скрыть пароль" : "Показать пароль"}
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-500 hover:text-white transition-colors" className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-500 hover:text-neutral-900 transition-colors dark:hover:text-white"
tabIndex={-1} tabIndex={-1}
> >
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />} {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
+15 -15
View File
@@ -68,7 +68,7 @@ function LocationSelect({
className={`rounded-full px-3 py-1.5 text-xs font-medium transition-all ${ className={`rounded-full px-3 py-1.5 text-xs font-medium transition-all ${
active active
? "bg-gold/20 text-gold border border-gold/40" ? "bg-gold/20 text-gold border border-gold/40"
: "bg-neutral-800 text-neutral-400 border border-white/10 hover:border-white/25 hover:text-white" : "bg-neutral-100 text-neutral-500 border border-neutral-200 hover:border-neutral-300 hover:text-neutral-900 dark:bg-neutral-800 dark:text-neutral-400 dark:border-white/10 dark:hover:border-white/25 dark:hover:text-white"
}`} }`}
> >
{active && <Check size={10} className="inline mr-1" />} {active && <Check size={10} className="inline mr-1" />}
@@ -144,16 +144,16 @@ function SlotsField({
type="date" type="date"
value={slot.date} value={slot.date}
onChange={(e) => updateSlot(i, { date: e.target.value })} onChange={(e) => updateSlot(i, { date: e.target.value })}
className={`w-[140px] rounded-lg border bg-neutral-800 px-3 py-2 text-sm text-white outline-none transition-colors [color-scheme:dark] ${ className={`w-[140px] rounded-lg border bg-neutral-100 px-3 py-2 text-sm text-neutral-900 outline-none transition-colors [color-scheme:light] dark:bg-neutral-800 dark:text-white dark:[color-scheme:dark] ${
!slot.date ? "border-red-500/50" : "border-white/10 focus:border-gold" !slot.date ? "border-red-500/50" : "border-neutral-200 focus:border-gold dark:border-white/10"
}`} }`}
/> />
<input <input
type="time" type="time"
value={slot.startTime} value={slot.startTime}
onChange={(e) => updateSlot(i, { startTime: e.target.value })} onChange={(e) => updateSlot(i, { startTime: e.target.value })}
className={`w-[100px] rounded-lg border bg-neutral-800 px-3 py-2 text-sm text-white outline-none transition-colors [color-scheme:dark] ${ className={`w-[100px] rounded-lg border bg-neutral-100 px-3 py-2 text-sm text-neutral-900 outline-none transition-colors [color-scheme:light] dark:bg-neutral-800 dark:text-white dark:[color-scheme:dark] ${
timeError ? "border-red-500/50" : "border-white/10 focus:border-gold" timeError ? "border-red-500/50" : "border-neutral-200 focus:border-gold dark:border-white/10"
}`} }`}
/> />
<span className="text-neutral-500 text-xs">&ndash;</span> <span className="text-neutral-500 text-xs">&ndash;</span>
@@ -161,12 +161,12 @@ function SlotsField({
type="time" type="time"
value={slot.endTime} value={slot.endTime}
onChange={(e) => updateSlot(i, { endTime: e.target.value })} onChange={(e) => updateSlot(i, { endTime: e.target.value })}
className={`w-[100px] rounded-lg border bg-neutral-800 px-3 py-2 text-sm text-white outline-none transition-colors [color-scheme:dark] ${ className={`w-[100px] rounded-lg border bg-neutral-100 px-3 py-2 text-sm text-neutral-900 outline-none transition-colors [color-scheme:light] dark:bg-neutral-800 dark:text-white dark:[color-scheme:dark] ${
timeError ? "border-red-500/50" : "border-white/10 focus:border-gold" timeError ? "border-red-500/50" : "border-neutral-200 focus:border-gold dark:border-white/10"
}`} }`}
/> />
{dur && ( {dur && (
<span className="text-[11px] text-neutral-500 bg-neutral-800/50 rounded-full px-2 py-0.5"> <span className="text-[11px] text-neutral-500 bg-neutral-200/50 rounded-full px-2 py-0.5 dark:bg-neutral-800/50">
{dur} {dur}
</span> </span>
)} )}
@@ -190,7 +190,7 @@ function SlotsField({
<button <button
type="button" type="button"
onClick={addSlot} onClick={addSlot}
className="flex items-center gap-2 rounded-lg border border-dashed border-white/10 bg-neutral-800/50 px-3 py-1.5 text-xs text-neutral-500 hover:text-gold hover:border-gold/30 transition-colors" className="flex items-center gap-2 rounded-lg border border-dashed border-neutral-200 bg-neutral-100/50 px-3 py-1.5 text-xs text-neutral-500 hover:text-gold hover:border-gold/30 transition-colors dark:border-white/10 dark:bg-neutral-800/50"
> >
<Plus size={12} /> <Plus size={12} />
Добавить дату Добавить дату
@@ -223,8 +223,8 @@ function InstagramLinkField({
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder="https://instagram.com/p/... или /reel/..." placeholder="https://instagram.com/p/... или /reel/..."
className={`w-full rounded-lg border bg-neutral-800 px-4 py-2.5 text-white placeholder-neutral-500 outline-none transition-colors ${ className={`w-full rounded-lg border bg-neutral-100 px-4 py-2.5 text-neutral-900 placeholder-neutral-400 outline-none transition-colors dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500 ${
error ? "border-red-500/50" : "border-white/10 focus:border-gold" error ? "border-red-500/50" : "border-neutral-200 focus:border-gold dark:border-white/10"
}`} }`}
/> />
{value && !error && ( {value && !error && (
@@ -318,13 +318,13 @@ function FilterBar({
value={search} value={search}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
placeholder="Поиск по названию или тренеру..." placeholder="Поиск по названию или тренеру..."
className="w-full rounded-lg border border-white/10 bg-neutral-800 pl-10 pr-4 py-2.5 text-sm text-white placeholder-neutral-500 outline-none focus:border-gold transition-colors" className="w-full rounded-lg border border-neutral-200 bg-neutral-100 pl-10 pr-4 py-2.5 text-sm 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"
/> />
{search && ( {search && (
<button <button
type="button" type="button"
onClick={() => onSearchChange("")} onClick={() => onSearchChange("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-500 hover:text-white transition-colors" className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-500 hover:text-neutral-900 transition-colors dark:hover:text-white"
> >
<X size={14} /> <X size={14} />
</button> </button>
@@ -340,7 +340,7 @@ function FilterBar({
className={`rounded-full px-3 py-1 text-xs font-medium transition-all ${ className={`rounded-full px-3 py-1 text-xs font-medium transition-all ${
dateFilter === key dateFilter === key
? "bg-gold/20 text-gold border border-gold/40" ? "bg-gold/20 text-gold border border-gold/40"
: "bg-neutral-800 text-neutral-400 border border-white/10 hover:border-white/25 hover:text-white" : "bg-neutral-100 text-neutral-500 border border-neutral-200 hover:border-neutral-300 hover:text-neutral-900 dark:bg-neutral-800 dark:text-neutral-400 dark:border-white/10 dark:hover:border-white/25 dark:hover:text-white"
}`} }`}
> >
{DATE_FILTER_LABELS[key]} {DATE_FILTER_LABELS[key]}
@@ -359,7 +359,7 @@ function FilterBar({
className={`rounded-full px-3 py-1 text-xs font-medium transition-all ${ className={`rounded-full px-3 py-1 text-xs font-medium transition-all ${
locationFilter === loc.name locationFilter === loc.name
? "bg-gold/20 text-gold border border-gold/40" ? "bg-gold/20 text-gold border border-gold/40"
: "bg-neutral-800 text-neutral-400 border border-white/10 hover:border-white/25 hover:text-white" : "bg-neutral-100 text-neutral-500 border border-neutral-200 hover:border-neutral-300 hover:text-neutral-900 dark:bg-neutral-800 dark:text-neutral-400 dark:border-white/10 dark:hover:border-white/25 dark:hover:text-white"
}`} }`}
> >
{loc.name} {loc.name}
+7 -7
View File
@@ -63,13 +63,13 @@ function UnreadWidget({ counts }: { counts: UnreadCounts }) {
<UserPlus size={20} /> <UserPlus size={20} />
</div> </div>
<div> <div>
<h2 className="font-medium text-white"> <h2 className="font-medium text-neutral-900 dark:text-white">
Новые записи Новые записи
<span className="ml-2 inline-flex items-center justify-center rounded-full bg-red-500 text-white text-[11px] font-bold min-w-[20px] h-[20px] px-1.5"> <span className="ml-2 inline-flex items-center justify-center rounded-full bg-red-500 text-white text-[11px] font-bold min-w-[20px] h-[20px] px-1.5">
{counts.total} {counts.total}
</span> </span>
</h2> </h2>
<p className="text-xs text-neutral-400">Не подтверждённые заявки</p> <p className="text-xs text-neutral-500 dark:text-neutral-400">Не подтверждённые заявки</p>
</div> </div>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
@@ -78,7 +78,7 @@ function UnreadWidget({ counts }: { counts: UnreadCounts }) {
<span className="rounded-full bg-gold/15 text-gold font-medium px-2 py-0.5"> <span className="rounded-full bg-gold/15 text-gold font-medium px-2 py-0.5">
{item.count} {item.count}
</span> </span>
<span className="text-neutral-400">{item.label}</span> <span className="text-neutral-500 dark:text-neutral-400">{item.label}</span>
</div> </div>
))} ))}
</div> </div>
@@ -99,7 +99,7 @@ export default function AdminDashboard() {
return ( return (
<div> <div>
<h1 className="text-2xl font-bold">Панель управления</h1> <h1 className="text-2xl font-bold">Панель управления</h1>
<p className="mt-1 text-neutral-400">Выберите раздел для редактирования</p> <p className="mt-1 text-neutral-500 dark:text-neutral-400">Выберите раздел для редактирования</p>
{/* Unread bookings widget */} {/* Unread bookings widget */}
{counts && counts.total > 0 && ( {counts && counts.total > 0 && (
@@ -116,14 +116,14 @@ export default function AdminDashboard() {
<Link <Link
key={card.href} key={card.href}
href={card.href} href={card.href}
className="group rounded-xl border border-white/10 bg-neutral-900 p-5 transition-all hover:border-gold/30 hover:bg-neutral-900/80" className="group rounded-xl border border-neutral-200 bg-white p-5 transition-all hover:border-gold/30 hover:bg-neutral-50 dark:border-white/10 dark:bg-neutral-900 dark:hover:bg-neutral-900/80"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gold/10 text-gold"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gold/10 text-gold">
<Icon size={20} /> <Icon size={20} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h2 className="font-medium text-white group-hover:text-gold transition-colors flex items-center gap-2"> <h2 className="font-medium text-neutral-900 group-hover:text-gold transition-colors flex items-center gap-2 dark:text-white">
{card.label} {card.label}
{isBookings && counts && counts.total > 0 && ( {isBookings && counts && counts.total > 0 && (
<span className="rounded-full bg-red-500 text-white text-[10px] font-bold min-w-[18px] h-[18px] flex items-center justify-center px-1"> <span className="rounded-full bg-red-500 text-white text-[10px] font-bold min-w-[18px] h-[18px] flex items-center justify-center px-1">
@@ -131,7 +131,7 @@ export default function AdminDashboard() {
</span> </span>
)} )}
</h2> </h2>
<p className="text-xs text-neutral-500">{card.desc}</p> <p className="text-xs text-neutral-500 dark:text-neutral-500">{card.desc}</p>
</div> </div>
</div> </div>
</Link> </Link>
+21 -21
View File
@@ -222,7 +222,7 @@ function ClassBlock({
: {}), : {}),
}} }}
className={`absolute left-1 right-1 rounded-md border border-white/20 border-l-3 px-2 py-0.5 text-left text-xs text-white cursor-grab active:cursor-grabbing overflow-hidden select-none ${colors} ${ className={`absolute left-1 right-1 rounded-md border border-white/20 border-l-3 px-2 py-0.5 text-left text-xs text-white cursor-grab active:cursor-grabbing overflow-hidden select-none ${colors} ${
isOverlapping ? "ring-2 ring-red-500 ring-offset-1 ring-offset-neutral-900" : "" isOverlapping ? "ring-2 ring-red-500 ring-offset-1 ring-offset-white dark:ring-offset-neutral-900" : ""
} ${isDragging ? "opacity-30" : "hover:opacity-90 hover:border-white/40"}`} } ${isDragging ? "opacity-30" : "hover:opacity-90 hover:border-white/40"}`}
title={`${cls.time}\n${cls.type}\n${cls.trainer}${cls.level ? ` · ${cls.level}` : ""}${cls.status ? ` · ${cls.status}` : ""}`} title={`${cls.time}\n${cls.type}\n${cls.trainer}${cls.level ? ` · ${cls.level}` : ""}${cls.status ? ` · ${cls.status}` : ""}`}
> >
@@ -415,14 +415,14 @@ function ClassModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
<div <div
className="w-full max-w-md rounded-xl border border-white/10 bg-neutral-900 p-6 shadow-2xl" className="w-full max-w-md rounded-xl border border-neutral-200 bg-white p-6 shadow-2xl dark:border-white/10 dark:bg-neutral-900"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-white"> <h3 className="text-lg font-bold text-neutral-900 dark:text-white">
{isNew ? "Новое занятие" : "Редактировать занятие"} {isNew ? "Новое занятие" : "Редактировать занятие"}
</h3> </h3>
<button type="button" onClick={onClose} className="text-neutral-400 hover:text-white"> <button type="button" onClick={onClose} className="text-neutral-400 hover:text-neutral-900 transition-colors dark:hover:text-white">
<X size={20} /> <X size={20} />
</button> </button>
</div> </div>
@@ -431,7 +431,7 @@ function ClassModal({
{/* Day selector */} {/* Day selector */}
{allDays.length > 1 && ( {allDays.length > 1 && (
<div> <div>
<label className="block text-sm text-neutral-400 mb-2">Дни</label> <label className="block text-sm text-neutral-500 mb-2 dark:text-neutral-400">Дни</label>
{/* Day toggle buttons */} {/* Day toggle buttons */}
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
@@ -445,7 +445,7 @@ function ClassModal({
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${ className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
active active
? "bg-gold/20 text-gold border border-gold/40" ? "bg-gold/20 text-gold border border-gold/40"
: "border border-white/10 text-neutral-500 hover:text-white hover:border-white/20" : "border border-neutral-200 text-neutral-500 hover:text-neutral-900 hover:border-neutral-300 dark:border-white/10 dark:hover:text-white dark:hover:border-white/20"
}`} }`}
> >
{d.dayShort} {d.dayShort}
@@ -473,10 +473,10 @@ function ClassModal({
}); });
} }
}} }}
className="flex items-center gap-2 text-sm text-neutral-300 select-none" className="flex items-center gap-2 text-sm text-neutral-600 select-none dark:text-neutral-300"
> >
<span className={`inline-flex items-center justify-center w-4 h-4 rounded border transition-colors ${ <span className={`inline-flex items-center justify-center w-4 h-4 rounded border transition-colors ${
sameTime ? "bg-gold border-gold" : "border-white/20 bg-neutral-800" sameTime ? "bg-gold border-gold" : "border-neutral-300 bg-neutral-100 dark:border-white/20 dark:bg-neutral-800"
}`}> }`}>
{sameTime && <span className="text-black text-xs font-bold leading-none"></span>} {sameTime && <span className="text-black text-xs font-bold leading-none"></span>}
</span> </span>
@@ -492,10 +492,10 @@ function ClassModal({
/> />
) : ( ) : (
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="block text-sm text-neutral-400">Время по дням</label> <label className="block text-sm text-neutral-500 dark:text-neutral-400">Время по дням</label>
{allDays.filter((d) => selectedDays.has(d.day)).map((d) => ( {allDays.filter((d) => selectedDays.has(d.day)).map((d) => (
<div key={d.day} className="flex items-center gap-2"> <div key={d.day} className="flex items-center gap-2">
<span className="shrink-0 text-xs font-medium text-neutral-400 min-w-[28px]"> <span className="shrink-0 text-xs font-medium text-neutral-500 min-w-[28px] dark:text-neutral-400">
{d.dayShort} {d.dayShort}
</span> </span>
<div className="flex-1"> <div className="flex-1">
@@ -576,7 +576,7 @@ function ClassModal({
}} }}
className={`flex-1 rounded-lg px-4 py-2.5 text-sm font-medium transition-opacity ${ className={`flex-1 rounded-lg px-4 py-2.5 text-sm font-medium transition-opacity ${
touched && !isValid touched && !isValid
? "bg-neutral-700 text-neutral-400 cursor-not-allowed" ? "bg-neutral-200 text-neutral-400 cursor-not-allowed dark:bg-neutral-700"
: "bg-gold text-black hover:opacity-90" : "bg-gold text-black hover:opacity-90"
}`} }`}
> >
@@ -973,18 +973,18 @@ function CalendarGrid({
{/* Calendar */} {/* Calendar */}
{sortedDays.length > 0 && ( {sortedDays.length > 0 && (
<div className="overflow-x-auto rounded-lg border border-white/10" ref={gridRef}> <div className="overflow-x-auto rounded-lg border border-neutral-200 dark:border-white/10" ref={gridRef}>
<div className="min-w-[600px]"> <div className="min-w-[600px]">
{/* Day headers */} {/* Day headers */}
<div className="flex border-b border-white/10 bg-neutral-800/50"> <div className="flex border-b border-neutral-200 bg-neutral-100 dark:border-white/10 dark:bg-neutral-800/50">
<div className="w-14 shrink-0 bg-neutral-900" /> <div className="w-14 shrink-0 bg-neutral-50 dark:bg-neutral-900" />
{sortedDays.map((day, di) => ( {sortedDays.map((day, di) => (
<div <div
key={day.day} key={day.day}
className="flex-1 border-l border-white/10 px-2 py-2 text-center" className="flex-1 border-l border-neutral-200 px-2 py-2 text-center dark:border-white/10"
> >
<div className="flex items-center justify-center gap-1"> <div className="flex items-center justify-center gap-1">
<span className="text-sm font-medium text-white">{day.dayShort}</span> <span className="text-sm font-medium text-neutral-900 dark:text-white">{day.dayShort}</span>
<span className="text-xs text-neutral-500">({day.classes.length})</span> <span className="text-xs text-neutral-500">({day.classes.length})</span>
</div> </div>
</div> </div>
@@ -1017,7 +1017,7 @@ function CalendarGrid({
<div <div
key={day.day} key={day.day}
ref={(el) => { columnRefs.current[di] = el; }} ref={(el) => { columnRefs.current[di] = el; }}
className={`flex-1 border-l border-white/10 relative ${drag ? "cursor-grabbing" : "cursor-pointer"}`} className={`flex-1 border-l border-neutral-200 relative dark:border-white/10 ${drag ? "cursor-grabbing" : "cursor-pointer"}`}
style={{ height: `${TOTAL_HOURS * HOUR_HEIGHT}px` }} style={{ height: `${TOTAL_HOURS * HOUR_HEIGHT}px` }}
onMouseMove={(e) => { onMouseMove={(e) => {
if (drag) return; if (drag) return;
@@ -1044,7 +1044,7 @@ function CalendarGrid({
{hours.slice(0, -1).map((h) => ( {hours.slice(0, -1).map((h) => (
<div <div
key={h} key={h}
className="absolute left-0 right-0 border-t border-white/5" className="absolute left-0 right-0 border-t border-neutral-200/60 dark:border-white/5"
style={{ top: `${(h - HOUR_START) * HOUR_HEIGHT}px` }} style={{ top: `${(h - HOUR_START) * HOUR_HEIGHT}px` }}
/> />
))} ))}
@@ -1052,7 +1052,7 @@ function CalendarGrid({
{hours.slice(0, -1).map((h) => ( {hours.slice(0, -1).map((h) => (
<div <div
key={`${h}-30`} key={`${h}-30`}
className="absolute left-0 right-0 border-t border-white/[0.02]" className="absolute left-0 right-0 border-t border-neutral-200/30 dark:border-white/[0.02]"
style={{ top: `${(h - HOUR_START) * HOUR_HEIGHT + HOUR_HEIGHT / 2}px` }} style={{ top: `${(h - HOUR_START) * HOUR_HEIGHT + HOUR_HEIGHT / 2}px` }}
/> />
))} ))}
@@ -1330,7 +1330,7 @@ export default function ScheduleEditorPage() {
className={`flex items-center gap-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${ className={`flex items-center gap-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
i === activeLocation i === activeLocation
? "bg-gold/10 text-gold border border-gold/30" ? "bg-gold/10 text-gold border border-gold/30"
: "border border-white/10 text-neutral-400 hover:text-white" : "border border-neutral-200 text-neutral-500 hover:text-neutral-900 dark:border-white/10 dark:text-neutral-400 dark:hover:text-white"
}`} }`}
> >
<button <button
@@ -1362,7 +1362,7 @@ export default function ScheduleEditorPage() {
update({ ...data, locations: newLocations }); update({ ...data, locations: newLocations });
setActiveLocation(newLocations.length - 1); setActiveLocation(newLocations.length - 1);
}} }}
className="rounded-lg border border-dashed border-white/20 px-4 py-2 text-sm text-neutral-500 hover:text-white transition-colors" className="rounded-lg border border-dashed border-neutral-300 px-4 py-2 text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:border-white/20 dark:hover:text-white"
> >
<Plus size={14} className="inline" /> Локация <Plus size={14} className="inline" /> Локация
</button> </button>
+4 -4
View File
@@ -165,18 +165,18 @@ export default function TeamEditorPage() {
renderItem={(member) => ( renderItem={(member) => (
<Link <Link
href={`/admin/team/${member.id}`} href={`/admin/team/${member.id}`}
className="flex items-center gap-4 flex-1 min-w-0 rounded-lg px-2 py-1.5 -my-1.5 hover:bg-white/[0.04] transition-colors" className="flex items-center gap-4 flex-1 min-w-0 rounded-lg px-2 py-1.5 -my-1.5 hover:bg-neutral-100 transition-colors dark:hover:bg-white/[0.04]"
> >
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-lg"> <div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-lg">
{member.image ? ( {member.image ? (
<Image src={member.image} alt={member.name} fill className="object-cover" sizes="56px" /> <Image src={member.image} alt={member.name} fill className="object-cover" sizes="56px" />
) : ( ) : (
<div className="h-full w-full bg-neutral-800" /> <div className="h-full w-full bg-neutral-200 dark:bg-neutral-800" />
)} )}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium text-white truncate">{member.name}</p> <p className="font-medium text-neutral-900 truncate dark:text-white">{member.name}</p>
<p className="text-sm text-neutral-400 truncate">{member.role}</p> <p className="text-sm text-neutral-500 truncate dark:text-neutral-400">{member.role}</p>
</div> </div>
</Link> </Link>
)} )}
+8 -1
View File
@@ -43,7 +43,14 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="ru" className="dark"> <html lang="ru" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{var t=localStorage.getItem('theme');if(t==='light'){document.documentElement.classList.remove('dark')}else{document.documentElement.classList.add('dark')}}catch(e){}})();`,
}}
/>
</head>
<body <body
className={`${inter.variable} ${oswald.variable} surface-base font-sans antialiased`} className={`${inter.variable} ${oswald.variable} surface-base font-sans antialiased`}
> >
+25 -5
View File
@@ -20,15 +20,32 @@ function cleanAddress(addr: string): string {
.trim(); .trim();
} }
function useIsDark() {
const [dark, setDark] = useState(true);
useEffect(() => {
function check() {
setDark(document.documentElement.classList.contains("dark"));
}
check();
const observer = new MutationObserver(check);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
return dark;
}
export function YandexMap({ addresses, height = 380 }: YandexMapProps) { export function YandexMap({ addresses, height = 380 }: YandexMapProps) {
const [mapSrc, setMapSrc] = useState<string | null>(null); const [baseUrl, setBaseUrl] = useState<string | null>(null);
const isDark = useIsDark();
useEffect(() => { useEffect(() => {
if (!addresses.length) return; if (!addresses.length) return;
let cancelled = false; let cancelled = false;
async function build() { async function build() {
// Geocode all addresses in parallel
const results = await Promise.allSettled( const results = await Promise.allSettled(
addresses.map(async (addr) => { addresses.map(async (addr) => {
const cleaned = cleanAddress(addr); const cleaned = cleanAddress(addr);
@@ -55,9 +72,9 @@ export function YandexMap({ addresses, height = 380 }: YandexMapProps) {
const centerLat = points.reduce((s, p) => s + p.lat, 0) / points.length; const centerLat = points.reduce((s, p) => s + p.lat, 0) / points.length;
const centerLon = points.reduce((s, p) => s + p.lon, 0) / points.length; const centerLon = points.reduce((s, p) => s + p.lon, 0) / points.length;
const zoom = points.length === 1 ? 15 : 12; const zoom = points.length === 1 ? 15 : 12;
const pts = points.map((p) => `${p.lon},${p.lat},pm2ntl`).join("~"); const pts = points.map((p) => `${p.lon},${p.lat},pm2ntl`).join("~");
setMapSrc(`https://yandex.ru/map-widget/v1/?ll=${centerLon},${centerLat}&z=${zoom}&pt=${pts}&l=map&theme=dark`);
setBaseUrl(`https://yandex.ru/map-widget/v1/?ll=${centerLon},${centerLat}&z=${zoom}&pt=${pts}&l=map`);
} }
build(); build();
@@ -66,7 +83,7 @@ export function YandexMap({ addresses, height = 380 }: YandexMapProps) {
if (!addresses.length) return null; if (!addresses.length) return null;
if (!mapSrc) { if (!baseUrl) {
return ( return (
<div style={{ width: "100%", height }} className="flex items-center justify-center text-neutral-500 text-sm"> <div style={{ width: "100%", height }} className="flex items-center justify-center text-neutral-500 text-sm">
Загрузка карты... Загрузка карты...
@@ -74,8 +91,11 @@ export function YandexMap({ addresses, height = 380 }: YandexMapProps) {
); );
} }
const mapSrc = `${baseUrl}&theme=${isDark ? "dark" : "light"}`;
return ( return (
<iframe <iframe
key={mapSrc}
src={mapSrc} src={mapSrc}
width="100%" width="100%"
height={height} height={height}