diff --git a/frontend/src/pages/AnalysisPage.css b/frontend/src/pages/AnalysisPage.css index 44eb7b2..69cc4f9 100644 --- a/frontend/src/pages/AnalysisPage.css +++ b/frontend/src/pages/AnalysisPage.css @@ -1,12 +1,19 @@ /* Session rail beside the analysis, as in a Qbank analysis view. */ -.an-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; } -.an-layout.rail-closed { grid-template-columns: 52px minmax(0, 1fr); } -.an-rail { position: sticky; top: 16px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; } +/* The rail runs the full height of the viewport against the left edge, rather + than sitting in a card, so a long session list is one continuous column. */ +.an-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 28px; align-items: start; } +.an-layout.rail-closed { grid-template-columns: 48px minmax(0, 1fr); } +.an-rail { + position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; + background: var(--card-bg); border-right: 1px solid var(--border); + margin-left: calc(50% - 50vw); padding-left: max(0px, calc(50vw - 50%)); + box-sizing: content-box; +} .an-rail-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border); } .an-rail-head h2 { margin: 0; font-size: .92rem; font-weight: 650; white-space: nowrap; } .an-layout.rail-closed .an-rail-head h2 { display: none; } .an-rail-head button { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 1.1rem; line-height: 1; padding: 2px 4px; } -.an-rail-list { list-style: none; margin: 0; padding: 0; max-height: 70vh; overflow-y: auto; } +.an-rail-list { list-style: none; margin: 0; padding: 0; flex: 1; overflow-y: auto; } .an-rail-list > li { border-bottom: 1px solid var(--border); } .an-rail-list > li:last-child { border-bottom: 0; } .an-rail-list a { display: flex; flex-direction: column; gap: 7px; padding: 12px 14px; text-decoration: none; color: var(--text); } @@ -17,7 +24,10 @@ @media (max-width: 1100px) { .an-layout, .an-layout.rail-closed { grid-template-columns: 1fr; } - .an-rail { position: static; } + .an-rail { + position: static; height: auto; margin-left: 0; padding-left: 0; + border-right: 0; border: 1px solid var(--border); border-radius: 12px; + } .an-rail-list { max-height: 260px; } } diff --git a/frontend/src/pages/QuestionBankPage.jsx b/frontend/src/pages/QuestionBankPage.jsx index 388c15a..d188cb4 100644 --- a/frontend/src/pages/QuestionBankPage.jsx +++ b/frontend/src/pages/QuestionBankPage.jsx @@ -6,6 +6,7 @@ import Dialog from '../components/Dialog' import CategoryTree from '../components/CategoryTree' import FacetPicker, { FacetRow } from '../components/FacetPicker' import './CustomQuizPage.css' // facet row + picker panel styles +import './QuestionBankPage.css' import { useDialog } from '../hooks/useDialog' const TeachChat = lazy(() => import('../components/TeachChat')) diff --git a/frontend/src/pages/QuizzesPage.css b/frontend/src/pages/QuizzesPage.css index 755ef23..5b1b295 100644 --- a/frontend/src/pages/QuizzesPage.css +++ b/frontend/src/pages/QuizzesPage.css @@ -261,3 +261,6 @@ .qz-grid { grid-template-columns: 1fr; } } + +.qz-seemore { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 6px 0 4px; } +@media (max-width: 640px) { .qz-seemore .btn { flex: 1; text-align: center; } } diff --git a/frontend/src/pages/QuizzesPage.jsx b/frontend/src/pages/QuizzesPage.jsx index 87b844c..70d42bb 100644 --- a/frontend/src/pages/QuizzesPage.jsx +++ b/frontend/src/pages/QuizzesPage.jsx @@ -202,7 +202,7 @@ function QuestionStudyModal({ question, query, onClose }) { } /* ── One management row: progress, primary action, and the action menu ── */ -function SessionRow({ row, categories, isModerator, onChanged, onDelete }) { +function SessionRow({ row, isModerator, onChanged, onDelete }) { const [menuOpen, setMenuOpen] = useState(false) const [renaming, setRenaming] = useState(false) const [draftTitle, setDraftTitle] = useState(row.title) @@ -254,16 +254,6 @@ function SessionRow({ row, categories, isModerator, onChanged, onDelete }) { finally { setBusy(false) } } - const assignCategory = async (value) => { - closeMenu(); setBusy(true); setError('') - const catId = value === '' ? null : Number(value) - try { - await api.patch(`/categories/quizzes/${row.quiz_id}`, null, { params: catId == null ? {} : { category_id: catId } }) - onChanged(row.quiz_id, { category_id: catId, category_name: categories.find(c => c.id === catId)?.name ?? null }) - } catch (err) { setError(err.response?.data?.detail || 'Could not move this test') } - finally { setBusy(false) } - } - return (
@@ -347,15 +337,6 @@ function SessionRow({ row, categories, isModerator, onChanged, onDelete }) { - } {canManage && <>
@@ -398,18 +379,18 @@ function QuizCard({ quiz, isModerator, onOpenSessions }) { export default function QuizzesPage() { const [rows, setRows] = useState([]) - const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) const [tab, setTab] = useState('sessions') const [stateFilter, setStateFilter] = useState('all') + // The full history lives on the analysis page; this is a launcher. + const [showAllSessions, setShowAllSessions] = useState(false) + const SESSION_PREVIEW = 6 const [searchQuery, setSearchQuery] = useState('') const [searchMode, setSearchMode] = useState('all') const [searchResults, setSearchResults] = useState(null) const [searching, setSearching] = useState(false) const [expandedSearch, setExpandedSearch] = useState(false) const [studyQuestion, setStudyQuestion] = useState(null) - const [newCatName, setNewCatName] = useState('') - const [addingCat, setAddingCat] = useState(false) const { dialogProps, openAlert } = useDialog() const debounceRef = useRef(null) const { user } = useAuth() @@ -417,13 +398,8 @@ export default function QuizzesPage() { const isModerator = user?.role === 'admin' || user?.role === 'moderator' useEffect(() => { - Promise.all([ - api.get('/quizzes/sessions'), - api.get('/categories/'), - ]).then(([sRes, cRes]) => { - setRows(Array.isArray(sRes.data) ? sRes.data : []) - setCategories(Array.isArray(cRes.data) ? cRes.data : []) - }).catch(console.error).finally(() => setLoading(false)) + api.get('/quizzes/sessions') + .then(res => setRows(Array.isArray(res.data) ? res.data : [])).catch(console.error).finally(() => setLoading(false)) }, []) useEffect(() => { @@ -451,24 +427,6 @@ export default function QuizzesPage() { const patchRow = (quizId, patch) => setRows(prev => prev.map(r => (r.quiz_id === quizId ? { ...r, ...patch } : r))) - const addCategory = async () => { - if (!newCatName.trim()) return - try { - const res = await api.post('/categories/', { name: newCatName.trim() }) - setCategories(prev => [...prev, res.data]) - setNewCatName('') - setAddingCat(false) - } catch (err) { await openAlert(err.response?.data?.detail || 'Failed to create category', { title: 'Error' }) } - } - - const deleteCategory = async (catId) => { - try { - await api.delete(`/categories/${catId}`) - setCategories(prev => prev.filter(c => c.id !== catId)) - setRows(prev => prev.map(r => (r.category_id === catId ? { ...r, category_id: null, category_name: null } : r))) - } catch { } - } - const counts = useMemo(() => ({ all: rows.length, in_progress: rows.filter(r => r.state === 'in_progress').length, @@ -481,16 +439,21 @@ export default function QuizzesPage() { [rows, stateFilter], ) + const previewRows = useMemo( + () => (showAllSessions ? visibleRows : visibleRows.slice(0, SESSION_PREVIEW)), + [visibleRows, showAllSessions], + ) + const dayGroups = useMemo(() => { const groups = [] - for (const row of visibleRows) { + for (const row of previewRows) { const label = dayLabel(row.last_activity) const last = groups[groups.length - 1] if (last && last.label === label) last.rows.push(row) else groups.push({ label, rows: [row] }) } return groups - }, [visibleRows]) + }, [previewRows]) const libraryGroups = useMemo(() => { const byCat = new Map() @@ -619,11 +582,6 @@ export default function QuizzesPage() { - {isModerator && ( - - )}
{tab === 'sessions' && ( @@ -649,13 +607,32 @@ export default function QuizzesPage() {

{group.label}

{group.rows.map(row => ( - ))}
))} + {visibleRows.length > SESSION_PREVIEW && ( +
+ {showAllSessions ? ( + + ) : ( + <> + + + Full session history → + + + )} +
+ )} + ) @@ -677,39 +654,6 @@ export default function QuizzesPage() { )) )} - {tab === 'manage' && isModerator && ( -
-
-

Test categories

- -
- {addingCat && ( -
- setNewCatName(e.target.value)} - placeholder="Category name…" aria-label="New category name" - onKeyDown={e => e.key === 'Enter' && addCategory()} - style={{ flex: 1, minWidth: 170, padding: '7px 12px', border: '1px solid var(--border)', borderRadius: 8, fontSize: '0.875rem', background: 'var(--input-bg)', color: 'var(--text)' }} /> - - -
- )} - {categories.length === 0 ? ( -

No categories yet.

- ) : ( -
- {categories.map(cat => ( -
- {cat.name} - ({cat.quiz_count}) - deleteCategory(cat.id)} /> -
- ))} -
- )} -
- )} )}