Skip to content

Commit 3ae9e1d

Browse files
Edwin ChanEdwin Chan
authored andcommitted
Make student rows clickable and responsive
1 parent 79df1de commit 3ae9e1d

5 files changed

Lines changed: 192 additions & 28 deletions

File tree

CENTRAL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Stack: Next.js, NextAuth (Google + dev bypass), Prisma, KaTeX for math, Lucide i
3333

3434
### Recently landed
3535

36+
- **Clickable, contained Students table** — the admin Students list no longer has a redundant View action column. Each row navigates to the student detail page, while the student name remains a native keyboard-accessible link. Route-specific fixed table sizing prevents desktop horizontal scrolling, and narrower viewports switch to labeled two-column student cards instead of a minimum-width scrolling table (sources: `app/admin/students/page.tsx`, `app/admin/students/student-table-row.tsx`, `app/globals.css`).
3637
- **Unframed profile summary grid** — the Strongest topics, Recent completions, and Bookmarked sets cards now sit directly in the profile layout without a redundant outer `profile-section` card around their existing individual borders (source: `app/users/[username]/page.tsx`).
3738
- **Immediate content rendering** — removed the global `AnimeRouteEffects` stagger, analytics card/bar entrance pass, trend-chart drawing animation, page fade, smooth scrolling, FTW result-row fade, and floating confetti motion. Routes and analytics now render at their final state immediately. Anime.js remains only for the shared math loading indicator and typewriter caret; ordinary hover/focus and functional feedback transitions remain (sources: `app/layout.tsx`, `app/admin/analytics/page.tsx`, `app/admin/sets/[id]/analytics/page.tsx`, `app/admin/analytics/trend-chart.tsx`, `app/globals.css`).
3839
- **Consistent page exits and sidebar state** — page headers and completion states use the shared `PageBackLink` control with one explicit `Back to …` label, one canonical 44 px control size, and a logical parent destination. The Classes landing page now includes its missing `Back to Dashboard` control, and nested class pages link directly back to `/classes`. Detail pages no longer present competing arrow buttons for both Dashboard and their parent collection. Sidebar activation now selects one most-specific segment-aware route, so the signed-in user's `/users/<username>` page highlights only **My Profile**, while another user's profile highlights **Users**. Active links expose `aria-current="page"`; no schema or deploy change is required (sources: `app/page-back-link.tsx`, `lib/sidebar-navigation.ts`, `app/site-sidebar-nav.tsx`).

DBSMO/Projects/dbsmo/Components.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ This note maps important [[dbsmo]] UI/components to their source files and usage
7272

7373
## Admin Analytics, Feedback, Audit
7474

75+
- `AdminStudentsPage` in `app/admin/students/page.tsx`: paginated/searchable student performance table. It uses a route-specific fixed table layout without horizontal scrolling at desktop widths and becomes labeled cards on narrower viewports. The action column is omitted; `StudentTableRow` in `app/admin/students/student-table-row.tsx` makes row whitespace navigate while retaining one native student-detail link for keyboard and assistive-technology access.
7576
- `AnalyticsOverviewPage` in `app/admin/analytics/page.tsx`: server route that builds analytics summary/trend/filter options.
7677
- `AnalyticsFilters` and local `SearchableSelect` in `app/admin/analytics/filters.tsx`: client filter bar that edits query params and supports searchable dropdowns/date range.
7778
- Analytics metrics in `app/admin/analytics/page.tsx` and `app/admin/sets/[id]/analytics/page.tsx` render directly at their final widths and values; there is no staggered entrance pass.

app/admin/students/page.tsx

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Link from "next/link";
2-
import { Download, ExternalLink, Search, Users } from "lucide-react";
2+
import { Download, Search, Users } from "lucide-react";
33
import { getServerSession } from "next-auth/next";
44
import { redirect } from "next/navigation";
55
import { prisma } from "@/lib/db";
@@ -9,6 +9,7 @@ import { hasPermission } from "@/lib/permissions";
99
import { SearchSuggestInput } from "@/app/search-suggest-input";
1010
import { isVisibleToStudent } from "@/lib/visibility";
1111
import { PageBackLink } from "@/app/page-back-link";
12+
import { StudentTableRow } from "./student-table-row";
1213

1314
export const dynamic = "force-dynamic";
1415

@@ -172,7 +173,7 @@ export default async function AdminStudentsPage({
172173
</div>
173174
<Users size={20} />
174175
</div>
175-
<div className="table-wrap">
176+
<div className="table-wrap students-table-wrap">
176177
<table className="students-table">
177178
<thead>
178179
<tr>
@@ -186,35 +187,42 @@ export default async function AdminStudentsPage({
186187
<th>Attempts</th>
187188
<th>Joined</th>
188189
<th>Last active</th>
189-
<th></th>
190190
</tr>
191191
</thead>
192192
<tbody>
193-
{paginatedRows.map((row) => (
194-
<tr key={row.id}>
195-
<td>
196-
<strong>{row.name ?? "—"}</strong>
197-
</td>
198-
<td>{row.email}</td>
199-
<td>{row.group ?? "—"}</td>
200-
<td>{row.performance.attemptedSets}</td>
201-
<td>{row.performance.masteryIndex.toFixed(1)}</td>
202-
<td>{row.performance.bestSetAverage.toFixed(1)}%</td>
203-
<td>{performanceEvidenceLabel(row.performance.evidence)}</td>
204-
<td>{row.attempts.length}</td>
205-
<td>{row.createdAt.toLocaleDateString()}</td>
206-
<td>{row.lastActive ? row.lastActive.toLocaleDateString() : "—"}</td>
207-
<td>
208-
<Link
209-
className="secondary-action compact"
210-
href={`/admin/students/${row.id}`}
211-
>
212-
<ExternalLink size={14} />
213-
View
214-
</Link>
215-
</td>
216-
</tr>
217-
))}
193+
{paginatedRows.map((row) => {
194+
const href = `/admin/students/${row.id}`;
195+
return (
196+
<StudentTableRow href={href} key={row.id}>
197+
<td data-label="Name">
198+
<Link
199+
aria-label={`Open ${row.name ?? row.email}`}
200+
className="student-row-primary-link"
201+
href={href}
202+
>
203+
{row.name ?? "—"}
204+
</Link>
205+
</td>
206+
<td data-label="Email">{row.email}</td>
207+
<td data-label="Group">{row.group ?? "—"}</td>
208+
<td data-label="Sets">{row.performance.attemptedSets}</td>
209+
<td data-label="Mastery index">
210+
{row.performance.masteryIndex.toFixed(1)}
211+
</td>
212+
<td data-label="Best-set avg">
213+
{row.performance.bestSetAverage.toFixed(1)}%
214+
</td>
215+
<td data-label="Evidence">
216+
{performanceEvidenceLabel(row.performance.evidence)}
217+
</td>
218+
<td data-label="Attempts">{row.attempts.length}</td>
219+
<td data-label="Joined">{row.createdAt.toLocaleDateString()}</td>
220+
<td data-label="Last active">
221+
{row.lastActive ? row.lastActive.toLocaleDateString() : "—"}
222+
</td>
223+
</StudentTableRow>
224+
);
225+
})}
218226
</tbody>
219227
</table>
220228
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client";
2+
3+
import type { MouseEvent, ReactNode } from "react";
4+
import { useRouter } from "next/navigation";
5+
6+
type StudentTableRowProps = {
7+
children: ReactNode;
8+
href: string;
9+
};
10+
11+
export function StudentTableRow({ children, href }: StudentTableRowProps) {
12+
const router = useRouter();
13+
14+
function openStudent(event: MouseEvent<HTMLTableRowElement>) {
15+
if ((event.target as HTMLElement).closest("a, button, input, select, textarea")) {
16+
return;
17+
}
18+
router.push(href);
19+
}
20+
21+
return (
22+
<tr className="student-table-row" onClick={openStudent}>
23+
{children}
24+
</tr>
25+
);
26+
}

app/globals.css

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12281,6 +12281,134 @@ html:is(.light, .dark) .notfound-digit-zero {
1228112281
background: color-mix(in srgb, var(--paper-blue) 55%, transparent);
1228212282
}
1228312283

12284+
.students-table-wrap {
12285+
overflow-x: clip;
12286+
}
12287+
12288+
.students-table {
12289+
width: 100%;
12290+
min-width: 0;
12291+
table-layout: fixed;
12292+
}
12293+
12294+
.students-table th,
12295+
.students-table td {
12296+
padding-inline: clamp(8px, 0.8vw, 14px);
12297+
white-space: normal;
12298+
overflow-wrap: anywhere;
12299+
}
12300+
12301+
.students-table th:nth-child(1) {
12302+
width: 13%;
12303+
}
12304+
12305+
.students-table th:nth-child(2) {
12306+
width: 20%;
12307+
}
12308+
12309+
.students-table th:nth-child(3) {
12310+
width: 7%;
12311+
}
12312+
12313+
.students-table th:nth-child(4) {
12314+
width: 5%;
12315+
}
12316+
12317+
.students-table th:nth-child(5),
12318+
.students-table th:nth-child(6) {
12319+
width: 10%;
12320+
}
12321+
12322+
.students-table th:nth-child(7) {
12323+
width: 11%;
12324+
}
12325+
12326+
.students-table th:nth-child(8) {
12327+
width: 7%;
12328+
}
12329+
12330+
.students-table th:nth-child(9),
12331+
.students-table th:nth-child(10) {
12332+
width: 8.5%;
12333+
}
12334+
12335+
.student-table-row {
12336+
cursor: pointer;
12337+
}
12338+
12339+
.student-row-primary-link {
12340+
position: relative;
12341+
z-index: 1;
12342+
color: var(--color-text-strong);
12343+
font-weight: 800;
12344+
text-decoration: none;
12345+
}
12346+
12347+
.student-row-primary-link:focus-visible {
12348+
outline: 2px solid var(--marker-cyan);
12349+
outline-offset: 4px;
12350+
}
12351+
12352+
@media (max-width: 1050px) {
12353+
.students-table-wrap {
12354+
overflow-x: visible;
12355+
padding: 12px 14px 16px;
12356+
}
12357+
12358+
.students-table,
12359+
.students-table tbody {
12360+
display: block;
12361+
width: 100%;
12362+
min-width: 0;
12363+
}
12364+
12365+
.students-table thead {
12366+
display: none;
12367+
}
12368+
12369+
.students-table tbody {
12370+
display: grid;
12371+
gap: 12px;
12372+
}
12373+
12374+
.students-table .student-table-row {
12375+
display: grid;
12376+
grid-template-columns: repeat(2, minmax(0, 1fr));
12377+
gap: 0;
12378+
padding: 10px;
12379+
border: 1.5px solid var(--ink-soft);
12380+
border-radius: 14px 11px 15px 10px;
12381+
background: var(--paper-raised);
12382+
box-shadow: var(--drawn-shadow-small);
12383+
}
12384+
12385+
.students-table td {
12386+
display: grid;
12387+
gap: 3px;
12388+
min-width: 0;
12389+
padding: 8px;
12390+
border: 0;
12391+
}
12392+
12393+
.students-table td::before {
12394+
content: attr(data-label);
12395+
color: var(--ink-muted);
12396+
font-family: var(--font-inter), "Inter", sans-serif;
12397+
font-size: 0.68rem;
12398+
font-weight: 800;
12399+
text-transform: uppercase;
12400+
}
12401+
12402+
.students-table td:nth-child(1),
12403+
.students-table td:nth-child(2) {
12404+
grid-column: 1 / -1;
12405+
}
12406+
12407+
.students-table td:nth-child(1) {
12408+
font-size: 1rem;
12409+
}
12410+
}
12411+
1228412412
/* Preserve horizontal math scrolling without painting a scrollbar under every formula. */
1228512413
.statement-math-inline,
1228612414
.statement-math-block {

0 commit comments

Comments
 (0)