800dee31b2
- Added boto3 as a dependency in pyproject.toml and uv.lock. - Introduced multiple new exercise images in various formats (jpg, webp, avif, png). - Added new machine images to enhance the workout assets library.
111 lines
4.6 KiB
TypeScript
111 lines
4.6 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
|
||
import type { ActivityCategory, CatalogEntity, CatalogKind, Workout } from "../../types";
|
||
import { categoryLabels, measurementLabels } from "../catalog/meta";
|
||
import { useCatalog } from "../catalog/hooks";
|
||
|
||
const categoryTabs: Array<ActivityCategory | "all" | "arms"> = ["all", "chest", "back", "legs", "shoulders", "arms", "core", "cardio"];
|
||
|
||
function categoryTabLabel(category: ActivityCategory | "all" | "arms") {
|
||
if (category === "all") return "Все";
|
||
if (category === "arms") return "Руки";
|
||
return categoryLabels[category];
|
||
}
|
||
|
||
export function AddExerciseDrawer({
|
||
open,
|
||
token,
|
||
workout,
|
||
onClose,
|
||
onAdd,
|
||
pending,
|
||
}: {
|
||
open: boolean;
|
||
token: string;
|
||
workout: Workout;
|
||
onClose: () => void;
|
||
onAdd: (entity: CatalogEntity, kind: CatalogKind, closeAfter: boolean) => void;
|
||
pending?: boolean;
|
||
}) {
|
||
const [kind, setKind] = useState<CatalogKind>("exercise");
|
||
const [search, setSearch] = useState("");
|
||
const [category, setCategory] = useState<ActivityCategory | "all" | "arms">("all");
|
||
const { exercises, machines } = useCatalog(token);
|
||
|
||
const addedIds = useMemo(() => {
|
||
const ids = new Set<string>();
|
||
workout.items.forEach((item) => {
|
||
if (item.activity_source_id) ids.add(item.activity_source_id);
|
||
if (item.exercise_id) ids.add(item.exercise_id);
|
||
if (item.equipment_id) ids.add(item.equipment_id);
|
||
});
|
||
return ids;
|
||
}, [workout.items]);
|
||
|
||
const list = useMemo(() => {
|
||
const source = kind === "exercise" ? exercises.data ?? [] : machines.data ?? [];
|
||
const needle = search.trim().toLowerCase();
|
||
return source.filter((entity) => {
|
||
if (category === "arms" && entity.category !== "biceps" && entity.category !== "triceps") return false;
|
||
if (category !== "all" && category !== "arms" && entity.category !== category) return false;
|
||
if (!needle) return true;
|
||
return `${entity.title} ${entity.description ?? ""}`.toLowerCase().includes(needle);
|
||
});
|
||
}, [category, kind, search, exercises.data, machines.data]);
|
||
|
||
if (!open) return null;
|
||
|
||
return (
|
||
<div className="drawer-scrim" onClick={onClose}>
|
||
<aside className="add-drawer" aria-label="Добавить в тренировку" onClick={(event) => event.stopPropagation()}>
|
||
<header>
|
||
<div>
|
||
<p className="eyebrow">Catalog drawer</p>
|
||
<h2>Добавить в тренировку</h2>
|
||
</div>
|
||
<button className="round-close" onClick={onClose} aria-label="Закрыть">×</button>
|
||
</header>
|
||
|
||
<input className="drawer-search" placeholder="Поиск упражнения или тренажера" value={search} onChange={(event) => setSearch(event.target.value)} />
|
||
<div className="segmented drawer-tabs">
|
||
<button className={kind === "exercise" ? "active" : ""} onClick={() => setKind("exercise")}>Упражнения</button>
|
||
<button className={kind === "machine" ? "active" : ""} onClick={() => setKind("machine")}>Тренажеры</button>
|
||
</div>
|
||
<div className="chip-row drawer-categories">
|
||
{categoryTabs.map((tab) => (
|
||
<button key={tab} className={category === tab ? "active" : ""} onClick={() => setCategory(tab)}>{categoryTabLabel(tab)}</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="drawer-list">
|
||
{list.map((entity) => {
|
||
const added = addedIds.has(entity.id);
|
||
return (
|
||
<article className={`drawer-pick ${added ? "already-added" : ""}`} key={`${kind}-${entity.id}`} onClick={() => onAdd(entity, kind, true)}>
|
||
{entity.image_s3_url ? <img src={entity.image_s3_url} alt="" /> : <span className="drawer-placeholder">{kind === "exercise" ? "EX" : "MC"}</span>}
|
||
<div>
|
||
<h3>{entity.title}</h3>
|
||
<p>{categoryLabels[entity.category]} · {measurementLabels[entity.measurement_type]}</p>
|
||
{added && <b>В тренировке</b>}
|
||
</div>
|
||
<button
|
||
className="plus-chip"
|
||
disabled={pending}
|
||
onClick={(event) => {
|
||
event.stopPropagation();
|
||
onAdd(entity, kind, false);
|
||
}}
|
||
aria-label="Добавить и оставить drawer открытым"
|
||
>
|
||
+
|
||
</button>
|
||
</article>
|
||
);
|
||
})}
|
||
{list.length === 0 && <p className="muted">Ничего не найдено.</p>}
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
);
|
||
}
|