diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 62cd479..73d36d3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -29,6 +29,7 @@ import NavSidebar from "./components/NavSidebar"; import Sidebar from "./components/Sidebar"; import Feed from "./components/Feed"; import ChannelPage from "./components/ChannelPage"; +import BackToTop from "./components/BackToTop"; import Channels, { type ChannelStatusFilter, type ChannelsView } from "./components/Channels"; import Playlists from "./components/Playlists"; import Stats from "./components/Stats"; @@ -667,6 +668,8 @@ export default function App() { setNotesOpen(false)} highlight={notesHighlight} /> )} + {/* Re-binds to the active page's scroll container on navigation (page or channel change). */} + ); } diff --git a/frontend/src/components/BackToTop.tsx b/frontend/src/components/BackToTop.tsx new file mode 100644 index 0000000..fc021bd --- /dev/null +++ b/frontend/src/components/BackToTop.tsx @@ -0,0 +1,44 @@ +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { ArrowUp } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +// A floating "back to top" button that fades in once the current page's scroll container is +// scrolled past a threshold, and smooth-scrolls it back to the top. Every page scrolls inside its +// own
(the normal pages share App's; the channel page has its own), +// so it targets the live
and re-binds whenever `dep` changes (i.e. on navigation). Portaled +// to so its fixed position is viewport-relative regardless of transformed ancestors. +export default function BackToTop({ dep, threshold = 600 }: { dep?: unknown; threshold?: number }) { + const { t } = useTranslation(); + const [show, setShow] = useState(false); + const scrollerRef = useRef(null); + + useEffect(() => { + const el = document.querySelector("main"); + scrollerRef.current = el; + if (!el) { + setShow(false); + return; + } + const onScroll = () => setShow(el.scrollTop > threshold); + onScroll(); // a freshly-bound page starts at the top → hidden + el.addEventListener("scroll", onScroll, { passive: true }); + return () => el.removeEventListener("scroll", onScroll); + }, [dep, threshold]); + + const toTop = () => scrollerRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + + return createPortal( + , + document.body + ); +} diff --git a/frontend/src/i18n/locales/de/common.json b/frontend/src/i18n/locales/de/common.json index 512fe96..f5ca1be 100644 --- a/frontend/src/i18n/locales/de/common.json +++ b/frontend/src/i18n/locales/de/common.json @@ -15,5 +15,6 @@ "accessRequestsMessage": "{{count}} ausstehend — prüfe sie auf der Benutzer-Seite.", "accessRequestsReview": "Überprüfen", "demoTitle": "Demo-Konto", - "demoSharedNotice": "Du bist im gemeinsamen Demo-Konto. Alles, was du hier tust — Playlists, ausgeblendete Videos, Einstellungen — ist gemeinsam und kann von anderen Demo-Besuchern gleichzeitig geändert werden." + "demoSharedNotice": "Du bist im gemeinsamen Demo-Konto. Alles, was du hier tust — Playlists, ausgeblendete Videos, Einstellungen — ist gemeinsam und kann von anderen Demo-Besuchern gleichzeitig geändert werden.", + "backToTop": "Nach oben" } diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 8082bbe..3091ff2 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -15,5 +15,6 @@ "accessRequestsMessage": "{{count}} pending — review them on the Users page.", "accessRequestsReview": "Review", "demoTitle": "Demo account", - "demoSharedNotice": "You're in the shared demo account. Everything you do here — playlists, hidden videos, settings — is communal and may be changed by other demo visitors at the same time." + "demoSharedNotice": "You're in the shared demo account. Everything you do here — playlists, hidden videos, settings — is communal and may be changed by other demo visitors at the same time.", + "backToTop": "Back to top" } diff --git a/frontend/src/i18n/locales/hu/common.json b/frontend/src/i18n/locales/hu/common.json index c004de7..b5b9efb 100644 --- a/frontend/src/i18n/locales/hu/common.json +++ b/frontend/src/i18n/locales/hu/common.json @@ -15,5 +15,6 @@ "accessRequestsMessage": "{{count}} függőben — nézd át a Felhasználók oldalon.", "accessRequestsReview": "Áttekintés", "demoTitle": "Demo fiók", - "demoSharedNotice": "Egy közös demo fiókban vagy. Minden, amit itt csinálsz — lejátszási listák, elrejtett videók, beállítások — közös, és más demo látogatók egyszerre módosíthatják." + "demoSharedNotice": "Egy közös demo fiókban vagy. Minden, amit itt csinálsz — lejátszási listák, elrejtett videók, beállítások — közös, és más demo látogatók egyszerre módosíthatják.", + "backToTop": "Vissza a tetejére" }