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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.1.5] - 2026-02-09

### Added

- Class-aware state foundations: `Classroom` model support, class utilities (`lib/classes.ts`), and class-focused storage/test coverage.
- Shared `ClassSelector` component for class selection across students, generator, play, breakout rooms, and projects.
- Unified quiz editor test coverage for form + import workflows (`components/quizzes/__tests__/quiz-editor-form.test.tsx`).
- Shared select wrapper tests for popup alignment defaults (`components/ui/__tests__/select.test.tsx`).

### Changed

- Student workflow expanded to class-first management: create/select classes, import full class rosters, and manage students in the active class.
- Generator, quiz play, breakout groups, and project lists now run with active-class scoping.
- Quiz editing/import flow consolidated into `QuizEditorForm`; legacy `quiz-import-card` removed.
- Quiz question list card behavior refined for better overflow/height handling.
- Shared `SelectContent` now defaults to popper-style content-fit behavior and supports explicit trigger-aligned positioning via `alignItemWithTrigger={true}` where needed.
- App version updated to `1.1.5` in `package.json`.
- Sidebar version label now reads version dynamically from `package.json` via server layout prop wiring.
- Dependency/version refresh from the `main..HEAD` baseline commits (including `package.json` and `bun.lock` updates from `e9d61c2`).

### Tests

- Expanded reducer/storage/type-guard coverage for class-aware persistence and migration paths.
- Added class utility tests (`lib/__tests__/classes.test.ts`) and updated student/reducer expectations.
- Added select alignment behavior tests in `components/ui/__tests__/select.test.tsx`.

### Documentation

- Updated README and project docs for class-scoped workflows and unified quiz editing/import.
- Updated component docs to describe select popup auto-sizing defaults and trigger-alignment override.

## [1.1.4] - 2026-02-05

### Changed
Expand Down
2 changes: 1 addition & 1 deletion app/breakout-rooms/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const metadata: Metadata = {
*/
export default function Page() {
return (
<div className="flex flex-col gap-4">
<div className="max-w-6xl mx-auto">
<BreakoutGroupsCard />
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion app/generator/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

import { Metadata } from 'next';

import GeneratorCard from '@/components/generator/generator-card';
Expand All @@ -15,7 +17,7 @@ export const metadata: Metadata = {
*/
export default function Page() {
return (
<div className="flex flex-col gap-4">
<div className="max-w-6xl mx-auto">
<GeneratorCard skeleton={<GeneratorCardSkeleton />} />
</div>
);
Expand Down
20 changes: 18 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Metadata } from 'next';

import { Geist, Geist_Mono } from 'next/font/google';
import { cookies } from 'next/headers';

import './globals.css';
import packageJson from '@/package.json';

import AppShell from '@/components/app-shell';
import Footer from '@/components/footer';
Expand Down Expand Up @@ -57,11 +59,20 @@ export const metadata: Metadata = {
* Renders the shared HTML shell and providers for all application routes.
* Wrap page content in this layout so theme, state, sidebar, and footer stay consistent.
*/
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const appVersion = packageJson.version;
const cookieStore = await cookies();
const sidebarCookieValue =
cookieStore.get('teacherbuddy_sidebar_state')?.value ??
cookieStore.get('sidebar_state')?.value ??
cookieStore.get('teacherbuddy:sidebar_state')?.value;
const defaultSidebarOpen =
sidebarCookieValue === undefined ? true : sidebarCookieValue === 'true';

return (
<html lang="en_GB" suppressHydrationWarning>
<head>
Expand Down Expand Up @@ -91,7 +102,12 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<ThemeProvider attribute="class" defaultTheme="dark">
<AppStoreProvider>
<AppShell footer={<Footer />}>{children}</AppShell>
<AppShell
appVersion={appVersion}
defaultSidebarOpen={defaultSidebarOpen}
footer={<Footer />}>
{children}
</AppShell>
<PrivacyNotice />
<Toaster closeButton position="bottom-center" />
</AppStoreProvider>
Expand Down
2 changes: 1 addition & 1 deletion app/play/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const metadata: Metadata = {
*/
export default function Page() {
return (
<div className="flex flex-col gap-4">
<div className="max-w-6xl mx-auto">
<QuizPlayCard skeleton={<QuizPlayCardSkeleton />} />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion app/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const metadata: Metadata = {
export default function Page() {
return (
<div className="flex flex-col gap-4">
<ProjectListBuilder />
<ProjectListView />
<ProjectListBuilder />
</div>
);
}
6 changes: 1 addition & 5 deletions app/quizzes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,5 @@ export const metadata: Metadata = {
* Provides the editor workflow with a skeleton shown before hydration.
*/
export default function Page() {
return (
<div className="flex flex-col gap-4">
<QuizEditor skeleton={<QuizEditorSkeleton />} />
</div>
);
return <QuizEditor skeleton={<QuizEditorSkeleton />} />;
}
12 changes: 9 additions & 3 deletions components/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ import Header from './header';
/**
* Global app shell that renders the sidebar, header, and page content.
* Styled to match the design-6 command center aesthetic with phase-colored navigation.
* Provide `appVersion` from server layout so the sidebar version stays in sync with package metadata.
* Provide `defaultSidebarOpen` from server cookie state so refreshes preserve sidebar preference.
*/
export default function AppShell({
appVersion,
defaultSidebarOpen,
children,
footer,
}: {
appVersion: string;
defaultSidebarOpen: boolean;
children: React.ReactNode;
footer?: React.ReactNode;
}) {
Expand All @@ -46,7 +52,7 @@ export default function AppShell({
};

return (
<SidebarProvider>
<SidebarProvider defaultOpen={defaultSidebarOpen}>
<Sidebar variant="inset" collapsible="icon">
<SidebarHeader>
<SidebarMenu>
Expand Down Expand Up @@ -86,7 +92,7 @@ export default function AppShell({
<div className="flex items-center justify-between gap-2 px-2 text-sm text-muted-foreground group-data-[collapsible=icon]:justify-center">
<span className="flex items-center gap-1.5 group-data-[collapsible=icon]:text-xs">
<SparklesIcon className="size-3 text-primary group-data-[collapsible=icon]:hidden" />
v1.1.4
v{appVersion}
</span>
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-primary/60 group-data-[collapsible=icon]:hidden">
Classroom
Expand All @@ -100,7 +106,7 @@ export default function AppShell({
meta={meta}
info={{ currentPath: pathname, pages: PAGE_INFOS }}
/>
<main className="flex-1 px-4 py-6 md:px-6 lg:px-8 container mx-auto max-w-6xl h-dvh">
<main className="flex-1 px-4 py-6 md:px-6 lg:px-8 container mx-auto h-dvh">
{children}
</main>
{footer ?? null}
Expand Down
8 changes: 4 additions & 4 deletions components/play/quiz-play-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,25 @@ export default function QuizPlayCard({
</div>
</CardContent>
<CardFooter className="px-6 xl:px-8">
<div className="flex flex-col gap-2 sm:flex-row">
<div className="flex w-full flex-col gap-2 sm:flex-row">
<Button
onClick={actions.drawQuizPair}
disabled={!canDraw}
className="h-9 font-semibold text-base sm:min-w-32">
className="h-9 w-full font-semibold text-base sm:min-w-32 sm:w-auto">
Draw Student + Question
</Button>
<Button
variant="secondary"
onClick={actions.revealAnswer}
disabled={!currentQuestion || state.domain.quizPlay.answerRevealed}
className="h-9 font-semibold text-base sm:min-w-32">
className="h-9 w-full font-semibold text-base sm:min-w-32 sm:w-auto">
Reveal Answer
</Button>
<Button
variant="ghost"
onClick={actions.resetQuizPlay}
disabled={!selectedQuizId}
className="h-9 font-semibold text-base sm:min-w-32">
className="h-9 w-full font-semibold text-base sm:min-w-32 sm:w-auto">
Reset Round
</Button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/projects/project-list-builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ export default function ProjectListBuilder() {
<Button
type="button"
onClick={handleCreateList}
className="h-9 font-semibold text-base">
className="h-9 w-full font-semibold text-base sm:w-auto">
Save Project List
</Button>
</div>
Expand Down
6 changes: 3 additions & 3 deletions components/quizzes/quiz-editor-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,8 @@ export default function QuizEditorForm({ quiz, quizId }: QuizEditorFormProps) {

return (
<>
<div ref={builderCardRef}>
<Card className="relative h-full overflow-hidden rounded-xl border-border/50 py-6 shadow-md lg:gap-6 xl:gap-8 xl:py-8">
<div ref={builderCardRef} className="min-w-0 w-full">
<Card className="relative h-full min-w-0 w-full overflow-hidden rounded-xl border-border/50 py-6 shadow-md lg:gap-6 xl:gap-8 xl:py-8">
<div
className="absolute left-0 top-0 h-full w-1 rounded-l-xl"
style={{ backgroundColor: 'var(--chart-3)', opacity: 0.6 }}
Expand Down Expand Up @@ -675,7 +675,7 @@ export default function QuizEditorForm({ quiz, quizId }: QuizEditorFormProps) {
</div>

<Card
className="relative flex min-h-0 flex-col overflow-hidden rounded-xl border-border/50 py-6 shadow-md lg:gap-6 xl:gap-8 xl:py-8"
className="relative flex min-h-0 min-w-0 w-full flex-col overflow-hidden rounded-xl border-border/50 py-6 shadow-md lg:gap-6 xl:gap-8 xl:py-8"
style={
builderCardHeight
? { maxHeight: `${builderCardHeight}px` }
Expand Down
2 changes: 1 addition & 1 deletion components/quizzes/quiz-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function QuizEditor({
}

return (
<div className="grid items-start gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
{/* Key pattern: reset all form state when quiz changes */}
<QuizEditorForm
key={activeQuizId ?? "new"}
Expand Down
7 changes: 6 additions & 1 deletion components/students/student-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export default function StudentTable({
(student) => student.id === editingStudentId,
) ?? null)
: null;
const selectedEditClass = editClassId
? (state.persisted.classes.find((entry) => entry.id === editClassId) ?? null)
: null;

/**
* Keeps the student list card height aligned to the form card on desktop widths.
Expand Down Expand Up @@ -414,7 +417,9 @@ export default function StudentTable({
if (editError) setEditError(null);
}}>
<SelectTrigger id="edit-student-class">
<SelectValue placeholder="Select class" />
<SelectValue placeholder="Select class">
{selectedEditClass?.name ?? 'Select class'}
</SelectValue>
</SelectTrigger>
<SelectContent>
{state.persisted.classes.map((entry) => (
Expand Down
59 changes: 59 additions & 0 deletions components/ui/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { render, waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';

/**
* Renders an open select so popup-level behavior can be asserted in tests.
*
* @param alignItemWithTrigger Optional popup alignment override.
* @returns Promise resolving to the rendered popup element.
*/
async function renderOpenSelect(
alignItemWithTrigger?: boolean,
): Promise<HTMLElement> {
render(
<Select defaultOpen defaultValue="class-a">
<SelectTrigger aria-label="Class select">
<SelectValue placeholder="Select class" />
</SelectTrigger>
<SelectContent
{...(alignItemWithTrigger === undefined
? {}
: { alignItemWithTrigger })}>
<SelectItem value="class-a">Class A</SelectItem>
<SelectItem value="class-b">Class B</SelectItem>
</SelectContent>
</Select>,
);

let popup: HTMLElement | null = null;
await waitFor(() => {
popup = document.querySelector<HTMLElement>('[data-slot="select-content"]');
expect(popup).not.toBeNull();
});

if (!popup) {
throw new Error('Select popup did not render');
}

return popup;
}

describe('SelectContent', () => {
it('defaults to popper-style popup behavior', async () => {
const popup = await renderOpenSelect();
expect(popup).toHaveAttribute('data-align-trigger', 'false');
});

it('supports explicit trigger-aligned popup behavior', async () => {
const popup = await renderOpenSelect(true);
expect(popup).toHaveAttribute('data-align-trigger', 'true');
});
});
13 changes: 10 additions & 3 deletions components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,21 @@ function SelectTrigger({
)
}

/**
* Renders the positioned select popup and option list.
* By default, the popup uses popper-style positioning so menu height tracks
* content naturally while still respecting the shared max-height cap.
* Callers can opt into trigger-aligned positioning with
* `alignItemWithTrigger={true}` when needed.
*/
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
alignItemWithTrigger = false,
...props
}: SelectPrimitive.Popup.Props &
Pick<
Expand All @@ -87,9 +94,9 @@ function SelectContent({
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-32 rounded-lg shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 h-fit max-h-[min(18rem,var(--available-height))] w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", className )}
{...props}
>
<SelectScrollUpButton />
{alignItemWithTrigger ? <SelectScrollUpButton /> : null}
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
{alignItemWithTrigger ? <SelectScrollDownButton /> : null}
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
Expand Down
Loading
Loading