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
63 changes: 56 additions & 7 deletions site/app/company/[slug]/SortableQuestionTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';

import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';

import DifficultyBadge from '@/components/DifficultyBadge';
import ProblemPanel from '@/components/ProblemPanel';
import type { Question } from '@/lib/types';
Expand Down Expand Up @@ -43,6 +45,8 @@ export default function SortableQuestionTable({
const [diffFilter, setDiffFilter] = useState<Set<Difficulty>>(new Set());
const [topicFilter, setTopicFilter] = useState<Set<string>>(new Set());
const [active, setActive] = useState<ActiveQuestion | null>(null);
const touchStart = useRef<{ x: number; y: number } | null>(null);
const touchMoved = useRef(false);

const questionsWithTopics = useMemo(
() => questions.map(q => ({ ...q, topics: getTopics(q.title) })),
Expand Down Expand Up @@ -82,7 +86,14 @@ export default function SortableQuestionTable({

const closePanel = useCallback(() => setActive(null), []);

// Some mobile browsers do not reliably synthesize click events for table rows
// after a touch gesture. Activate the same action explicitly at touch end.
const activateQuestion = useCallback((q: QuestionWithTopics) => {
openPanel(q);
}, [openPanel]);

const surpriseMe = useCallback(() => {

if (visible.length === 0) return;
const pick = visible[Math.floor(Math.random() * visible.length)];
setActive({ question: pick, maxFreq });
Expand Down Expand Up @@ -131,7 +142,13 @@ export default function SortableQuestionTable({
)}

{/* Surprise Me — opens a random question from the current filtered list */}
<button type="button" className="surprise-btn" onClick={surpriseMe} title="Open a random question">
<button
type="button"
className="surprise-btn"
onClick={surpriseMe}
onTouchEnd={e => { e.preventDefault(); surpriseMe(); }}
title="Open a random question"
>
🎲 Surprise me
</button>

Expand All @@ -156,9 +173,38 @@ export default function SortableQuestionTable({
<tr
key={q.id || q.title}
className="question-row"
onClick={() => openPanel(q)}
onClick={() => activateQuestion(q)}
onTouchStart={e => {
const touch = e.touches[0];
touchStart.current = { x: touch.clientX, y: touch.clientY };
touchMoved.current = false;
}}
onTouchMove={e => {
const start = touchStart.current;
const touch = e.touches[0];
if (start && touch) {
const movedX = touch.clientX - start.x;
const movedY = touch.clientY - start.y;
if (Math.hypot(movedX, movedY) > 10) touchMoved.current = true;
}
}}
onTouchEnd={e => {
const wasScroll = touchMoved.current;
touchStart.current = null;
touchMoved.current = false;
if (!wasScroll) {
e.preventDefault();
activateQuestion(q);
}
}}
tabIndex={0}
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') openPanel(q); }}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
activateQuestion(q);
}
}}

aria-label={`${q.title} — click to view details`}
style={{ cursor: 'pointer' }}
>
Expand Down Expand Up @@ -198,8 +244,9 @@ export default function SortableQuestionTable({
)}
</div>

{/* Right-side problem panel */}
{active && (
{/* Right-side problem panel. Render at document.body so a mobile
overflow/stacking context cannot clip or hide the fixed panel. */}
{active && typeof document !== 'undefined' && createPortal(
<ProblemPanel
questionId={active.question.id}
title={active.question.title}
Expand All @@ -210,8 +257,10 @@ export default function SortableQuestionTable({
topics={active.question.topics}
currentCompanySlug={companySlug}
onClose={closePanel}
/>
/>,
document.body,
)}

</>
);
}
17 changes: 13 additions & 4 deletions site/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,11 @@ button { color: inherit; }
background: var(--csn-white);
transition: background-color 120ms ease;
}
.question-row { cursor: pointer; }
.question-row {
cursor: pointer;
touch-action: manipulation;
}

.question-row:hover { background: var(--paper-tint) !important; }

.question-table tbody td {
Expand Down Expand Up @@ -880,7 +884,9 @@ button { color: inherit; }

/* ── Surprise Me button ──────────────────────────────────────────────────── */
.surprise-btn {
touch-action: manipulation;
margin-left: auto;

padding: 0.3rem 0.8rem;
border: 1.5px solid var(--csn-brass);
border-radius: var(--radius);
Expand Down Expand Up @@ -1053,9 +1059,12 @@ button { color: inherit; }
.header-nav { grid-column: 1 / -1; grid-row: 2; justify-content: flex-start; padding: 0.4rem 0 0.75rem; border-top: 1px solid var(--rule-faint); }
.search-wrap { grid-column: 2; }
.featured-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.question-table thead th.th-freq,
.question-table tbody td.td-freq { display: none; }
.question-table thead th { top: 6.5rem; }
/* Keep all question metadata available on mobile. The table wrapper
provides horizontal scrolling when the full column set is wider than
the viewport. Header cells remain pinned to the wrapper, not the site
header, so rows cannot scroll behind them. */
.question-table thead th { top: 0; }

}

@media (max-width: 480px) {
Expand Down