fix: schedule status system — auto-key, config order, label lookup
- Auto-generate status key from label (admin doesn't need to set keys) - Remove visible key field from status config editor - Order statuses/levels in filters by config order (matches admin panel) - Shared findStatusConfig() for robust label lookup (by key, label, or derived key) - Custom status badges in DayCard, GroupCard, MobileSchedule - Simplified filter logic with clsStatus helper - Removed dead code: TIME_PRESETS, StatusFilter type - SelectField: blur input after selection to prevent re-open
This commit is contained in:
@@ -251,6 +251,7 @@ export function SelectField({
|
|||||||
onChange(opt.value);
|
onChange(opt.value);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setSearch("");
|
setSearch("");
|
||||||
|
inputRef.current?.blur();
|
||||||
}}
|
}}
|
||||||
className={`w-full px-4 py-2 text-left text-sm transition-colors hover:bg-white/5 ${
|
className={`w-full px-4 py-2 text-left text-sm transition-colors hover:bg-white/5 ${
|
||||||
opt.value === value ? "text-gold bg-gold/5" : "text-white"
|
opt.value === value ? "text-gold bg-gold/5" : "text-white"
|
||||||
|
|||||||
@@ -50,11 +50,16 @@ function buildLevelOptions(config: ScheduleConfig) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusKey(s: { key: string; label: string }): string {
|
||||||
|
// Use explicit key if set, otherwise derive from label
|
||||||
|
return s.key || s.label.toLowerCase().replace(/[^a-zа-яё0-9]/gi, "_").replace(/_+/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
function buildStatusOptions(config: ScheduleConfig) {
|
function buildStatusOptions(config: ScheduleConfig) {
|
||||||
const statuses = config.statuses ?? DEFAULT_CONFIG.statuses;
|
const statuses = config.statuses ?? DEFAULT_CONFIG.statuses;
|
||||||
return [
|
return [
|
||||||
{ value: "", label: "Без статуса" },
|
{ value: "", label: "Без статуса" },
|
||||||
...statuses.map((s) => ({ value: s.key, label: s.label })),
|
...statuses.map((s) => ({ value: statusKey(s), label: s.label })),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +280,16 @@ function ClassModal({
|
|||||||
statusOptions: { value: string; label: string }[];
|
statusOptions: { value: string; label: string }[];
|
||||||
statusHint: string;
|
statusHint: string;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState<ScheduleClass>(cls);
|
const [draft, setDraft] = useState<ScheduleClass>(() => {
|
||||||
|
// Migrate legacy booleans to status field
|
||||||
|
if (!cls.status && (cls.hasSlots || cls.recruiting)) {
|
||||||
|
return {
|
||||||
|
...cls,
|
||||||
|
status: cls.hasSlots ? "hasSlots" : "recruiting",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return cls;
|
||||||
|
});
|
||||||
const trainerOptions = trainers.map((t) => ({ value: t, label: t }));
|
const trainerOptions = trainers.map((t) => ({ value: t, label: t }));
|
||||||
const typeOptions = classTypes.map((t) => ({ value: t, label: t }));
|
const typeOptions = classTypes.map((t) => ({ value: t, label: t }));
|
||||||
const isNew = !onDelete;
|
const isNew = !onDelete;
|
||||||
@@ -514,7 +528,7 @@ function ClassModal({
|
|||||||
/>
|
/>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Статус"
|
label="Статус"
|
||||||
value={draft.status || (draft.recruiting ? "recruiting" : draft.hasSlots ? "hasSlots" : "")}
|
value={draft.status || ""}
|
||||||
onChange={(v) => setDraft({
|
onChange={(v) => setDraft({
|
||||||
...draft,
|
...draft,
|
||||||
status: v || undefined,
|
status: v || undefined,
|
||||||
@@ -1199,23 +1213,17 @@ function ConfigEditor({ cfg, updateCfg, onSync }: { cfg: ScheduleConfig; updateC
|
|||||||
<ArrayEditor
|
<ArrayEditor
|
||||||
items={normalized.statuses}
|
items={normalized.statuses}
|
||||||
onChange={(statuses) => updateCfg({ ...normalized, statuses })}
|
onChange={(statuses) => updateCfg({ ...normalized, statuses })}
|
||||||
createItem={() => ({ key: `status_${Date.now()}`, label: "", description: "" })}
|
createItem={() => ({ key: "", label: "", description: "" })}
|
||||||
label="Статусы групп"
|
label="Статусы групп"
|
||||||
addLabel="Добавить статус"
|
addLabel="Добавить статус"
|
||||||
collapsible
|
collapsible
|
||||||
getItemTitle={(item) => item.label || "Новый статус"}
|
getItemTitle={(item) => item.label || "Новый статус"}
|
||||||
renderItem={(item, _i, update) => (
|
renderItem={(item, _i, update) => (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<InputField
|
|
||||||
label="Ключ (латиницей)"
|
|
||||||
value={item.key}
|
|
||||||
onChange={(v) => update({ ...item, key: v.replace(/[^a-zA-Z0-9_]/g, "") })}
|
|
||||||
placeholder="например: intensive"
|
|
||||||
/>
|
|
||||||
<InputField
|
<InputField
|
||||||
label="Название"
|
label="Название"
|
||||||
value={item.label}
|
value={item.label}
|
||||||
onChange={(v) => update({ ...item, label: v })}
|
onChange={(v) => update({ ...item, label: v, key: v.toLowerCase().replace(/[^a-zа-яё0-9]/gi, "_").replace(/_+/g, "_") })}
|
||||||
placeholder="Название статуса"
|
placeholder="Название статуса"
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
@@ -1262,7 +1270,12 @@ export default function ScheduleEditorPage() {
|
|||||||
|
|
||||||
adminFetch("/api/admin/sections/scheduleConfig")
|
adminFetch("/api/admin/sections/scheduleConfig")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((c: ScheduleConfig) => { if (c?.levels) setConfig(c); })
|
.then((c: Partial<ScheduleConfig>) => {
|
||||||
|
setConfig({
|
||||||
|
levels: c?.levels ?? DEFAULT_CONFIG.levels,
|
||||||
|
statuses: c?.statuses ?? DEFAULT_CONFIG.statuses,
|
||||||
|
});
|
||||||
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -192,19 +192,37 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe
|
|||||||
for (const cls of day.classes) {
|
for (const cls of day.classes) {
|
||||||
typeSet.add(cls.type);
|
typeSet.add(cls.type);
|
||||||
trainerSet.add(cls.trainer);
|
trainerSet.add(cls.trainer);
|
||||||
if (cls.status) statusSet.add(cls.status);
|
const clsStatus = cls.status || (cls.recruiting ? "recruiting" : cls.hasSlots ? "hasSlots" : "");
|
||||||
if (cls.hasSlots) statusSet.add("hasSlots");
|
if (clsStatus) statusSet.add(clsStatus);
|
||||||
if (cls.recruiting) statusSet.add("recruiting");
|
|
||||||
if (cls.level) levelSet.add(cls.level);
|
if (cls.level) levelSet.add(cls.level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Also include all configured statuses/levels so they appear in filters
|
||||||
|
if (scheduleConfig?.statuses) {
|
||||||
|
for (const s of scheduleConfig.statuses) if (s.key) statusSet.add(s.key);
|
||||||
|
}
|
||||||
|
if (scheduleConfig?.levels) {
|
||||||
|
for (const l of scheduleConfig.levels) if (l.value) levelSet.add(l.value);
|
||||||
|
}
|
||||||
|
// Order statuses by config order, then any extras from data
|
||||||
|
const configStatusOrder = (scheduleConfig?.statuses ?? []).map((s) => s.key).filter(Boolean);
|
||||||
|
const orderedStatuses = [
|
||||||
|
...configStatusOrder.filter((k) => statusSet.has(k)),
|
||||||
|
...Array.from(statusSet).filter((k) => !configStatusOrder.includes(k)),
|
||||||
|
];
|
||||||
|
// Order levels by config order
|
||||||
|
const configLevelOrder = (scheduleConfig?.levels ?? []).map((l) => l.value).filter(Boolean);
|
||||||
|
const orderedLevels = [
|
||||||
|
...configLevelOrder.filter((v) => levelSet.has(v)),
|
||||||
|
...Array.from(levelSet).filter((v) => !configLevelOrder.includes(v)),
|
||||||
|
];
|
||||||
return {
|
return {
|
||||||
types: Array.from(typeSet).sort(),
|
types: Array.from(typeSet).sort(),
|
||||||
availableStatuses: Array.from(statusSet),
|
availableStatuses: orderedStatuses,
|
||||||
levels: Array.from(levelSet).sort(),
|
levels: orderedLevels,
|
||||||
trainerNames: Array.from(trainerSet).sort(),
|
trainerNames: Array.from(trainerSet).sort(),
|
||||||
};
|
};
|
||||||
}, [activeDays]);
|
}, [activeDays, scheduleConfig]);
|
||||||
|
|
||||||
// Parse time range for filtering
|
// Parse time range for filtering
|
||||||
const activeTimeRange = isTimeFilterActive(filterTime)
|
const activeTimeRange = isTimeFilterActive(filterTime)
|
||||||
@@ -227,19 +245,17 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe
|
|||||||
.map((day) => ({
|
.map((day) => ({
|
||||||
...day,
|
...day,
|
||||||
classes: day.classes.filter(
|
classes: day.classes.filter(
|
||||||
(cls) =>
|
(cls) => {
|
||||||
(filterTrainerSet.size === 0 || filterTrainerSet.has(cls.trainer)) &&
|
const clsStatus = cls.status || (cls.recruiting ? "recruiting" : cls.hasSlots ? "hasSlots" : "");
|
||||||
|
return (filterTrainerSet.size === 0 || filterTrainerSet.has(cls.trainer)) &&
|
||||||
(filterTypes.size === 0 || filterTypes.has(cls.type)) &&
|
(filterTypes.size === 0 || filterTypes.has(cls.type)) &&
|
||||||
(filterStatusSet.size === 0 ||
|
(filterStatusSet.size === 0 || (clsStatus && filterStatusSet.has(clsStatus))) &&
|
||||||
(cls.status && filterStatusSet.has(cls.status as StatusTag)) ||
|
|
||||||
(filterStatusSet.has("hasSlots" as StatusTag) && cls.hasSlots) ||
|
|
||||||
(filterStatusSet.has("recruiting" as StatusTag) && cls.recruiting)) &&
|
|
||||||
(!filterLevel || cls.level === filterLevel) &&
|
(!filterLevel || cls.level === filterLevel) &&
|
||||||
(!activeTimeRange || (() => {
|
(!activeTimeRange || (() => {
|
||||||
const m = startTimeMinutes(cls.time);
|
const m = startTimeMinutes(cls.time);
|
||||||
return m >= activeTimeRange[0] && m < activeTimeRange[1];
|
return m >= activeTimeRange[0] && m < activeTimeRange[1];
|
||||||
})())
|
})());
|
||||||
),
|
}),
|
||||||
}))
|
}))
|
||||||
.filter((day) => day.classes.length > 0);
|
.filter((day) => day.classes.length > 0);
|
||||||
}, [activeDays, filterTrainerSet, filterTypes, filterStatusSet, filterLevel, filterTime, activeTimeRange, filterDaySet]);
|
}, [activeDays, filterTrainerSet, filterTypes, filterStatusSet, filterLevel, filterTime, activeTimeRange, filterDaySet]);
|
||||||
@@ -443,6 +459,7 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe
|
|||||||
showLocation={isAllMode}
|
showLocation={isAllMode}
|
||||||
onBook={(v) => dispatch({ type: "SET_BOOKING", value: v })}
|
onBook={(v) => dispatch({ type: "SET_BOOKING", value: v })}
|
||||||
trainerPhotos={trainerPhotos}
|
trainerPhotos={trainerPhotos}
|
||||||
|
scheduleConfig={scheduleConfig}
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ function ClassRow({
|
|||||||
набор
|
набор
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && (
|
||||||
|
<span className="shrink-0 rounded-full bg-gold/15 border border-gold/25 px-2 py-0.5 text-[10px] font-semibold text-gold">
|
||||||
|
{cls.status}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleFilterTrainer(cls.trainer)}
|
onClick={() => toggleFilterTrainer(cls.trainer)}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { useMemo } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { User, Calendar } from "lucide-react";
|
import { User, Calendar } from "lucide-react";
|
||||||
import { GroupCard } from "@/components/ui/GroupCard";
|
import { GroupCard } from "@/components/ui/GroupCard";
|
||||||
|
import { findStatusConfig } from "./constants";
|
||||||
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
|
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
|
||||||
|
import type { SiteContent } from "@/types/content";
|
||||||
|
|
||||||
interface ScheduleGroup {
|
interface ScheduleGroup {
|
||||||
trainer: string;
|
trainer: string;
|
||||||
@@ -12,6 +14,7 @@ interface ScheduleGroup {
|
|||||||
level?: string;
|
level?: string;
|
||||||
hasSlots: boolean;
|
hasSlots: boolean;
|
||||||
recruiting: boolean;
|
recruiting: boolean;
|
||||||
|
status?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
locationAddress?: string;
|
locationAddress?: string;
|
||||||
slots: { day: string; dayShort: string; time: string }[];
|
slots: { day: string; dayShort: string; time: string }[];
|
||||||
@@ -33,6 +36,7 @@ function buildGroups(days: ScheduleDayMerged[]): ScheduleGroup[] {
|
|||||||
existing.slots.push({ day: day.day, dayShort: day.dayShort, time: cls.time });
|
existing.slots.push({ day: day.day, dayShort: day.dayShort, time: cls.time });
|
||||||
if (cls.hasSlots) existing.hasSlots = true;
|
if (cls.hasSlots) existing.hasSlots = true;
|
||||||
if (cls.recruiting) existing.recruiting = true;
|
if (cls.recruiting) existing.recruiting = true;
|
||||||
|
if (cls.status && !existing.status) existing.status = cls.status;
|
||||||
if (cls.level && !existing.level) existing.level = cls.level;
|
if (cls.level && !existing.level) existing.level = cls.level;
|
||||||
} else {
|
} else {
|
||||||
map.set(key, {
|
map.set(key, {
|
||||||
@@ -41,6 +45,7 @@ function buildGroups(days: ScheduleDayMerged[]): ScheduleGroup[] {
|
|||||||
level: cls.level,
|
level: cls.level,
|
||||||
hasSlots: !!cls.hasSlots,
|
hasSlots: !!cls.hasSlots,
|
||||||
recruiting: !!cls.recruiting,
|
recruiting: !!cls.recruiting,
|
||||||
|
status: cls.status,
|
||||||
location: cls.locationName,
|
location: cls.locationName,
|
||||||
locationAddress: cls.locationAddress,
|
locationAddress: cls.locationAddress,
|
||||||
slots: [{ day: day.day, dayShort: day.dayShort, time: cls.time }],
|
slots: [{ day: day.day, dayShort: day.dayShort, time: cls.time }],
|
||||||
@@ -126,6 +131,7 @@ interface GroupViewProps {
|
|||||||
showLocation?: boolean;
|
showLocation?: boolean;
|
||||||
onBook?: (groupInfo: string) => void;
|
onBook?: (groupInfo: string) => void;
|
||||||
trainerPhotos?: Record<string, string>;
|
trainerPhotos?: Record<string, string>;
|
||||||
|
scheduleConfig?: SiteContent["scheduleConfig"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const WEEKDAY_NAMES = ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"];
|
const WEEKDAY_NAMES = ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"];
|
||||||
@@ -140,6 +146,7 @@ export function GroupView({
|
|||||||
showLocation,
|
showLocation,
|
||||||
onBook,
|
onBook,
|
||||||
trainerPhotos = {},
|
trainerPhotos = {},
|
||||||
|
scheduleConfig,
|
||||||
}: GroupViewProps) {
|
}: GroupViewProps) {
|
||||||
const groups = buildGroups(filteredDays);
|
const groups = buildGroups(filteredDays);
|
||||||
const byTrainer = groupByTrainer(groups);
|
const byTrainer = groupByTrainer(groups);
|
||||||
@@ -234,6 +241,8 @@ export function GroupView({
|
|||||||
level={group.level}
|
level={group.level}
|
||||||
recruiting={group.recruiting}
|
recruiting={group.recruiting}
|
||||||
hasSlots={group.hasSlots}
|
hasSlots={group.hasSlots}
|
||||||
|
status={group.status}
|
||||||
|
statusLabel={findStatusConfig(scheduleConfig?.statuses, group.status ?? "")?.label}
|
||||||
address={group.locationAddress}
|
address={group.locationAddress}
|
||||||
location={group.location}
|
location={group.location}
|
||||||
merged={merged}
|
merged={merged}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ function ClassRow({
|
|||||||
набор
|
набор
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && (
|
||||||
|
<span className="shrink-0 rounded-full bg-gold/15 border border-gold/25 px-1.5 py-px text-[9px] font-semibold text-gold">
|
||||||
|
{cls.status}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{cls.level && (
|
{cls.level && (
|
||||||
<span className="shrink-0 rounded-full bg-rose-500/15 border border-rose-500/25 px-1.5 py-px text-[9px] font-semibold text-rose-600 dark:text-rose-400">
|
<span className="shrink-0 rounded-full bg-rose-500/15 border border-rose-500/25 px-1.5 py-px text-[9px] font-semibold text-rose-600 dark:text-rose-400">
|
||||||
{cls.level}
|
{cls.level}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { X, SlidersHorizontal } from "lucide-react";
|
|||||||
import {
|
import {
|
||||||
pillBase,
|
pillBase,
|
||||||
isTimeFilterActive,
|
isTimeFilterActive,
|
||||||
|
findStatusConfig,
|
||||||
type StatusTag,
|
type StatusTag,
|
||||||
type TimeFilter,
|
type TimeFilter,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
@@ -160,7 +161,7 @@ export function ScheduleFilters({
|
|||||||
{/* Desktop — card layout */}
|
{/* Desktop — card layout */}
|
||||||
<div className="hidden sm:block space-y-2">
|
<div className="hidden sm:block space-y-2">
|
||||||
{availableStatuses.map((statusKey) => {
|
{availableStatuses.map((statusKey) => {
|
||||||
const cfg = scheduleConfig?.statuses?.find((s) => s.key === statusKey);
|
const cfg = findStatusConfig(scheduleConfig?.statuses, statusKey);
|
||||||
const label = cfg?.label || statusKey;
|
const label = cfg?.label || statusKey;
|
||||||
const desc = cfg?.description;
|
const desc = cfg?.description;
|
||||||
const active = filterStatusSet.has(statusKey);
|
const active = filterStatusSet.has(statusKey);
|
||||||
@@ -199,7 +200,7 @@ export function ScheduleFilters({
|
|||||||
{/* Mobile — compact pills */}
|
{/* Mobile — compact pills */}
|
||||||
<div className="flex flex-wrap gap-2 sm:hidden">
|
<div className="flex flex-wrap gap-2 sm:hidden">
|
||||||
{availableStatuses.map((statusKey) => {
|
{availableStatuses.map((statusKey) => {
|
||||||
const cfg = scheduleConfig?.statuses?.find((s) => s.key === statusKey);
|
const cfg = findStatusConfig(scheduleConfig?.statuses, statusKey);
|
||||||
const label = cfg?.label || statusKey;
|
const label = cfg?.label || statusKey;
|
||||||
const active = filterStatusSet.has(statusKey);
|
const active = filterStatusSet.has(statusKey);
|
||||||
return (
|
return (
|
||||||
@@ -228,7 +229,7 @@ export function ScheduleFilters({
|
|||||||
const desc = scheduleConfig?.levels?.find((l) => l.value === level)?.description;
|
const desc = scheduleConfig?.levels?.find((l) => l.value === level)?.description;
|
||||||
const active = filterLevel === level;
|
const active = filterLevel === level;
|
||||||
return (
|
return (
|
||||||
<span key={level} className="relative group inline-flex items-center gap-1">
|
<span key={level} className="relative inline-flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterLevel(active ? null : level)}
|
onClick={() => setFilterLevel(active ? null : level)}
|
||||||
className={`rounded-xl px-4 py-2 text-xs font-semibold transition-all cursor-pointer border ${
|
className={`rounded-xl px-4 py-2 text-xs font-semibold transition-all cursor-pointer border ${
|
||||||
|
|||||||
@@ -63,8 +63,19 @@ export function buildTypeDots(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type StatusTag = string;
|
export type StatusTag = string;
|
||||||
/** @deprecated Use Set<StatusTag> instead */
|
|
||||||
export type StatusFilter = "all" | "hasSlots" | "recruiting";
|
/** Find a status config entry by key, matching by key, label, or derived key */
|
||||||
|
export function findStatusConfig(
|
||||||
|
statuses: { key: string; label: string; description: string }[] | undefined,
|
||||||
|
statusKey: string,
|
||||||
|
): { key: string; label: string; description: string } | undefined {
|
||||||
|
if (!statuses) return undefined;
|
||||||
|
return statuses.find((s) =>
|
||||||
|
s.key === statusKey ||
|
||||||
|
s.label === statusKey ||
|
||||||
|
s.label.toLowerCase().replace(/[^a-zа-яё0-9]/gi, "_").replace(/_+/g, "_") === statusKey
|
||||||
|
);
|
||||||
|
}
|
||||||
export interface TimeFilter {
|
export interface TimeFilter {
|
||||||
from: string; // "HH:MM" or ""
|
from: string; // "HH:MM" or ""
|
||||||
to: string; // "HH:MM" or ""
|
to: string; // "HH:MM" or ""
|
||||||
@@ -76,12 +87,6 @@ export function isTimeFilterActive(t: TimeFilter): boolean {
|
|||||||
return t.from !== "" || t.to !== "";
|
return t.from !== "" || t.to !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TIME_PRESETS: { label: string; from: string; to: string }[] = [
|
|
||||||
{ label: "Утро", from: "06:00", to: "12:00" },
|
|
||||||
{ label: "День", from: "12:00", to: "18:00" },
|
|
||||||
{ label: "Вечер", from: "18:00", to: "23:00" },
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Parse start time from "HH:MM–HH:MM" to minutes since midnight */
|
/** Parse start time from "HH:MM–HH:MM" to minutes since midnight */
|
||||||
export function startTimeMinutes(time: string): number {
|
export function startTimeMinutes(time: string): number {
|
||||||
const start = time.split("–")[0]?.trim() ?? "";
|
const start = time.split("–")[0]?.trim() ?? "";
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface GroupCardProps {
|
|||||||
level?: string;
|
level?: string;
|
||||||
recruiting?: boolean;
|
recruiting?: boolean;
|
||||||
hasSlots?: boolean;
|
hasSlots?: boolean;
|
||||||
|
status?: string;
|
||||||
|
statusLabel?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
merged: GroupCardSlot[];
|
merged: GroupCardSlot[];
|
||||||
@@ -32,6 +34,8 @@ export function GroupCard({
|
|||||||
level,
|
level,
|
||||||
recruiting,
|
recruiting,
|
||||||
hasSlots,
|
hasSlots,
|
||||||
|
status,
|
||||||
|
statusLabel,
|
||||||
address,
|
address,
|
||||||
location,
|
location,
|
||||||
merged,
|
merged,
|
||||||
@@ -85,6 +89,11 @@ export function GroupCard({
|
|||||||
набор
|
набор
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{status && status !== "hasSlots" && status !== "recruiting" && (
|
||||||
|
<span className={`rounded-full bg-gold/15 border border-gold/25 ${badgeSize} font-semibold text-gold`}>
|
||||||
|
{statusLabel || status}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{showLocation && (address || location) && (
|
{showLocation && (address || location) && (
|
||||||
<span className={`inline-flex items-center gap-1 rounded-full bg-white/[0.04] border border-white/[0.08] ${locSize} font-medium text-white/40`}>
|
<span className={`inline-flex items-center gap-1 rounded-full bg-white/[0.04] border border-white/[0.08] ${locSize} font-medium text-white/40`}>
|
||||||
<MapPin size={locIcon} />
|
<MapPin size={locIcon} />
|
||||||
|
|||||||
Reference in New Issue
Block a user