- WebAuthn: register/auth options, device management - PWA widget page + manifest shortcuts - Group schedule endpoint (timezones + locations) - UI #3-#6: compact headers on tasks/calendar/projects/goals - UI #9: mobile responsive top bars - webauthn_credentials table Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
'use client';
|
|
import { useState, useEffect } from 'react';
|
|
import FullCalendar from '@fullcalendar/react';
|
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
import interactionPlugin from '@fullcalendar/interaction';
|
|
import { useTranslation } from '@/lib/i18n';
|
|
import { useAuth } from '@/lib/auth';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
interface CalTask {
|
|
id: string;
|
|
title: string;
|
|
scheduled_at: string | null;
|
|
due_at: string | null;
|
|
status: string;
|
|
group_name: string;
|
|
group_color: string;
|
|
}
|
|
|
|
const LOCALE_MAP: Record<string, string> = {
|
|
cs: 'cs',
|
|
he: 'he',
|
|
ru: 'ru',
|
|
ua: 'uk',
|
|
};
|
|
|
|
export default function CalendarPage() {
|
|
const [tasks, setTasks] = useState<CalTask[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { t, locale } = useTranslation();
|
|
const { token } = useAuth();
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
if (!token) {
|
|
router.replace('/login');
|
|
return;
|
|
}
|
|
fetch('/api/v1/tasks?limit=100', {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
})
|
|
.then(r => {
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
return r.json();
|
|
})
|
|
.then(d => setTasks(d.data || []))
|
|
.catch(err => setError(err.message));
|
|
}, [token, router]);
|
|
|
|
const events = tasks
|
|
.filter((tk) => tk.scheduled_at !== null || tk.due_at !== null)
|
|
.map(tk => ({
|
|
id: tk.id,
|
|
title: tk.title,
|
|
start: (tk.scheduled_at || tk.due_at) as string,
|
|
end: (tk.due_at || tk.scheduled_at) as string,
|
|
backgroundColor: tk.group_color || '#3B82F6',
|
|
borderColor: tk.group_color || '#3B82F6',
|
|
extendedProps: { status: tk.status, group: tk.group_name },
|
|
}));
|
|
|
|
if (!token) return null;
|
|
|
|
return (
|
|
<div className="pb-24 sm:pb-8 px-4 sm:px-0">
|
|
{/* Compact single-row header */}
|
|
<div className="flex items-center justify-between gap-2 mb-3">
|
|
<h1 className="text-xl font-bold dark:text-white truncate">{t('calendar.title')}</h1>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-2 sm:p-4 shadow calendar-compact">
|
|
<FullCalendar
|
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
initialView="timeGridWeek"
|
|
headerToolbar={{
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay',
|
|
}}
|
|
events={events}
|
|
editable={true}
|
|
selectable={true}
|
|
locale={LOCALE_MAP[locale] || 'cs'}
|
|
direction={locale === 'he' ? 'rtl' : 'ltr'}
|
|
firstDay={1}
|
|
height="auto"
|
|
slotMinTime="06:00:00"
|
|
slotMaxTime="23:00:00"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|