initial commit
This commit is contained in:
commit
39fdd43d8e
127 files changed
+13919
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { WEEKDAY_LABELS, type WorkoutSummary } from "../api";
|
||||
|
||||
function daysInMonth(year: number, month: number) {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
export function AppCalendar({
|
||||
value,
|
||||
workouts = [],
|
||||
onPick,
|
||||
}: {
|
||||
value: string;
|
||||
workouts?: WorkoutSummary[];
|
||||
onPick: (iso: string) => void;
|
||||
}) {
|
||||
const initial = new Date(value + "T12:00:00");
|
||||
const [year, setYear] = useState(initial.getFullYear());
|
||||
const [month, setMonth] = useState(initial.getMonth() + 1);
|
||||
const byDate = useMemo(() => {
|
||||
const map = new Map<string, WorkoutSummary>();
|
||||
for (const w of workouts) map.set(w.scheduled_date, w);
|
||||
return map;
|
||||
}, [workouts]);
|
||||
const firstWeekday = (new Date(year, month - 1, 1).getDay() + 6) % 7;
|
||||
const totalDays = daysInMonth(year, month);
|
||||
const today = new Date();
|
||||
const todayIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
|
||||
const shift = (delta: number) => {
|
||||
const d = new Date(year, month - 1 + delta, 1);
|
||||
setYear(d.getFullYear());
|
||||
setMonth(d.getMonth() + 1);
|
||||
};
|
||||
const statusClass = (s: string) => {
|
||||
if (s === "completed") return "done";
|
||||
if (s === "in_progress" || s === "paused") return "active";
|
||||
if (s === "abandoned") return "missed";
|
||||
return "planned";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="calendar move-calendar">
|
||||
<div className="cal-nav">
|
||||
<button type="button" className="btn ghost" onClick={() => shift(-1)} aria-label="Предыдущий месяц">
|
||||
‹
|
||||
</button>
|
||||
<h2>{new Date(year, month - 1, 1).toLocaleString("ru-RU", { month: "long", year: "numeric" })}</h2>
|
||||
<button type="button" className="btn ghost" onClick={() => shift(1)} aria-label="Следующий месяц">
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
<div className="cal-grid head">
|
||||
{WEEKDAY_LABELS.map((d) => (
|
||||
<div key={d}>{d}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="cal-grid">
|
||||
{Array.from({ length: firstWeekday }).map((_, i) => (
|
||||
<div key={`e${i}`} className="cal-cell empty" />
|
||||
))}
|
||||
{Array.from({ length: totalDays }).map((_, i) => {
|
||||
const day = i + 1;
|
||||
const key = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
const w = byDate.get(key);
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`cal-cell ${key === todayIso ? "today" : ""} ${key === value ? "picked" : ""} ${w ? statusClass(w.status) : ""}`}
|
||||
onClick={() => onPick(key)}
|
||||
>
|
||||
<span>{day}</span>
|
||||
{w && <i className="dot" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user