From 8b544290abcfd5cb8bf85e66b40c450572551589 Mon Sep 17 00:00:00 2001 From: Guy Mor Date: Mon, 8 Sep 2025 17:13:52 +0300 Subject: [PATCH 1/2] added stick to bottom button --- frontend/src/App.tsx | 2 +- frontend/src/{ => components}/PadPage.tsx | 103 ++++++++++++++++------ 2 files changed, 77 insertions(+), 28 deletions(-) rename frontend/src/{ => components}/PadPage.tsx (82%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c63499f..cddd734 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import { HomePage } from './components/HomePage'; -import { PadPage } from './PadPage'; +import { PadPage } from './components/PadPage'; function App() { return ( diff --git a/frontend/src/PadPage.tsx b/frontend/src/components/PadPage.tsx similarity index 82% rename from frontend/src/PadPage.tsx rename to frontend/src/components/PadPage.tsx index c76b78f..af98130 100644 --- a/frontend/src/PadPage.tsx +++ b/frontend/src/components/PadPage.tsx @@ -1,12 +1,12 @@ import { Navigate, useParams } from 'react-router-dom'; -import { PadEditor } from './components/PadEditor'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { RUNNERS } from './runners/runner'; -import { Button } from './components/Button'; -import { TabLayout } from './components/TabLayout'; -import { SideBySideLayout } from './components/SideBySideLayout'; -import { CollaborationBalloon, CollaborationToggle } from './components/CollaborationBalloon'; -import { useCollaboration } from './hooks/useCollaboration'; +import { PadEditor } from './PadEditor'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { RUNNERS } from '../runners/runner'; +import { Button } from './Button'; +import { TabLayout } from './TabLayout'; +import { SideBySideLayout } from './SideBySideLayout'; +import { CollaborationBalloon, CollaborationToggle } from './CollaborationBalloon'; +import { useCollaboration } from '../hooks/useCollaboration'; import { BAD_KEY_ERROR, getLanguageCodeSample, @@ -15,10 +15,10 @@ import { type PadRoom, SUPPORTED_LANGUAGES, } from 'coderjam-shared'; -import { getUserColorClassname } from './utils/userColors'; -import { useLocalStorageState } from './hooks/useLocalStorageState'; -import useIsOnMobile from './hooks/useIsOnMobile'; -import { Header } from './components/Header'; +import { getUserColorClassname } from '../utils/userColors'; +import { useLocalStorageState } from '../hooks/useLocalStorageState'; +import useIsOnMobile from '../hooks/useIsOnMobile'; +import { Header } from './Header'; const INITIAL_OUTPUT: OutputEntry[] = [ { type: 'log', text: 'Code execution results will be displayed here.' }, @@ -39,6 +39,11 @@ export function PadPage() { const [pad, setPad] = useState(undefined); const [username, setUsername] = useLocalStorageState('username', 'Guest'); const [isCollaborationVisible, setIsCollaborationVisible] = useState(false); + const [stickToBottom, setStickToBottom] = useLocalStorageState( + 'stick_to_bottom', + true + ); + const outputContainerRef = useRef(null); const currentRunner = pad ? RUNNERS[pad.language] : undefined; const isOnMobile = useIsOnMobile(); @@ -273,6 +278,16 @@ export function PadPage() { changeOutput(CLEAN_OUTPUT); }, [changeOutput]); + // Auto-scroll to bottom when output changes and stick to bottom is enabled + useEffect(() => { + if (stickToBottom && outputContainerRef.current) { + outputContainerRef.current.scroll({ + top: outputContainerRef.current.scrollHeight, + behavior: 'smooth', + }); + } + }, [pad?.output, stickToBottom]); + const handleUsernameChange = useCallback( (newUsername: string) => { if (!pad) { @@ -379,25 +394,59 @@ export function PadPage() { {!isOnMobile && (

Output

- + + + + + + +
)}
From cf8fd08cdcdcb14ac7f3d5025839c523400bbcd3 Mon Sep 17 00:00:00 2001 From: Guy Mor Date: Tue, 9 Sep 2025 02:24:08 +0300 Subject: [PATCH 2/2] auto scrolling is enabled/disabled when the user scrolls --- frontend/src/components/PadPage.tsx | 66 ++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/PadPage.tsx b/frontend/src/components/PadPage.tsx index af98130..f5f5c9f 100644 --- a/frontend/src/components/PadPage.tsx +++ b/frontend/src/components/PadPage.tsx @@ -39,10 +39,10 @@ export function PadPage() { const [pad, setPad] = useState(undefined); const [username, setUsername] = useLocalStorageState('username', 'Guest'); const [isCollaborationVisible, setIsCollaborationVisible] = useState(false); - const [stickToBottom, setStickToBottom] = useLocalStorageState( - 'stick_to_bottom', - true - ); + const [autoScrolling, setAutoScrolling] = useLocalStorageState('auto_scrolling', true); + // Ref to track if the next scroll to bottom event should be ignored + // We want the autoscrolling to enable/disable when the user scrolls but we don't want to trigger it when we auto scroll + const ignoreNextScrollEvent = useRef(false); const outputContainerRef = useRef(null); const currentRunner = pad ? RUNNERS[pad.language] : undefined; const isOnMobile = useIsOnMobile(); @@ -280,13 +280,51 @@ export function PadPage() { // Auto-scroll to bottom when output changes and stick to bottom is enabled useEffect(() => { - if (stickToBottom && outputContainerRef.current) { + if ( + autoScrolling && + outputContainerRef.current && + !isScrolledToBottom(outputContainerRef.current) + ) { + ignoreNextScrollEvent.current = true; outputContainerRef.current.scroll({ top: outputContainerRef.current.scrollHeight, behavior: 'smooth', }); } - }, [pad?.output, stickToBottom]); + }, [pad?.output, autoScrolling]); + + // Enable / disable auto-scrolling when the user scrolls + const handleOutputScroll = useCallback( + (ev: Event) => { + const target = ev.target as HTMLElement; + const isAtBottom = isScrolledToBottom(target); + + // exit early if should ignore scroll event + // if we are at the bottom, the scroll event is done and we should not longer ignore scroll events + if (ignoreNextScrollEvent.current) { + if (isAtBottom) { + // Done scrolling, reset ignore flag + ignoreNextScrollEvent.current = false; + } + return; + } + + setAutoScrolling(isAtBottom); + }, + [setAutoScrolling] + ); + + useEffect(() => { + if (!outputContainerRef.current) { + return; + } + const outputContainer = outputContainerRef.current; + + outputContainer.addEventListener('scroll', handleOutputScroll); + return () => { + outputContainer.removeEventListener('scroll', handleOutputScroll); + }; + }, [handleOutputScroll]); const handleUsernameChange = useCallback( (newUsername: string) => { @@ -396,15 +434,8 @@ export function PadPage() {

Output