From 94e332ba57d30f453f4891e9c653997a898b319d Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Fri, 16 Jan 2026 15:16:48 +0100 Subject: [PATCH] fix: (web) improve quest table refresh UX - Keep card visible during refresh to prevent flicker - Add smooth animations when content loads - Spin refresh icon independently from skeleton - Show skeleton in place without replacing entire card --- web/src/components/quest-table.tsx | 251 ++++++++++++++++------------- 1 file changed, 136 insertions(+), 115 deletions(-) diff --git a/web/src/components/quest-table.tsx b/web/src/components/quest-table.tsx index 42dced3..9964a33 100644 --- a/web/src/components/quest-table.tsx +++ b/web/src/components/quest-table.tsx @@ -37,13 +37,6 @@ const TRIGGER_EVENT_LABELS: Record = { TRIVIA_WIN: "Trivia Win", }; -function formatQuestRewards(rewards: { xp?: number; balance?: number }): string { - const parts: string[] = []; - if (rewards?.xp) parts.push(`${rewards.xp} ⭐`); - if (rewards?.balance) parts.push(`${rewards.balance} 🪙`); - return parts.join(" • ") || "None"; -} - function getTriggerEventLabel(triggerEvent: string): string { return TRIGGER_EVENT_LABELS[triggerEvent] || triggerEvent; } @@ -71,7 +64,7 @@ function TruncatedText({ text, maxLength = 100 }: { text: string; maxLength?: nu function QuestTableSkeleton() { return ( -
+
@@ -98,7 +91,7 @@ function QuestTableSkeleton() { function EmptyQuestState() { return ( -
+
- -
- Quest Inventory - - Loading... - -
- - - - - - ); +function QuestTableContent({ quests }: { quests: QuestListItem[] }) { + if (quests.length === 0) { + return ; } + return ( +
+ + + + + + + + + + + + + + {quests.map((quest) => { + const requirements = quest.requirements as { target?: number }; + const rewards = quest.rewards as { xp?: number; balance?: number }; + const target = requirements?.target || 1; + + return ( + + + + + + + + + + ); + })} + +
+ ID + + Name + + Description + + Trigger Event + + Target + + XP Reward + + AU Reward +
+ #{quest.id} + + {quest.name} + + + + + {getTriggerEventLabel(quest.triggerEvent)} + + + {target} + + {rewards?.xp ? ( + + + {rewards.xp} + + ) : ( + - + )} + + {rewards?.balance ? ( + + 🪙 + {rewards.balance} + + ) : ( + - + )} +
+
+ ); +} + +export function QuestTable({ quests, isLoading, onRefresh }: QuestTableProps) { + const [isRefreshing, setIsRefreshing] = React.useState(false); + const previousQuestsRef = React.useRef(quests); + const [displayQuests, setDisplayQuests] = React.useState(quests); + + React.useEffect(() => { + if (isLoading) { + setIsRefreshing(true); + } else { + setIsRefreshing(false); + previousQuestsRef.current = quests; + if (quests.length !== previousQuestsRef.current.length || + JSON.stringify(quests) !== JSON.stringify(previousQuestsRef.current)) { + setDisplayQuests(quests); + } + } + }, [isLoading, quests]); + + const handleRefresh = async () => { + setIsRefreshing(true); + onRefresh?.(); + }; + return (
Quest Inventory
- - {quests.length} quest{quests.length !== 1 ? "s" : ""} - + {isLoading ? ( + + Loading... + + ) : ( + + {quests.length} quest{quests.length !== 1 ? "s" : ""} + + )}
- {quests.length === 0 ? ( - + {isLoading ? ( + ) : ( -
- - - - - - - - - - - - - - {quests.map((quest) => { - const requirements = quest.requirements as { target?: number }; - const rewards = quest.rewards as { xp?: number; balance?: number }; - const target = requirements?.target || 1; - - return ( - - - - - - - - - - ); - })} - -
- ID - - Name - - Description - - Trigger Event - - Target - - XP Reward - - AU Reward -
- #{quest.id} - - {quest.name} - - - - - {getTriggerEventLabel(quest.triggerEvent)} - - - {target} - - {rewards?.xp ? ( - - - {rewards.xp} - - ) : ( - - - )} - - {rewards?.balance ? ( - - 🪙 - {rewards.balance} - - ) : ( - - - )} -
-
+ )}