From aca5300600401b12a8f039bdb9fa0cb6fa3515aa Mon Sep 17 00:00:00 2001 From: Antra1705 Date: Mon, 8 Jun 2026 15:40:15 +0530 Subject: [PATCH] feat(ux): add loading and disabled states for async action buttons --- src/app/dashboard/settings/page.tsx | 17 ++++++---- src/app/rooms/[roomId]/RoomClient.tsx | 26 ++++++++++----- src/components/ConfirmModal.tsx | 14 +++++--- src/components/GoalTracker.tsx | 48 +++++++++++++++++---------- src/components/ProjectMetrics.tsx | 23 +++++++++---- src/components/StreakTracker.tsx | 21 ++++++------ src/components/TodayFocusHero.tsx | 25 ++++++++++---- 7 files changed, 112 insertions(+), 62 deletions(-) diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 40611b095..08428c49c 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -1127,20 +1127,20 @@ function SettingsPageContent() { @@ -1149,8 +1149,9 @@ function SettingsPageContent() { @@ -1171,9 +1172,10 @@ function SettingsPageContent() { type="text" value={repoSearchQuery} onChange={(e) => setRepoSearchQuery(e.target.value)} + disabled={saving} placeholder="Type to search your repositories..." aria-label="Search repositories to pin" - className="w-full rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm text-[var(--card-foreground)] placeholder:text-[var(--muted-foreground)] focus-visible:ring-2 focus-visible:ring-[var(--accent)] mb-4" + className="w-full rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm text-[var(--card-foreground)] placeholder:text-[var(--muted-foreground)] focus-visible:ring-2 focus-visible:ring-[var(--accent)] mb-4 disabled:opacity-50 disabled:cursor-not-allowed" /> {loadingRepos ? ( @@ -1200,8 +1202,9 @@ function SettingsPageContent() { diff --git a/src/app/rooms/[roomId]/RoomClient.tsx b/src/app/rooms/[roomId]/RoomClient.tsx index 6d00aa88b..2214678f3 100644 --- a/src/app/rooms/[roomId]/RoomClient.tsx +++ b/src/app/rooms/[roomId]/RoomClient.tsx @@ -22,6 +22,7 @@ export default function RoomClient({ const router = useRouter(); const [messages, setMessages] = useState(initialMessages); const [members, setMembers] = useState(initialMembers); + const [deleting, setDeleting] = useState(false); function handleSent(msg: RoomMessage) { setMessages((prev) => [...prev, msg]); @@ -42,12 +43,20 @@ export default function RoomClient({ async function handleDeleteRoom() { if (!confirm('Are you sure you want to delete this room? This cannot be undone.')) return; - const res = await fetch(`/api/rooms/${room.id}`, { method: 'DELETE' }); - if (res.ok) { - router.push('/rooms'); - } else { - const data = await res.json(); - alert(data.error ?? 'Failed to delete room'); + setDeleting(true); + try { + const res = await fetch(`/api/rooms/${room.id}`, { method: 'DELETE' }); + if (res.ok) { + router.push('/rooms'); + } else { + const data = await res.json(); + alert(data.error ?? 'Failed to delete room'); + } + } catch (e) { + console.error(e); + alert('Failed to delete room. Please check your connection.'); + } finally { + setDeleting(false); } } @@ -75,9 +84,10 @@ export default function RoomClient({ {room.is_owner && ( )} diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index c04a28758..5b37902f2 100644 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -10,6 +10,7 @@ interface ConfirmModalProps { cancelLabel?: string; onConfirm: () => void; onCancel: () => void; + disabled?: boolean; } export default function ConfirmModal({ @@ -20,6 +21,7 @@ export default function ConfirmModal({ cancelLabel = "Cancel", onConfirm, onCancel, + disabled = false, }: ConfirmModalProps) { const modalRef = useRef(null); @@ -27,7 +29,7 @@ export default function ConfirmModal({ if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { + if (e.key === "Escape" && !disabled) { onCancel(); } }; @@ -40,7 +42,7 @@ export default function ConfirmModal({ document.removeEventListener("keydown", handleKeyDown); document.body.style.overflow = "unset"; }; - }, [isOpen, onCancel]); + }, [isOpen, onCancel, disabled]); if (!isOpen) return null; @@ -49,7 +51,7 @@ export default function ConfirmModal({ {/* Backdrop */}
diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 6c0b71383..fd512ed69 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -682,7 +682,7 @@ export default function StreakTracker() {

)} - {!freezeLoading && freeze?.hasFreeze && ( + {freeze && freeze.hasFreeze && (
)} - {!freezeLoading && !freeze?.hasFreeze && ( + {freeze && !freeze.hasFreeze && (
Streak Freeze @@ -742,13 +743,13 @@ export default function StreakTracker() {
)} diff --git a/src/components/TodayFocusHero.tsx b/src/components/TodayFocusHero.tsx index f3f741ffd..e1e05e3f7 100644 --- a/src/components/TodayFocusHero.tsx +++ b/src/components/TodayFocusHero.tsx @@ -42,6 +42,8 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { const [greeting, setGreeting] = useState<"morning" | "afternoon" | "evening">("morning"); const [todayKey, setTodayKey] = useState(""); const [isMounted, setIsMounted] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isClearing, setIsClearing] = useState(false); const greetingLabel = useMemo(() => { const base = @@ -105,6 +107,7 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { const trimmedGoal = inputValue.trim(); if (!trimmedGoal || !todayKey) return; + setIsSaving(true); try { window.localStorage.setItem(todayKey, trimmedGoal); } catch (e) {} @@ -122,12 +125,15 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { }); } catch (err) { console.error("Failed to save daily focus", err); + } finally { + setIsSaving(false); } } async function handleClear() { if (!todayKey) return; + setIsClearing(true); try { window.localStorage.removeItem(todayKey); } catch (e) {} @@ -143,6 +149,8 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { }); } catch (err) { console.error("Failed to clear daily focus", err); + } finally { + setIsClearing(false); } } @@ -219,9 +227,10 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) {
@@ -242,8 +251,9 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { setInputValue(event.target.value)} + disabled={isSaving || isClearing} placeholder="Write your main dev goal for today..." - className="w-full rounded-xl border border-[var(--border)] bg-[var(--background)] px-4 py-3 text-sm text-[var(--foreground)] shadow-sm transition placeholder:text-[var(--muted-foreground)]" + className="w-full rounded-xl border border-[var(--border)] bg-[var(--background)] px-4 py-3 text-sm text-[var(--foreground)] shadow-sm transition placeholder:text-[var(--muted-foreground)] disabled:opacity-50 disabled:cursor-not-allowed" /> @@ -251,18 +261,19 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { {goal ? ( ) : null}