From 1ae4f5bfd8a1ee4943fb70e3696f3d3f20691216 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 24 Oct 2025 09:27:06 +0000 Subject: [PATCH] fix: prevent guest link icon from re-animating on WebSocket updates The external guest link icon was fading in/out on every WebSocket update, creating a distracting visual effect. Now the fade-in animation only plays when the URL first becomes available (initial load or when saving a new URL), rather than re-triggering on every state update. - Add fadeIn keyframe animation to Tailwind config - Track animation state with shouldAnimateIcon signal - Only animate on transition from no URL to having a URL - Clear animation flag after 200ms to prevent re-triggering --- .../src/components/Dashboard/GuestRow.tsx | 23 +++++++++++++++++-- frontend-modern/tailwind.config.js | 7 ++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend-modern/src/components/Dashboard/GuestRow.tsx b/frontend-modern/src/components/Dashboard/GuestRow.tsx index e8ce814..af500a9 100644 --- a/frontend-modern/src/components/Dashboard/GuestRow.tsx +++ b/frontend-modern/src/components/Dashboard/GuestRow.tsx @@ -61,6 +61,7 @@ export function GuestRow(props: GuestRowProps) { const isEditingUrl = createMemo(() => currentlyEditingGuestId() === guestId()); const [customUrl, setCustomUrl] = createSignal(props.customUrl); + const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false); const [drawerOpen, setDrawerOpen] = createSignal(drawerState.get(initialGuestId) ?? false); const editingUrlValue = createMemo(() => { editingValuesVersion(); // Subscribe to changes @@ -82,7 +83,17 @@ export function GuestRow(props: GuestRowProps) { createEffect(() => { // Don't update customUrl from props if this guest is currently being edited if (currentlyEditingGuestId() !== guestId()) { - setCustomUrl(props.customUrl); + const prevUrl = customUrl(); + const newUrl = props.customUrl; + + // Only animate when URL transitions from empty to having a value + if (!prevUrl && newUrl) { + setShouldAnimateIcon(true); + // Remove animation class after it completes + setTimeout(() => setShouldAnimateIcon(false), 200); + } + + setCustomUrl(newUrl); } }); @@ -250,6 +261,14 @@ export function GuestRow(props: GuestRowProps) { try { await GuestMetadataAPI.updateMetadata(guestId(), { customUrl: newUrl }); + + // Animate if transitioning from no URL to having a URL + const hadUrl = !!customUrl(); + if (!hadUrl && newUrl) { + setShouldAnimateIcon(true); + setTimeout(() => setShouldAnimateIcon(false), 200); + } + setCustomUrl(newUrl || undefined); // Notify parent to update metadata @@ -430,7 +449,7 @@ export function GuestRow(props: GuestRowProps) { href={customUrl()} target="_blank" rel="noopener noreferrer" - class="flex-shrink-0 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors" + class={`flex-shrink-0 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors ${shouldAnimateIcon() ? 'animate-fadeIn' : ''}`} title="Open in new tab" onClick={(event) => event.stopPropagation()} > diff --git a/frontend-modern/tailwind.config.js b/frontend-modern/tailwind.config.js index 2e56f29..891b93e 100644 --- a/frontend-modern/tailwind.config.js +++ b/frontend-modern/tailwind.config.js @@ -13,6 +13,13 @@ export default { }, animation: { 'spin-slow': 'spin 2s linear infinite', + 'fadeIn': 'fadeIn 0.2s ease-in', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + } } }, },