feat(storybook): add TeacherClassroomCard stories
Add Storybook stories for the TeacherClassroomCard component with 14 variants covering: - Basic usage (enrolled, in-classroom, active views) - View count variations (empty, large, none present) - Custom settings (expiry time, long names) - Dark theme variants - Responsive width testing Also exports TeacherClassroomCard and TeacherClassroomCardProps from StudentFilterBar.tsx for use in stories. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8f55c9f4f
commit
a5e5788fa9
|
|
@ -655,7 +655,7 @@ const EXPIRY_OPTIONS = [
|
|||
{ value: 120, label: '2 hours' },
|
||||
] as const
|
||||
|
||||
interface TeacherClassroomCardProps {
|
||||
export interface TeacherClassroomCardProps {
|
||||
classroom: Classroom
|
||||
currentView: StudentView
|
||||
onViewChange: (view: StudentView) => void
|
||||
|
|
@ -674,7 +674,7 @@ interface TeacherClassroomCardProps {
|
|||
* │ [📋 Enrolled (10)] [🏫 Present (5)] [🎯 Active (2)] │
|
||||
* └──────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
function TeacherClassroomCard({
|
||||
export function TeacherClassroomCard({
|
||||
classroom,
|
||||
currentView,
|
||||
onViewChange,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,489 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { ThemeProvider } from '@/contexts/ThemeContext'
|
||||
import type { Classroom } from '@/db/schema'
|
||||
import type { StudentView } from './ViewSelector'
|
||||
import { TeacherClassroomCard } from './StudentFilterBar'
|
||||
import { css } from '../../../styled-system/css'
|
||||
|
||||
// Create a fresh query client for each story
|
||||
function createQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Story wrapper with providers
|
||||
function StoryWrapper({
|
||||
children,
|
||||
theme = 'light',
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
theme?: 'light' | 'dark'
|
||||
}) {
|
||||
const queryClient = createQueryClient()
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<div
|
||||
data-theme={theme}
|
||||
className={css({
|
||||
minHeight: '200px',
|
||||
padding: '2rem',
|
||||
backgroundColor: theme === 'dark' ? '#1a1a2e' : '#f5f5f5',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Mock classroom data
|
||||
const mockClassroom: Classroom = {
|
||||
id: 'classroom-1',
|
||||
teacherId: 'teacher-1',
|
||||
name: "Mrs. Smith's Class",
|
||||
code: 'ABC123',
|
||||
createdAt: new Date('2024-01-15'),
|
||||
entryPromptExpiryMinutes: null,
|
||||
}
|
||||
|
||||
const mockClassroomWithCustomExpiry: Classroom = {
|
||||
...mockClassroom,
|
||||
id: 'classroom-2',
|
||||
name: 'Math 101',
|
||||
code: 'MATH42',
|
||||
entryPromptExpiryMinutes: 60,
|
||||
}
|
||||
|
||||
const mockClassroomLongName: Classroom = {
|
||||
...mockClassroom,
|
||||
id: 'classroom-3',
|
||||
name: 'Advanced Mathematics and Problem Solving - Period 3',
|
||||
code: 'ADVMT3',
|
||||
}
|
||||
|
||||
// Available views for teacher
|
||||
const teacherViews: StudentView[] = ['enrolled', 'in-classroom', 'in-classroom-active']
|
||||
|
||||
const meta: Meta<typeof TeacherClassroomCard> = {
|
||||
title: 'Practice/TeacherClassroomCard',
|
||||
component: TeacherClassroomCard,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof TeacherClassroomCard>
|
||||
|
||||
// =============================================================================
|
||||
// Basic Usage
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Default state with enrolled view selected
|
||||
*/
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* With "In Classroom" view selected (showing present students)
|
||||
*/
|
||||
export const InClassroomView: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('in-classroom')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* With "Active Sessions" view selected
|
||||
*/
|
||||
export const ActiveSessionsView: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('in-classroom-active')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// View Count Variations
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Empty classroom - no students enrolled yet
|
||||
*/
|
||||
export const EmptyClassroom: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 0,
|
||||
'in-classroom': 0,
|
||||
'in-classroom-active': 0,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Large classroom with many students
|
||||
*/
|
||||
export const LargeClassroom: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 35,
|
||||
'in-classroom': 28,
|
||||
'in-classroom-active': 15,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* No students present (all enrolled but none in classroom)
|
||||
*/
|
||||
export const NonePresent: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 20,
|
||||
'in-classroom': 0,
|
||||
'in-classroom-active': 0,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Custom Settings
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Classroom with custom entry prompt expiry time
|
||||
*/
|
||||
export const CustomExpiryTime: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroomWithCustomExpiry}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 10,
|
||||
'in-classroom': 5,
|
||||
'in-classroom-active': 2,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Classroom with a very long name (tests truncation)
|
||||
*/
|
||||
export const LongClassName: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroomLongName}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 15,
|
||||
'in-classroom': 10,
|
||||
'in-classroom-active': 4,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Without Add Student Button
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Without the add student button (read-only view)
|
||||
*/
|
||||
export const WithoutAddButton: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Theme
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Dark theme variant
|
||||
*/
|
||||
export const DarkTheme: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper theme="dark">
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark theme with "In Classroom" view
|
||||
*/
|
||||
export const DarkThemeInClassroom: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('in-classroom')
|
||||
return (
|
||||
<StoryWrapper theme="dark">
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark theme - empty classroom
|
||||
*/
|
||||
export const DarkThemeEmpty: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper theme="dark">
|
||||
<div className={css({ width: '400px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 0,
|
||||
'in-classroom': 0,
|
||||
'in-classroom-active': 0,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Responsive Widths
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Narrow container (tests responsiveness)
|
||||
*/
|
||||
export const NarrowContainer: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '300px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Wide container
|
||||
*/
|
||||
export const WideContainer: Story = {
|
||||
render: () => {
|
||||
const [currentView, setCurrentView] = useState<StudentView>('enrolled')
|
||||
return (
|
||||
<StoryWrapper>
|
||||
<div className={css({ width: '600px' })}>
|
||||
<TeacherClassroomCard
|
||||
classroom={mockClassroom}
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
availableViews={teacherViews}
|
||||
viewCounts={{
|
||||
enrolled: 12,
|
||||
'in-classroom': 8,
|
||||
'in-classroom-active': 3,
|
||||
}}
|
||||
onAddStudentToClassroom={() => console.log('Add student clicked')}
|
||||
/>
|
||||
</div>
|
||||
</StoryWrapper>
|
||||
)
|
||||
},
|
||||
}
|
||||
Loading…
Reference in New Issue