From 808d595befdf29e767deaaef57c4bf98add19dd5 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sun, 11 Jan 2026 03:41:58 +0100 Subject: [PATCH] chore(logs): add scroll-to-bottom button --- src/components/LogsPage.tsx | 76 ++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/src/components/LogsPage.tsx b/src/components/LogsPage.tsx index f2e949e5..3bdc39d8 100644 --- a/src/components/LogsPage.tsx +++ b/src/components/LogsPage.tsx @@ -1,5 +1,5 @@ -import { useState, useCallback, useEffect, useRef } from 'react' -import { AlertTriangleIcon, Loader2Icon, RefreshCwIcon, DownloadIcon } from 'lucide-react' +import { useState, useCallback, useEffect, useRef, useMemo } from 'react' +import { AlertTriangleIcon, Loader2Icon, RefreshCwIcon, DownloadIcon, ArrowDownIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useStore } from 'zustand' import { Alert, AlertDescription } from '@/components/ui/alert' @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { isDevMode } from '@/constants/debugFeatures' import { fetchLog } from '@/lib/api/logs' -import { cn } from '@/lib/utils' +import { cn, delayedPromise } from '@/lib/utils' import { authStore } from '@/store/authStore' import PageTitle from './ui/jam/PageTitle' @@ -27,26 +27,42 @@ function LogContent({ value, refresh }: LogContentProps) { const { t } = useTranslation() const logContentRef = useRef(null) const [isLoadingRefresh, setIsLoadingRefresh] = useState(false) + const [logScrollProgress, setLogScrollProgress] = useState(0) + const isScrolledToLogBottom = useMemo(() => logScrollProgress >= 0.995, [logScrollProgress]) - useEffect(() => { - if (!value || !logContentRef.current) return - - logContentRef.current.scrollTo({ + const scrollToLogBottom = () => { + logContentRef.current?.scrollTo({ top: logContentRef.current.scrollHeight, behavior: 'smooth', }) + } + + const logScrollHandler = (event: React.UIEvent) => { + const containerHeight = event.currentTarget.clientHeight + const scrollHeight = event.currentTarget.scrollHeight + + const scrollTop = event.currentTarget.scrollTop + setLogScrollProgress((scrollTop + containerHeight) / scrollHeight) + } + + useEffect(() => { + if (!value) return + scrollToLogBottom() }, [value]) - const handleRefresh = useCallback(() => { + const handleRefresh = useCallback(async () => { if (isLoadingRefresh) return setIsLoadingRefresh(true) const abortCtrl = new AbortController() - refresh(abortCtrl.signal).finally(() => { - // Add a short delay to avoid flickering - setTimeout(() => setIsLoadingRefresh(false), 250) - }) + try { + await refresh(abortCtrl.signal) + // Add a short delay to avoid flickering + .then(() => delayedPromise(210)) + } finally { + setIsLoadingRefresh(false) + } }, [isLoadingRefresh, refresh]) const handleDownload = useCallback(() => { @@ -66,25 +82,19 @@ function LogContent({ value, refresh }: LogContentProps) { return ( - {JMWALLETD_LOG_FILE_NAME} + {JMWALLETD_LOG_FILE_NAME}
-
- +
-          {value || 'No logs available'}
+          {value}
         
+
) @@ -146,7 +170,7 @@ export const LogsPage = () => { if (isDevMode()) { // adding content enables manual testing of other component // and functionality (e.g. downloading, refreshing, etc.) - setLogFileContent(errorMessage) + setLogFileContent(`${errorMessage}\n`.repeat(1_000)) } setAlert({