Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/courses/calendar/page.tsx
T. Zehetbauer abac22feb1
Some checks failed
Workflow / ⚫️ Test (push) Has been cancelled
Workflow / ʦ TypeScript (push) Has been cancelled
feat: enhance accessibility and testing with data-test attributes and improve error handling
2026-04-01 10:46:44 +02:00

257 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from 'next/link';
import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react';
import { createCourseManagementApi } from '@kit/course-management/api';
import { formatDate } from '@kit/shared/dates';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
interface PageProps {
params: Promise<{ account: string }>;
}
const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
const MONTH_NAMES = [
'Januar',
'Februar',
'März',
'April',
'Mai',
'Juni',
'Juli',
'August',
'September',
'Oktober',
'November',
'Dezember',
];
function getDaysInMonth(year: number, month: number): number {
return new Date(year, month + 1, 0).getDate();
}
function getFirstWeekday(year: number, month: number): number {
const day = new Date(year, month, 1).getDay();
return day === 0 ? 6 : day - 1;
}
export default async function CourseCalendarPage({ params }: PageProps) {
const { account } = await params;
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
.select('id')
.eq('slug', account)
.single();
if (!acct) return <AccountNotFound />;
const api = createCourseManagementApi(client);
const courses = await api.listCourses(acct.id, { page: 1, pageSize: 100 });
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const daysInMonth = getDaysInMonth(year, month);
const firstWeekday = getFirstWeekday(year, month);
// Build set of days that have running courses
const courseDates = new Set<number>();
for (const course of courses.data) {
const courseItem = course as Record<string, unknown>;
if (courseItem.status === 'cancelled') continue;
const startDate = courseItem.start_date
? new Date(String(courseItem.start_date))
: null;
const endDate = courseItem.end_date
? new Date(String(courseItem.end_date))
: null;
if (!startDate) continue;
const courseStart = startDate;
const courseEnd = endDate ?? startDate;
for (let d = 1; d <= daysInMonth; d++) {
const dayDate = new Date(year, month, d);
if (dayDate >= courseStart && dayDate <= courseEnd) {
courseDates.add(d);
}
}
}
// Build calendar grid
const cells: Array<{
day: number | null;
hasCourse: boolean;
isToday: boolean;
}> = [];
for (let i = 0; i < firstWeekday; i++) {
cells.push({ day: null, hasCourse: false, isToday: false });
}
for (let d = 1; d <= daysInMonth; d++) {
cells.push({
day: d,
hasCourse: courseDates.has(d),
isToday:
d === now.getDate() &&
month === now.getMonth() &&
year === now.getFullYear(),
});
}
while (cells.length % 7 !== 0) {
cells.push({ day: null, hasCourse: false, isToday: false });
}
const activeCourses = courses.data.filter(
(courseItem: Record<string, unknown>) =>
courseItem.status === 'open' || courseItem.status === 'running',
);
return (
<CmsPageShell account={account} title="Kurskalender">
<div className="flex w-full flex-col gap-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href={`/home/${account}/courses`}>
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<p className="text-muted-foreground">Kurstermine im Überblick</p>
</div>
</div>
{/* Month Calendar */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Button variant="ghost" size="icon" disabled>
<ChevronLeft className="h-4 w-4" />
</Button>
<CardTitle>
{MONTH_NAMES[month]} {year}
</CardTitle>
<Button variant="ghost" size="icon" disabled>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* Weekday Header */}
<div className="mb-1 grid grid-cols-7 gap-1">
{WEEKDAYS.map((day) => (
<div
key={day}
className="text-muted-foreground py-2 text-center text-xs font-medium"
>
{day}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-1">
{cells.map((cell, idx) => (
<div
key={idx}
className={`relative flex h-12 items-center justify-center rounded-md text-sm transition-colors ${
cell.day === null
? 'bg-transparent'
: cell.hasCourse
? 'bg-emerald-500/15 font-semibold text-emerald-700 dark:text-emerald-400'
: 'bg-muted/30 hover:bg-muted/50'
} ${cell.isToday ? 'ring-primary ring-2 ring-offset-1' : ''}`}
>
{cell.day !== null && (
<>
<span>{cell.day}</span>
{cell.hasCourse && (
<span className="absolute bottom-1 left-1/2 h-1.5 w-1.5 -translate-x-1/2 rounded-full bg-emerald-500" />
)}
</>
)}
</div>
))}
</div>
{/* Legend */}
<div className="text-muted-foreground mt-4 flex items-center gap-4 text-xs">
<div className="flex items-center gap-1.5">
<span className="inline-block h-3 w-3 rounded-sm bg-emerald-500/15" />
Kurstag
</div>
<div className="flex items-center gap-1.5">
<span className="bg-muted/30 inline-block h-3 w-3 rounded-sm" />
Frei
</div>
<div className="flex items-center gap-1.5">
<span className="ring-primary inline-block h-3 w-3 rounded-sm ring-2" />
Heute
</div>
</div>
</CardContent>
</Card>
{/* Active Courses this Month */}
<Card>
<CardHeader>
<CardTitle>Aktive Kurse ({activeCourses.length})</CardTitle>
</CardHeader>
<CardContent>
{activeCourses.length === 0 ? (
<p className="text-muted-foreground text-sm">
Keine aktiven Kurse in diesem Monat.
</p>
) : (
<div className="space-y-3">
{activeCourses.map((course: Record<string, unknown>) => (
<div
key={String(course.id)}
className="flex items-center justify-between rounded-md border p-3"
>
<div>
<Link
href={`/home/${account}/courses/${String(course.id)}`}
className="font-medium hover:underline"
>
{String(course.name)}
</Link>
<p className="text-muted-foreground text-xs">
{formatDate(course.start_date as string)} {' '}
{formatDate(course.end_date as string)}
</p>
</div>
<Badge
variant={
String(course.status) === 'running' ? 'info' : 'default'
}
>
{String(course.status) === 'running'
? 'Laufend'
: 'Offen'}
</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</CmsPageShell>
);
}