6447db3008
Build and Deploy / Build & Push (push) Successful in 1m7s
The home page no longer reads ?week=YYYY-MM-DD from the URL. Selected week lives in the tcWeek cookie, set via a server action that revalidates the home page so the next render reflects it. The URL stays at "/" regardless of which week the user is viewing. WeekNav prev/next/today buttons (and the arrow-key bindings) call the server action directly — no router.refresh dance, no client-side cookie write. BackToCalendarLink drops its localStorage-based href reconstruction and just links to "/" since the cookie already remembers the right week across navigations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
165 lines
4.7 KiB
TypeScript
165 lines
4.7 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect } from "react";
|
||
import { setWeek, clearWeek } from "@/app/actions/week";
|
||
|
||
interface WeekNavProps {
|
||
weekStart: string; // YYYY-MM-DD (Sunday)
|
||
weekDates: string[]; // 7 dates YYYY-MM-DD
|
||
isCurrentWeek: boolean;
|
||
}
|
||
|
||
const MONTHS = [
|
||
"Jan","Feb","Mar","Apr","May","Jun",
|
||
"Jul","Aug","Sep","Oct","Nov","Dec",
|
||
];
|
||
|
||
function formatLabel(dates: string[]): string {
|
||
const s = new Date(dates[0] + "T00:00:00");
|
||
const e = new Date(dates[6] + "T00:00:00");
|
||
if (s.getFullYear() === e.getFullYear() && s.getMonth() === e.getMonth()) {
|
||
return `${MONTHS[s.getMonth()]} ${s.getDate()}–${e.getDate()}, ${s.getFullYear()}`;
|
||
}
|
||
const startStr = `${MONTHS[s.getMonth()]} ${s.getDate()}`;
|
||
const endStr = `${MONTHS[e.getMonth()]} ${e.getDate()}, ${e.getFullYear()}`;
|
||
return `${startStr} – ${endStr}`;
|
||
}
|
||
|
||
function formatDateLocal(d: Date): string {
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||
const day = String(d.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${day}`;
|
||
}
|
||
|
||
function shiftWeek(weekStart: string, delta: number): string {
|
||
const d = new Date(weekStart + "T00:00:00");
|
||
d.setDate(d.getDate() + delta * 7);
|
||
return formatDateLocal(d);
|
||
}
|
||
|
||
export function WeekNav({ weekStart, weekDates, isCurrentWeek }: WeekNavProps) {
|
||
const nav = (delta: number) => {
|
||
void setWeek(shiftWeek(weekStart, delta));
|
||
};
|
||
const jumpToToday = () => {
|
||
void clearWeek();
|
||
};
|
||
|
||
useEffect(() => {
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||
if (e.key === "ArrowLeft") nav(-1);
|
||
if (e.key === "ArrowRight") nav(1);
|
||
};
|
||
window.addEventListener("keydown", onKey);
|
||
return () => window.removeEventListener("keydown", onKey);
|
||
}, [weekStart]);
|
||
|
||
return (
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||
<button
|
||
onClick={() => nav(-1)}
|
||
aria-label="Previous week"
|
||
style={navBtnStyle}
|
||
onMouseOver={(e) => Object.assign((e.target as HTMLElement).style, navBtnHover)}
|
||
onMouseOut={(e) => Object.assign((e.target as HTMLElement).style, navBtnStyle)}
|
||
>
|
||
←
|
||
</button>
|
||
|
||
{!isCurrentWeek && (
|
||
<button
|
||
onClick={jumpToToday}
|
||
aria-label="Jump to current week"
|
||
style={todayBtnStyle}
|
||
onMouseOver={(e) => Object.assign((e.target as HTMLElement).style, todayBtnHover)}
|
||
onMouseOut={(e) => Object.assign((e.target as HTMLElement).style, todayBtnStyle)}
|
||
>
|
||
Today
|
||
</button>
|
||
)}
|
||
|
||
<span style={{
|
||
fontSize: "1rem",
|
||
fontWeight: 600,
|
||
color: "var(--color-text)",
|
||
minWidth: 140,
|
||
textAlign: "center",
|
||
letterSpacing: "-0.01em",
|
||
fontVariantNumeric: "tabular-nums",
|
||
}}>
|
||
{formatLabel(weekDates)}
|
||
</span>
|
||
|
||
<button
|
||
onClick={() => nav(1)}
|
||
aria-label="Next week"
|
||
style={navBtnStyle}
|
||
onMouseOver={(e) => Object.assign((e.target as HTMLElement).style, navBtnHover)}
|
||
onMouseOut={(e) => Object.assign((e.target as HTMLElement).style, navBtnStyle)}
|
||
>
|
||
→
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const navBtnStyle: React.CSSProperties = {
|
||
padding: "10px 16px",
|
||
borderRadius: 8,
|
||
border: "1px solid var(--color-border)",
|
||
background: "var(--color-surface)",
|
||
color: "var(--color-text-muted)",
|
||
cursor: "pointer",
|
||
fontSize: "1rem",
|
||
lineHeight: 1,
|
||
transition: "background 150ms ease, border-color 150ms ease, color 150ms ease",
|
||
minWidth: 44,
|
||
textAlign: "center",
|
||
};
|
||
|
||
const navBtnHover: React.CSSProperties = {
|
||
padding: "10px 16px",
|
||
borderRadius: 8,
|
||
border: "1px solid var(--color-text-dim)",
|
||
background: "var(--color-surface-2)",
|
||
color: "var(--color-text-secondary)",
|
||
cursor: "pointer",
|
||
fontSize: "1rem",
|
||
lineHeight: 1,
|
||
transition: "background 150ms ease, border-color 150ms ease, color 150ms ease",
|
||
minWidth: 44,
|
||
textAlign: "center",
|
||
};
|
||
|
||
const todayBtnStyle: React.CSSProperties = {
|
||
padding: "5px 12px",
|
||
borderRadius: 6,
|
||
border: "1px solid var(--color-accent-muted)",
|
||
background: "transparent",
|
||
color: "var(--color-accent)",
|
||
cursor: "pointer",
|
||
fontSize: "0.75rem",
|
||
fontWeight: 600,
|
||
letterSpacing: "0.04em",
|
||
textTransform: "uppercase",
|
||
lineHeight: 1,
|
||
transition: "background 150ms ease, color 150ms ease",
|
||
};
|
||
|
||
const todayBtnHover: React.CSSProperties = {
|
||
padding: "5px 12px",
|
||
borderRadius: 6,
|
||
border: "1px solid var(--color-accent-muted)",
|
||
background: "var(--color-accent-muted)",
|
||
color: "var(--color-accent-text)",
|
||
cursor: "pointer",
|
||
fontSize: "0.75rem",
|
||
fontWeight: 600,
|
||
letterSpacing: "0.04em",
|
||
textTransform: "uppercase",
|
||
lineHeight: 1,
|
||
transition: "background 150ms ease, color 150ms ease",
|
||
};
|