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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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="Текст параграфа..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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} />}
|
||||||
|
|||||||
@@ -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">–</span>
|
<span className="text-neutral-500 text-xs">–</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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user