Files
workout_watcher/frontend/src/features/workout/AddExerciseDrawer.tsx
T
Artem Kashaev 800dee31b2 Add boto3 dependency and update exercise/machine assets
- 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.
2026-05-29 15:50:33 +05:00

111 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}