Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
96 changes: 96 additions & 0 deletions web-client/src/__tests__/DashboardPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { MemoryRouter } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('@/features/auth', async () => {
const { MOCK_PERSONAS } = await import('@/mocks/personas')

return {
useAuth: () => ({ user: MOCK_PERSONAS.coach }),
}
})

vi.mock('@/app/pages/api/dashboardQueries', async () => {
const { dashboardFixtures } = await import('@/mocks/fixtures/dashboard')

return {
useDashboard: () => ({
data: dashboardFixtures.coach,
isLoading: false,
error: null,
}),
}
})

vi.mock('@/features/sport-events/api/queries', async () => {
const { eventSummaryFixtures } = await import('@/mocks/fixtures')
const { MOCK_PERSONAS } = await import('@/mocks/personas')
const { scopeEvents } = await import('@/mocks/scope')

return {
useEventsList: () => ({
data: scopeEvents(eventSummaryFixtures, MOCK_PERSONAS.coach),
isLoading: false,
error: null,
}),
}
})

vi.mock('@/features/organization/api/queries', () => ({
useSportsList: () => ({
data: [],
isLoading: false,
error: null,
}),
useTeamsList: () => ({
data: [],
isLoading: false,
error: null,
}),
}))

const { DashboardPage } = await import('@/app/pages/DashboardPage')
const { dashboardFixtures } = await import('@/mocks/fixtures/dashboard')

describe('DashboardPage', () => {
let container: HTMLDivElement
let root: Root

beforeEach(() => {
vi.clearAllMocks()
document.body.innerHTML = '<div id="root"></div>'
container = document.getElementById('root') as HTMLDivElement
root = createRoot(container)
})

afterEach(async () => {
await act(async () => {
root.unmount()
})
document.body.innerHTML = ''
})

async function render() {
await act(async () => {
root.render(
<MemoryRouter>
<DashboardPage />
</MemoryRouter>,
)
})
}

it('renders the coach team and roster size from the dashboard fixture', async () => {
const dashboard = dashboardFixtures.coach

expect(dashboard.role).toBe('trainer')

await render()

if (dashboard.role === 'trainer') {
expect(container.textContent).toContain(dashboard.team.name)
expect(container.textContent).toContain(`${dashboard.total_members} roster members`)
}
})
})
20 changes: 19 additions & 1 deletion web-client/src/app/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const initials = (name: string) =>
export function DashboardPage() {
const { view, states } = useDashboardViewModel()
const showBalanceCard = Boolean(view.myBalance || states.myBalance?.isLoading)
const showTeamCard = Boolean(view.myTeam)
const showEventsCards = Boolean(view.myEvents || states.myEvents?.isLoading)
const showFeedbackStat = Boolean(view.myFeedback || states.myFeedback?.isLoading)

Expand All @@ -40,9 +41,10 @@ export function DashboardPage() {
<AdminCountsSection counts={view.adminCounts} state={states.adminCounts} />
)}

{(showBalanceCard || showEventsCards || showFeedbackStat) && (
{(showBalanceCard || showTeamCard || showEventsCards || showFeedbackStat) && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
{showBalanceCard && <BalanceCard balance={view.myBalance} state={states.myBalance} />}
{showTeamCard && <TeamCard team={view.myTeam} />}
{showEventsCards && <EventsCards events={view.myEvents} state={states.myEvents} />}
{showFeedbackStat && (
<FeedbackStat feedback={view.myFeedback} state={states.myFeedback} />
Expand Down Expand Up @@ -85,6 +87,22 @@ function AdminCountsSection({
)
}

function TeamCard({
team,
}: {
team?: NonNullable<ReturnType<typeof useDashboardViewModel>['view']['myTeam']>
}) {
if (!team) return null

return (
<StatCard
label="My Team"
value={team.teamName}
meta={`${team.totalMembers} roster members`}
/>
)
}

function BalanceCard({
balance,
state,
Expand Down
10 changes: 10 additions & 0 deletions web-client/src/app/pages/model/useDashboardViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface DashboardFeedbackSection {
items: DashboardFeedbackItem[]
}

export interface DashboardTeamSection {
teamName: string
totalMembers: number
}

export interface DashboardAdminCountsSection {
totalTeams: number
directors: number
Expand All @@ -74,6 +79,7 @@ export interface DashboardView {
myEvents?: DashboardEventsSection
myBalance?: DashboardBalanceSection
myFeedback?: DashboardFeedbackSection
myTeam?: DashboardTeamSection
adminCounts?: DashboardAdminCountsSection
sports?: DashboardSportSection[]
}
Expand Down Expand Up @@ -297,6 +303,10 @@ function fillSections(
break

case 'trainer':
view.myTeam = {
teamName: data.team.name,
totalMembers: data.total_members,
}
view.myEvents = buildEventsSection(data.upcoming_events, null, org.events)
view.myFeedback = buildFeedbackSection(data.recent_feedback)
states.myEvents = org.eventsState
Expand Down
147 changes: 147 additions & 0 deletions web-client/src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"use client"

import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"

import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"

function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}

function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
}

function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
}

function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/20 duration-100 supports-backdrop-filter:backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-none bg-popover p-6 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}

function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}

function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}

function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg leading-none font-semibold tracking-wider uppercase", className)}
{...props}
/>
)
}

function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("mt-0.5 text-sm leading-relaxed text-muted-foreground", className)}
{...props}
/>
)
}

function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants({ variant: "destructive" }), className)}
{...props}
/>
)
}

function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}

export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
Loading
Loading