Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import { Select } from './Select';
import { Button } from './Button';
import { capitalize, SUPPORTED_LANGUAGES } from 'coderjam-shared';
import { Tooltip } from './Tooltip';

export type HeaderProps = {
language: string;
Expand Down Expand Up @@ -47,26 +48,27 @@ export function Header({ language, onLanguageChange, isRunning, onRunCode }: Hea
</span>
</div>
</h1>
<a
href="https://github.com/guymor4/coderjam"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center p-2 text-dark-300 hover:text-dark-100 hover:bg-dark-700 rounded transition-colors duration-200"
title="View source on GitHub"
>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
<Tooltip text="View source on GitHub" direction="bottom">
<a
href="https://github.com/guymor4/coderjam"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center p-2 text-dark-300 hover:text-dark-100 hover:bg-dark-700 rounded transition-colors duration-200"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
</a>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
</a>
</Tooltip>
</div>
<div className="flex items-center gap-2 md:gap-4">
<Select
Expand Down
109 changes: 62 additions & 47 deletions frontend/src/components/PadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getUserColorClassname } from '../utils/userColors';
import { useLocalStorageState } from '../hooks/useLocalStorageState';
import useIsOnMobile from '../hooks/useIsOnMobile';
import { Header } from './Header';
import { Tooltip } from './Tooltip';

const INITIAL_OUTPUT: OutputEntry[] = [
{ type: 'log', text: 'Code execution results will be displayed here.' },
Expand All @@ -44,7 +45,7 @@ export function PadPage() {
// 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<boolean>(false);
const outputContainerRef = useRef<HTMLDivElement>(null);
const [outputContainer, setOutputContainer] = useState<HTMLDivElement | null>(null);
const currentRunner = pad ? RUNNERS[pad.language] : undefined;
const isOnMobile = useIsOnMobile();

Expand Down Expand Up @@ -281,51 +282,53 @@ export function PadPage() {

// Auto-scroll to bottom when output changes and stick to bottom is enabled
useEffect(() => {
if (
autoScrolling &&
outputContainerRef.current &&
!isScrolledToBottom(outputContainerRef.current)
) {
if (autoScrolling && outputContainer && !isScrolledToBottom(outputContainer)) {
ignoreNextScrollEvent.current = true;
outputContainerRef.current.scroll({
top: outputContainerRef.current.scrollHeight,
outputContainer.scroll({
top: outputContainer.scrollHeight,
behavior: 'smooth',
});
}
}, [pad?.output, autoScrolling]);
}, [pad?.output, autoScrolling, outputContainer]);

// Enable / disable auto-scrolling when the user scrolls
const handleOutputScroll = useCallback(
(ev: Event) => {
const target = ev.target as HTMLElement;
const isAtBottom = isScrolledToBottom(target);
console.log('Output scroll event');

// 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
console.log('Done scrolling, resetting ignore flag');
ignoreNextScrollEvent.current = false;
}
console.log('Ignoring scroll event');
return;
}

console.log('Setting auto-scrolling to:', isAtBottom);
setAutoScrolling(isAtBottom);
},
[setAutoScrolling]
);

useEffect(() => {
if (!outputContainerRef.current) {
if (!outputContainer) {
console.error('No output container found');
return;
}
const outputContainer = outputContainerRef.current;

console.log('Output container found:', outputContainer);

outputContainer.addEventListener('scroll', handleOutputScroll);
return () => {
outputContainer.removeEventListener('scroll', handleOutputScroll);
};
}, [handleOutputScroll]);
}, [handleOutputScroll, outputContainer]);

const handleUsernameChange = useCallback(
(newUsername: string) => {
Expand Down Expand Up @@ -434,30 +437,40 @@ export function PadPage() {
<div className="flex items-center justify-between px-6 py-4 border-b border-dark-600">
<h2 className="text-lg font-semibold text-dark-50">Output</h2>
<div className="flex gap-2">
<Button
variant={autoScrolling ? 'default' : 'outline'}
onClick={() => setAutoScrolling(!autoScrolling)}
<Tooltip
text={
autoScrolling
? 'Auto-scrolling is enabled'
: 'Auto-scrolling is disabled'
}
delay={200}
direction="bottom"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<Button
variant={autoScrolling ? 'default' : 'outline'}
onClick={() => setAutoScrolling(!autoScrolling)}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 12l-7 7m0 0l-7-7m7 7V3"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 23h14"
/>
</svg>
</Button>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 12l-7 7m0 0l-7-7m7 7V3"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 23h14"
/>
</svg>
</Button>
</Tooltip>
<Button variant="outline" onClick={clearOutput}>
<svg
className="w-4 h-4 mr-2"
Expand All @@ -478,7 +491,7 @@ export function PadPage() {
</div>
)}
<div
ref={outputContainerRef}
ref={(ref) => setOutputContainer(ref)}
data-testid="output"
className="flex-1 p-4 bg-dark-900 overflow-y-auto font-mono text-sm"
>
Expand Down Expand Up @@ -514,7 +527,7 @@ export function PadPage() {
className={`w-2 h-2 rounded-full flex items-center justify-center bg-current ${getUserColorClassname(user.name)}`}
></div>
<span className="text-sm text-dark-200">
{user.name} {user.id === pad?.ownerId ? '(Code runner)' : ''}
{user.name} {user.id === pad?.ownerId ? '(Code executor)' : ''}
</span>
</div>
))}
Expand All @@ -540,19 +553,20 @@ export function PadPage() {
maxLength={20}
/>
</div>
<div>
<Tooltip
text={
isOwner
? 'You are the code executor of this pad'
: isConnected
? 'You are connected to the server'
: 'You are not connected to the server'
}
>
<span className="text-sm text-dark-300">
{isConnected ? 'Connected' : 'Disconnected'}
{isOwner && isConnected && (
<span
className="ml-2 text-yellow-400"
title="You are the code executor"
>
👑
</span>
)}
{isOwner && isConnected && ' (Code executor)'}
</span>
</div>
</Tooltip>
</div>
</div>

Expand Down Expand Up @@ -582,7 +596,8 @@ export function PadPage() {
className={`w-2 h-2 rounded-full flex items-center justify-center bg-current ${getUserColorClassname(user.name)}`}
></div>
<span className="text-sm text-dark-200">
{user.name} {user.id === pad?.ownerId ? '👑' : ''}
{user.name}{' '}
{user.id === pad?.ownerId ? '(code executor)' : ''}
</span>
</div>
))}
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useState, useRef, useEffect } from 'react';

interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'type'> {
// Tooltip text, if not provided the tooltip will not be displayed
text?: string;
// Tooltip delay in milliseconds
delay?: number;
direction?: 'top' | 'bottom';
children: React.ReactNode;
}

export const Tooltip: React.FC<TooltipProps> = ({
text,
delay = 100,
children,
className = '',
direction = 'top',
...props
}) => {
const [shown, setShown] = useState(false);
const timeoutRef = useRef<number | null>(null);

const handleMouseEnter = React.useCallback(() => {
if (text) {
timeoutRef.current = window.setTimeout(() => {
setShown(true);
}, delay);
}
}, [text, delay]);

const handleMouseLeave = React.useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setShown(false);
}, []);

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

return text ? (
<div
role="tooltip"
{...props}
className={`relative flex ${className}`} // Flex to make children stretch
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
<div
className={`
absolute left-1/2 transform -translate-x-1/2 w-max
${direction === 'top' ? 'bottom-full mb-1' : 'top-full mt-1'}
${shown ? 'opacity-100' : 'opacity-0 pointer-events-none'}
bg-dark-500 text-xs rounded py-1 px-2 z-10
transition-opacity duration-200`}
>
{text}
</div>
</div>
) : (
children
);
};
2 changes: 1 addition & 1 deletion frontend/src/runners/go-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ let singletonGo: Go | undefined = undefined;
let cmd: WebAssembly.WebAssemblyInstantiatedSource;

function postprocessOutput(output: OutputEntry[]): OutputEntry[] {
return output.filter(entry => entry.text !== '# command-line-arguments');
return output.filter((entry) => entry.text !== '# command-line-arguments');
}

function isReady(): boolean {
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/runners/python-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ async function runCode(code: string): Promise<RunResult> {
} catch (errRaw: unknown) {
const err = errRaw as Error;
console.error('Pyodide error:', err);
outputEntries.push({
text: err.message,
type: 'error',
});
outputEntries.push({
text: err.message,
type: 'error',
});
}
pyodide.setStdout({});
pyodide.setStderr({});
Expand Down