From 31f4494ff2f7f5f8746814dc145ddab16303c3dd Mon Sep 17 00:00:00 2001 From: Ruby Date: Mon, 2 Mar 2026 16:38:27 +0800 Subject: [PATCH 1/5] feat: enable batch toggling in my schedule --- app/page.tsx | 103 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 18 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index bf62776..4ad975f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -84,17 +84,57 @@ const INITIAL_MEMBERS: Member[] = [ // ─── Schedule Grid Component ────────────────────────────────────────────────── +type DragState = { + startDay: number; + startHourIdx: number; + curDay: number; + curHourIdx: number; + filling: boolean; // true = turning slots ON, false = turning OFF +}; + function ScheduleGrid({ availability, - onToggle, + onBatchToggle, emerald = false, }: { availability: TimeSlot[]; - onToggle?: (day: number, hour: number) => void; + onBatchToggle?: (slots: TimeSlot[], fill: boolean) => void; emerald?: boolean; }) { + const [drag, setDrag] = useState(null); + const dragging = useRef(false); + + // Commit the selection when mouse is released anywhere + useEffect(() => { + function handleMouseUp() { + if (!dragging.current || !drag) return; + const d0 = Math.min(drag.startDay, drag.curDay); + const d1 = Math.max(drag.startDay, drag.curDay); + const h0 = Math.min(drag.startHourIdx, drag.curHourIdx); + const h1 = Math.max(drag.startHourIdx, drag.curHourIdx); + const selected: TimeSlot[] = []; + for (let d = d0; d <= d1; d++) + for (let hi = h0; hi <= h1; hi++) + selected.push(slot(d, HOURS[hi])); + onBatchToggle?.(selected, drag.filling); + dragging.current = false; + setDrag(null); + } + document.addEventListener("mouseup", handleMouseUp); + return () => document.removeEventListener("mouseup", handleMouseUp); + }, [drag, onBatchToggle]); + + function inDragRect(d: number, hi: number): boolean { + if (!drag) return false; + const d0 = Math.min(drag.startDay, drag.curDay); + const d1 = Math.max(drag.startDay, drag.curDay); + const h0 = Math.min(drag.startHourIdx, drag.curHourIdx); + const h1 = Math.max(drag.startHourIdx, drag.curHourIdx); + return d >= d0 && d <= d1 && hi >= h0 && hi <= h1; + } + return ( -
+
@@ -107,7 +147,7 @@ function ScheduleGrid({ - {HOURS.map((h) => ( + {HOURS.map((h, hi) => ( ); @@ -170,17 +238,16 @@ export default function MeetFlow() { ).map((h) => slot(d, h)) ); - function toggleMySlot(day: number, hour: number) { - const s = slot(day, hour); + function batchToggleMySlots(slots: TimeSlot[], fill: boolean) { setMembers((prev) => prev.map((m) => m.id !== "me" ? m : { ...m, - availability: m.availability.includes(s) - ? m.availability.filter((x) => x !== s) - : [...m.availability, s], + availability: fill + ? [...new Set([...m.availability, ...slots])] + : m.availability.filter((x) => !slots.includes(x)), } ) ); @@ -305,7 +372,7 @@ export default function MeetFlow() {

我的時間表

- 點擊格子來切換你的空閒時段 + 點擊或拖曳選取矩形範圍來批次切換空閒時段

@@ -318,7 +385,7 @@ export default function MeetFlow() { /> From 26df5ff83ba6b38a671dcd9ef68707985050226a Mon Sep 17 00:00:00 2001 From: hannah035 Date: Sun, 22 Mar 2026 10:38:11 +0800 Subject: [PATCH 2/5] feat: add delete member functionality --- app/page.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 4ad975f..ae9b553 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Input } from "@/components/ui/input"; -import { Plus, Users, Calendar, User, CalendarCheck } from "lucide-react"; +import { Plus, Users, Calendar, User, CalendarCheck, Trash2 } from "lucide-react"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -268,6 +268,14 @@ export default function MeetFlow() { setOpen(false); } + function deleteMember(id: string) { + if (id === "me") return; + if (window.confirm("確定要刪除這位成員嗎?")) { + setMembers((prev) => prev.filter((m) => m.id !== id)); + if (viewId === id) setViewId("me"); + } + } + return (
{/* ── Header ── */} @@ -356,10 +364,19 @@ export default function MeetFlow() { {m.availability.length} 個空閒時段

- {m.id === "me" && ( + {m.id === "me" ? ( + ) : ( + )} From 8acbc536ec190a9899db85bfddf2923335c6eae0 Mon Sep 17 00:00:00 2001 From: hannah035 Date: Sun, 22 Mar 2026 10:52:28 +0800 Subject: [PATCH 3/5] feat: add login flow and prioritize schedule view --- app/page.tsx | 91 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index ae9b553..992a8bf 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Input } from "@/components/ui/input"; -import { Plus, Users, Calendar, User, CalendarCheck, Trash2 } from "lucide-react"; +import { Plus, Users, Calendar, User, CalendarCheck } from "lucide-react"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -220,14 +220,71 @@ function Legend({ items }: { items: { color: string; label: string }[] }) { ); } +// ─── Login Screen ───────────────────────────────────────────────────────────── + +function LoginScreen({ onJoin }: { onJoin: (name: string) => void }) { + const [name, setName] = useState(""); + + return ( +
+ + +
+
+ +
+
+ 歡迎來到 MeetFlow +

+ 請輸入您的名稱以開始安排會議時間 +

+
+ +
+ setName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && name.trim() && onJoin(name)} + autoFocus + /> + +
+
+
+
+ ); +} + // ─── Main App ───────────────────────────────────────────────────────────────── export default function MeetFlow() { + const [loggedIn, setLoggedIn] = useState(false); const [members, setMembers] = useState(INITIAL_MEMBERS); const [newName, setNewName] = useState(""); const [open, setOpen] = useState(false); const [viewId, setViewId] = useState("xiao-liang"); + if (!loggedIn) { + return ( + { + setMembers((prev) => + prev.map((m) => (m.id === "me" ? { ...m, name } : m)) + ); + setLoggedIn(true); + }} + /> + ); + } + const me = members.find((m) => m.id === "me")!; const others = members.filter((m) => m.id !== "me"); const viewing = members.find((m) => m.id === viewId) ?? others[0]; @@ -268,14 +325,6 @@ export default function MeetFlow() { setOpen(false); } - function deleteMember(id: string) { - if (id === "me") return; - if (window.confirm("確定要刪除這位成員嗎?")) { - setMembers((prev) => prev.filter((m) => m.id !== id)); - if (viewId === id) setViewId("me"); - } - } - return (
{/* ── Header ── */} @@ -286,21 +335,24 @@ export default function MeetFlow() { Beta +
+ Hi, {me.name} +
{/* ── Main ── */}
- + - - - 成員 - 我的時間表 + + + 成員 + 查看成員 @@ -364,19 +416,10 @@ export default function MeetFlow() { {m.availability.length} 個空閒時段

- {m.id === "me" ? ( + {m.id === "me" && ( - ) : ( - )} From 83ec170395b1b1a55fbde37a5b792eecf2406ab5 Mon Sep 17 00:00:00 2001 From: hannah035 Date: Sun, 22 Mar 2026 11:00:09 +0800 Subject: [PATCH 4/5] feat: add multi-week support --- app/page.tsx | 70 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 992a8bf..5ce4ff9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -31,6 +31,8 @@ type Member = { const DAYS = ["週一", "週二", "週三", "週四", "週五"]; const HOURS = [9, 10, 11, 12, 13, 14, 15, 16, 17]; +const WEEKS = ["本週", "下週", "下下週", "第四週"]; + const COLORS = [ "bg-orange-500", "bg-pink-500", @@ -41,7 +43,7 @@ const COLORS = [ "bg-cyan-500", ]; -const slot = (day: number, hour: number): TimeSlot => `${day}-${hour}`; +const slot = (week: number, day: number, hour: number): TimeSlot => `${week}-${day}-${hour}`; // ─── Fake initial data ──────────────────────────────────────────────────────── @@ -52,11 +54,11 @@ const INITIAL_MEMBERS: Member[] = [ name: "我", color: "bg-blue-500", availability: [ - slot(0, 9), slot(0, 10), slot(0, 11), // Mon 9–12 - slot(0, 14), slot(0, 15), slot(0, 16), // Mon 14–17 - slot(2, 9), slot(2, 10), slot(2, 11), // Wed 9–12(共同) - slot(3, 14), slot(3, 15), slot(3, 16), // Thu 14–17 - slot(4, 9), slot(4, 10), // Fri 9–11 + slot(0, 0, 9), slot(0, 0, 10), slot(0, 0, 11), // Mon 9–12 + slot(0, 0, 14), slot(0, 0, 15), slot(0, 0, 16), // Mon 14–17 + slot(0, 2, 9), slot(0, 2, 10), slot(0, 2, 11), // Wed 9–12(共同) + slot(0, 3, 14), slot(0, 3, 15), slot(0, 3, 16), // Thu 14–17 + slot(0, 4, 9), slot(0, 4, 10), // Fri 9–11 ], }, { @@ -64,10 +66,10 @@ const INITIAL_MEMBERS: Member[] = [ name: "小梁", color: "bg-green-500", availability: [ - slot(0, 9), slot(0, 10), slot(0, 11), // Mon 9–12 - slot(2, 9), slot(2, 10), slot(2, 11), // Wed 9–12(共同) - slot(2, 14), slot(2, 15), slot(2, 16), // Wed 14–17 - slot(4, 9), slot(4, 10), // Fri 9–11 + slot(0, 0, 9), slot(0, 0, 10), slot(0, 0, 11), // Mon 9–12 + slot(0, 2, 9), slot(0, 2, 10), slot(0, 2, 11), // Wed 9–12(共同) + slot(0, 2, 14), slot(0, 2, 15), slot(0, 2, 16), // Wed 14–17 + slot(0, 4, 9), slot(0, 4, 10), // Fri 9–11 ], }, { @@ -75,9 +77,9 @@ const INITIAL_MEMBERS: Member[] = [ name: "盧盧", color: "bg-purple-500", availability: [ - slot(1, 10), slot(1, 11), slot(1, 12), // Tue 10–13 - slot(2, 9), slot(2, 10), slot(2, 11), // Wed 9–12(共同) - slot(3, 14), slot(3, 15), // Thu 14–16 + slot(0, 1, 10), slot(0, 1, 11), slot(0, 1, 12), // Tue 10–13 + slot(0, 2, 9), slot(0, 2, 10), slot(0, 2, 11), // Wed 9–12(共同) + slot(0, 3, 14), slot(0, 3, 15), // Thu 14–16 ], }, ]; @@ -96,10 +98,12 @@ function ScheduleGrid({ availability, onBatchToggle, emerald = false, + week = 0, }: { availability: TimeSlot[]; onBatchToggle?: (slots: TimeSlot[], fill: boolean) => void; emerald?: boolean; + week?: number; }) { const [drag, setDrag] = useState(null); const dragging = useRef(false); @@ -115,14 +119,14 @@ function ScheduleGrid({ const selected: TimeSlot[] = []; for (let d = d0; d <= d1; d++) for (let hi = h0; hi <= h1; hi++) - selected.push(slot(d, HOURS[hi])); + selected.push(slot(week, d, HOURS[hi])); onBatchToggle?.(selected, drag.filling); dragging.current = false; setDrag(null); } document.addEventListener("mouseup", handleMouseUp); return () => document.removeEventListener("mouseup", handleMouseUp); - }, [drag, onBatchToggle]); + }, [drag, onBatchToggle, week]); function inDragRect(d: number, hi: number): boolean { if (!drag) return false; @@ -153,7 +157,7 @@ function ScheduleGrid({ {h}:00 {DAYS.map((_, d) => { - const s = slot(d, h); + const s = slot(week, d, h); const active = availability.includes(s); const inRect = inDragRect(d, hi); @@ -267,6 +271,7 @@ function LoginScreen({ onJoin }: { onJoin: (name: string) => void }) { export default function MeetFlow() { const [loggedIn, setLoggedIn] = useState(false); + const [week, setWeek] = useState(0); const [members, setMembers] = useState(INITIAL_MEMBERS); const [newName, setNewName] = useState(""); const [open, setOpen] = useState(false); @@ -291,8 +296,8 @@ export default function MeetFlow() { const commonSlots = DAYS.flatMap((_, d) => HOURS.filter((h) => - members.every((m) => m.availability.includes(slot(d, h))) - ).map((h) => slot(d, h)) + members.every((m) => m.availability.includes(slot(week, d, h))) + ).map((h) => slot(week, d, h)) ); function batchToggleMySlots(slots: TimeSlot[], fill: boolean) { @@ -343,8 +348,22 @@ export default function MeetFlow() { {/* ── Main ── */}
+
+ {WEEKS.map((label, idx) => ( + + ))} +
+ - + 我的時間表 @@ -430,7 +449,7 @@ export default function MeetFlow() { {/* ── Tab 2: My Schedule ── */}
-

我的時間表

+

我的時間表 ({WEEKS[week]})

點擊或拖曳選取矩形範圍來批次切換空閒時段

@@ -446,6 +465,7 @@ export default function MeetFlow() { @@ -454,7 +474,7 @@ export default function MeetFlow() { {/* ── Tab 3: View Member ── */}
-

查看成員時間表

+

查看成員時間表 ({WEEKS[week]})

選擇成員來查看他們的空閒時段

@@ -503,7 +523,7 @@ export default function MeetFlow() { }, ]} /> - + )} @@ -514,7 +534,7 @@ export default function MeetFlow() { {/* ── Tab 4: Common Availability ── */}
-

共同空閒時間

+

共同空閒時間 ({WEEKS[week]})

所有 {members.length} 位成員都空閒的時段

@@ -533,7 +553,7 @@ export default function MeetFlow() { 目前沒有共同空閒時段

) : ( - + )} @@ -541,7 +561,7 @@ export default function MeetFlow() { {commonSlots.length > 0 && (
{commonSlots.map((s) => { - const [d, h] = s.split("-").map(Number); + const [w, d, h] = s.split("-").map(Number); return (
Date: Sun, 22 Mar 2026 11:13:40 +0800 Subject: [PATCH 5/5] feat: integrate real current dates and infinite week navigation --- app/page.tsx | 183 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 119 insertions(+), 64 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 5ce4ff9..20d17d0 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,24 +14,20 @@ import { } from "@/components/ui/dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Input } from "@/components/ui/input"; -import { Plus, Users, Calendar, User, CalendarCheck } from "lucide-react"; - -// ─── Types ──────────────────────────────────────────────────────────────────── - -type TimeSlot = string; // "day-hour", e.g. "0-9" = Monday 9am - -type Member = { - id: string; - name: string; - color: string; - availability: TimeSlot[]; -}; +import { + Plus, + Users, + Calendar, + User, + CalendarCheck, + ChevronLeft, + ChevronRight, +} from "lucide-react"; // ─── Constants ──────────────────────────────────────────────────────────────── const DAYS = ["週一", "週二", "週三", "週四", "週五"]; const HOURS = [9, 10, 11, 12, 13, 14, 15, 16, 17]; -const WEEKS = ["本週", "下週", "下下週", "第四週"]; const COLORS = [ "bg-orange-500", @@ -43,10 +39,44 @@ const COLORS = [ "bg-cyan-500", ]; -const slot = (week: number, day: number, hour: number): TimeSlot => `${week}-${day}-${hour}`; +// Helper: Format date to YYYY-MM-DD +function formatDateKey(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +} + +// Slot ID: YYYY-MM-DD-HH +const slot = (date: Date, hour: number): TimeSlot => + `${formatDateKey(date)}-${hour}`; + +// Helper: Get Monday of the current week +function getMonday(d: Date) { + const date = new Date(d); + const day = date.getDay(); + const diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday + return new Date(date.setDate(diff)); +} + +// Helper: Get dates for Mon-Fri of a given week base +function getWeekDates(baseDate: Date) { + const dates = []; + const start = getMonday(baseDate); + for (let i = 0; i < 5; i++) { + const d = new Date(start); + d.setDate(start.getDate() + i); + dates.push(d); + } + return dates; +} // ─── Fake initial data ──────────────────────────────────────────────────────── +// Generate initial dates based on *current* week +const now = new Date(); +const currentWeek = getWeekDates(now); + // 假資料:三人皆有「週三 9–11」共同空閒,方便展示 const INITIAL_MEMBERS: Member[] = [ { @@ -54,11 +84,11 @@ const INITIAL_MEMBERS: Member[] = [ name: "我", color: "bg-blue-500", availability: [ - slot(0, 0, 9), slot(0, 0, 10), slot(0, 0, 11), // Mon 9–12 - slot(0, 0, 14), slot(0, 0, 15), slot(0, 0, 16), // Mon 14–17 - slot(0, 2, 9), slot(0, 2, 10), slot(0, 2, 11), // Wed 9–12(共同) - slot(0, 3, 14), slot(0, 3, 15), slot(0, 3, 16), // Thu 14–17 - slot(0, 4, 9), slot(0, 4, 10), // Fri 9–11 + slot(currentWeek[0], 9), slot(currentWeek[0], 10), slot(currentWeek[0], 11), // Mon 9–12 + slot(currentWeek[0], 14), slot(currentWeek[0], 15), slot(currentWeek[0], 16), // Mon 14–17 + slot(currentWeek[2], 9), slot(currentWeek[2], 10), slot(currentWeek[2], 11), // Wed 9–12(共同) + slot(currentWeek[3], 14), slot(currentWeek[3], 15), slot(currentWeek[3], 16), // Thu 14–17 + slot(currentWeek[4], 9), slot(currentWeek[4], 10), // Fri 9–11 ], }, { @@ -66,10 +96,10 @@ const INITIAL_MEMBERS: Member[] = [ name: "小梁", color: "bg-green-500", availability: [ - slot(0, 0, 9), slot(0, 0, 10), slot(0, 0, 11), // Mon 9–12 - slot(0, 2, 9), slot(0, 2, 10), slot(0, 2, 11), // Wed 9–12(共同) - slot(0, 2, 14), slot(0, 2, 15), slot(0, 2, 16), // Wed 14–17 - slot(0, 4, 9), slot(0, 4, 10), // Fri 9–11 + slot(currentWeek[0], 9), slot(currentWeek[0], 10), slot(currentWeek[0], 11), // Mon 9–12 + slot(currentWeek[2], 9), slot(currentWeek[2], 10), slot(currentWeek[2], 11), // Wed 9–12(共同) + slot(currentWeek[2], 14), slot(currentWeek[2], 15), slot(currentWeek[2], 16), // Wed 14–17 + slot(currentWeek[4], 9), slot(currentWeek[4], 10), // Fri 9–11 ], }, { @@ -77,9 +107,9 @@ const INITIAL_MEMBERS: Member[] = [ name: "盧盧", color: "bg-purple-500", availability: [ - slot(0, 1, 10), slot(0, 1, 11), slot(0, 1, 12), // Tue 10–13 - slot(0, 2, 9), slot(0, 2, 10), slot(0, 2, 11), // Wed 9–12(共同) - slot(0, 3, 14), slot(0, 3, 15), // Thu 14–16 + slot(currentWeek[1], 10), slot(currentWeek[1], 11), slot(currentWeek[1], 12), // Tue 10–13 + slot(currentWeek[2], 9), slot(currentWeek[2], 10), slot(currentWeek[2], 11), // Wed 9–12(共同) + slot(currentWeek[3], 14), slot(currentWeek[3], 15), // Thu 14–16 ], }, ]; @@ -98,12 +128,12 @@ function ScheduleGrid({ availability, onBatchToggle, emerald = false, - week = 0, + weekDates, }: { availability: TimeSlot[]; onBatchToggle?: (slots: TimeSlot[], fill: boolean) => void; emerald?: boolean; - week?: number; + weekDates: Date[]; }) { const [drag, setDrag] = useState(null); const dragging = useRef(false); @@ -119,14 +149,15 @@ function ScheduleGrid({ const selected: TimeSlot[] = []; for (let d = d0; d <= d1; d++) for (let hi = h0; hi <= h1; hi++) - selected.push(slot(week, d, HOURS[hi])); + // Use the actual date from weekDates[d] + selected.push(slot(weekDates[d], HOURS[hi])); onBatchToggle?.(selected, drag.filling); dragging.current = false; setDrag(null); } document.addEventListener("mouseup", handleMouseUp); return () => document.removeEventListener("mouseup", handleMouseUp); - }, [drag, onBatchToggle, week]); + }, [drag, onBatchToggle, weekDates]); function inDragRect(d: number, hi: number): boolean { if (!drag) return false; @@ -143,9 +174,12 @@ function ScheduleGrid({
))} @@ -156,10 +190,10 @@ function ScheduleGrid({ - {DAYS.map((_, d) => { - const s = slot(week, d, h); + {weekDates.map((d, di) => { + const s = slot(d, h); const active = availability.includes(s); - const inRect = inDragRect(d, hi); + const inRect = inDragRect(di, hi); let cellClass: string; if (inRect) { @@ -176,7 +210,7 @@ function ScheduleGrid({ } return ( -
{h}:00 @@ -115,16 +155,44 @@ function ScheduleGrid({ {DAYS.map((_, d) => { const s = slot(d, h); const active = availability.includes(s); - const cellClass = active - ? emerald + const inRect = inDragRect(d, hi); + + let cellClass: string; + if (inRect) { + // Preview: show what the result will be + cellClass = drag!.filling + ? "bg-primary/60 border-primary/60" + : "bg-muted border-border opacity-40"; + } else if (active) { + cellClass = emerald ? "bg-emerald-400 border-emerald-400" - : "bg-primary border-primary" - : "bg-muted border-border hover:bg-muted/60"; + : "bg-primary border-primary"; + } else { + cellClass = "bg-muted border-border hover:bg-muted/60"; + } + return (
onToggle?.(d, h)} + className={`h-8 rounded border transition-colors ${cellClass} ${onBatchToggle ? "cursor-pointer" : "cursor-default"}`} + onMouseDown={(e) => { + if (!onBatchToggle) return; + e.preventDefault(); + dragging.current = true; + setDrag({ + startDay: d, + startHourIdx: hi, + curDay: d, + curHourIdx: hi, + filling: !active, + }); + }} + onMouseOver={() => { + if (!dragging.current) return; + setDrag((prev) => + prev ? { ...prev, curDay: d, curHourIdx: hi } : prev + ); + }} />
- {DAYS.map((d) => ( - - {d} + {weekDates.map((d, i) => ( + +
{DAYS[i]}
+
+ {d.getMonth() + 1}/{d.getDate()} +
{h}:00 +
{ @@ -184,9 +218,9 @@ function ScheduleGrid({ e.preventDefault(); dragging.current = true; setDrag({ - startDay: d, + startDay: di, startHourIdx: hi, - curDay: d, + curDay: di, curHourIdx: hi, filling: !active, }); @@ -194,7 +228,7 @@ function ScheduleGrid({ onMouseOver={() => { if (!dragging.current) return; setDrag((prev) => - prev ? { ...prev, curDay: d, curHourIdx: hi } : prev + prev ? { ...prev, curDay: di, curHourIdx: hi } : prev ); }} /> @@ -271,12 +305,33 @@ function LoginScreen({ onJoin }: { onJoin: (name: string) => void }) { export default function MeetFlow() { const [loggedIn, setLoggedIn] = useState(false); - const [week, setWeek] = useState(0); + const [baseDate, setBaseDate] = useState(() => { + const d = new Date(); + // Start at current week's Monday + return getMonday(d); + }); + const [members, setMembers] = useState(INITIAL_MEMBERS); const [newName, setNewName] = useState(""); const [open, setOpen] = useState(false); const [viewId, setViewId] = useState("xiao-liang"); + const weekDates = Array.from({ length: 5 }).map((_, i) => { + const d = new Date(baseDate); + d.setDate(baseDate.getDate() + i); + return d; + }); + + const weekLabel = `${weekDates[0].getMonth() + 1}/${weekDates[0].getDate()} - ${weekDates[4].getMonth() + 1}/${weekDates[4].getDate()}`; + + function changeWeek(delta: number) { + setBaseDate((prev) => { + const next = new Date(prev); + next.setDate(prev.getDate() + delta * 7); + return next; + }); + } + if (!loggedIn) { return ( m.id !== "me"); const viewing = members.find((m) => m.id === viewId) ?? others[0]; - const commonSlots = DAYS.flatMap((_, d) => + const commonSlots = DAYS.flatMap((_, i) => HOURS.filter((h) => - members.every((m) => m.availability.includes(slot(week, d, h))) - ).map((h) => slot(week, d, h)) + members.every((m) => m.availability.includes(slot(weekDates[i], h))) + ).map((h) => slot(weekDates[i], h)) ); function batchToggleMySlots(slots: TimeSlot[], fill: boolean) { @@ -348,18 +403,16 @@ export default function MeetFlow() { {/* ── Main ── */}
-
- {WEEKS.map((label, idx) => ( - - ))} +
+ +
+ {weekLabel} +
+
@@ -449,7 +502,7 @@ export default function MeetFlow() { {/* ── Tab 2: My Schedule ── */}
-

我的時間表 ({WEEKS[week]})

+

我的時間表

點擊或拖曳選取矩形範圍來批次切換空閒時段

@@ -465,7 +518,7 @@ export default function MeetFlow() { @@ -474,7 +527,7 @@ export default function MeetFlow() { {/* ── Tab 3: View Member ── */}
-

查看成員時間表 ({WEEKS[week]})

+

查看成員時間表

選擇成員來查看他們的空閒時段

@@ -523,7 +576,7 @@ export default function MeetFlow() { }, ]} /> - + )} @@ -534,7 +587,7 @@ export default function MeetFlow() { {/* ── Tab 4: Common Availability ── */}
-

共同空閒時間 ({WEEKS[week]})

+

共同空閒時間

所有 {members.length} 位成員都空閒的時段

@@ -553,7 +606,7 @@ export default function MeetFlow() { 目前沒有共同空閒時段

) : ( - + )} @@ -561,13 +614,15 @@ export default function MeetFlow() { {commonSlots.length > 0 && (
{commonSlots.map((s) => { - const [w, d, h] = s.split("-").map(Number); + const [y, m, d, h] = s.split("-").map(Number); + // Careful: month is 1-based in our key + const dateObj = new Date(y, m - 1, d); return (
- {DAYS[d]} {h}:00–{h + 1}:00 + {dateObj.getMonth() + 1}/{dateObj.getDate()} ({DAYS[dateObj.getDay() === 0 ? 6 : dateObj.getDay() - 1]}) {h}:00–{h + 1}:00
); })}