feat: Implement active workout flow with status management
- Added `status`, `total_sets`, and `total_volume` fields to the Workout model. - Introduced `source_kind`, `title_snapshot`, and `image_s3_url_snapshot` fields to the WorkoutItem model. - Created endpoints for managing active workouts, including finishing and discarding workouts. - Updated workout creation to ensure only one active workout exists per user. - Implemented batch addition of workout sets and updates to workout set details. - Enhanced database schema with Alembic migrations to support new fields and constraints. - Added validation to ensure at least one field is provided for workout set updates. - Updated calorie estimation logic to reflect new workout set structure.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { CatalogEntity } from "../../types";
|
||||
|
||||
export function CatalogCard({ entity, action }: { entity: CatalogEntity; action?: ReactNode }) {
|
||||
return (
|
||||
<article className="card catalog-card">
|
||||
{entity.image_s3_url ? <img src={entity.image_s3_url} alt="" /> : <div className="image-placeholder">TW</div>}
|
||||
<div>
|
||||
<span className={entity.is_builtin ? "pill" : "pill user"}>{entity.is_builtin ? "стандартное" : "мое"}</span>
|
||||
<h3>{entity.name}</h3>
|
||||
<p>{entity.description || "Без описания"}</p>
|
||||
{action}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { FormEvent } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { api } from "../../api";
|
||||
import type { CatalogKind } from "../../types";
|
||||
import { CatalogCard } from "./CatalogCard";
|
||||
import { useCatalog } from "./hooks";
|
||||
|
||||
export function CatalogPage({ kind }: { kind: CatalogKind }) {
|
||||
const { auth } = useAuth();
|
||||
const token = auth.accessToken;
|
||||
const queryClient = useQueryClient();
|
||||
const { equipment, exercises } = useCatalog(token);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const list = useMemo(() => (kind === "exercise" ? exercises.data : equipment.data) ?? [], [kind, exercises.data, equipment.data]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const image = file ? await api.uploadImage(token, kind, file) : {};
|
||||
const payload = { name, description, ...image };
|
||||
return kind === "equipment" ? api.createEquipment(token, payload) : api.createExercise(token, payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setFile(null);
|
||||
void queryClient.invalidateQueries({ queryKey: ["catalog", kind === "equipment" ? "equipment" : "exercises"] });
|
||||
},
|
||||
});
|
||||
|
||||
function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
createMutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="stack">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<p className="eyebrow">Catalog</p>
|
||||
<h2>{kind === "exercise" ? "Упражнения" : "Тренажеры"}</h2>
|
||||
</div>
|
||||
<div className="segmented">
|
||||
<Link to="/catalog/exercises" activeProps={{ className: "active" }}>Упражнения</Link>
|
||||
<Link to="/catalog/equipment" activeProps={{ className: "active" }}>Тренажеры</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="card form-grid" onSubmit={submit}>
|
||||
<label>
|
||||
Название
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} required />
|
||||
</label>
|
||||
<label>
|
||||
Описание
|
||||
<input value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Картинка
|
||||
<input type="file" accept="image/png,image/jpeg,image/webp" onChange={(event) => setFile(event.target.files?.[0] ?? null)} />
|
||||
</label>
|
||||
<button className="primary" disabled={createMutation.isPending}>{createMutation.isPending ? "Сохранение..." : "Добавить"}</button>
|
||||
{createMutation.error && <p className="error">{createMutation.error.message}</p>}
|
||||
</form>
|
||||
|
||||
<div className="catalog-grid">
|
||||
{list.map((entity) => <CatalogCard entity={entity} key={entity.id} />)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { api } from "../../api";
|
||||
|
||||
export function useCatalog(token: string) {
|
||||
const equipment = useQuery({ queryKey: ["catalog", "equipment"], queryFn: () => api.equipment(token) });
|
||||
const exercises = useQuery({ queryKey: ["catalog", "exercises"], queryFn: () => api.exercises(token) });
|
||||
return { equipment, exercises };
|
||||
}
|
||||
Reference in New Issue
Block a user