From 31f4494ff2f7f5f8746814dc145ddab16303c3dd Mon Sep 17 00:00:00 2001 From: Ruby Date: Mon, 2 Mar 2026 16:38:27 +0800 Subject: [PATCH] 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() { />
{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 + ); + }} />