Initial state for GitNexus analysis
This commit is contained in:
248
apps/web/app/[locale]/home/[account]/courses/calendar/page.tsx
Normal file
248
apps/web/app/[locale]/home/[account]/courses/calendar/page.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
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 { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
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 <div>Konto nicht gefunden</div>;
|
||||
|
||||
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 c = course as Record<string, unknown>;
|
||||
if (c.status === 'cancelled') continue;
|
||||
const startDate = c.start_date ? new Date(String(c.start_date)) : null;
|
||||
const endDate = c.end_date ? new Date(String(c.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(
|
||||
(c: Record<string, unknown>) =>
|
||||
c.status === 'open' || c.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>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Kurskalender</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Kurstermine im Überblick
|
||||
</p>
|
||||
</div>
|
||||
</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="grid grid-cols-7 gap-1 mb-1">
|
||||
{WEEKDAYS.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="text-center text-xs font-medium text-muted-foreground py-2"
|
||||
>
|
||||
{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 text-emerald-700 dark:text-emerald-400 font-semibold'
|
||||
: 'bg-muted/30 hover:bg-muted/50'
|
||||
} ${cell.isToday ? 'ring-2 ring-primary ring-offset-1' : ''}`}
|
||||
>
|
||||
{cell.day !== null && (
|
||||
<>
|
||||
<span>{cell.day}</span>
|
||||
{cell.hasCourse && (
|
||||
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-4 flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<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="inline-block h-3 w-3 rounded-sm bg-muted/30" />
|
||||
Frei
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-block h-3 w-3 rounded-sm ring-2 ring-primary" />
|
||||
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-sm text-muted-foreground">
|
||||
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-xs text-muted-foreground">
|
||||
{course.start_date
|
||||
? new Date(String(course.start_date)).toLocaleDateString('de-DE')
|
||||
: '—'}{' '}
|
||||
–{' '}
|
||||
{course.end_date
|
||||
? new Date(String(course.end_date)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={String(course.status) === 'running' ? 'info' : 'default'}>
|
||||
{String(course.status) === 'running' ? 'Laufend' : 'Offen'}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user