Initial commit
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */
|
||||
/* Hallmark · component: calendar-page · genre: editorial · theme: Atelier (Dark & Light) · studied-DNA: https://inihaus.de */
|
||||
import React, { useState } from 'react';
|
||||
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, ArrowRight, MapPin, Clock, Layers } from 'lucide-react';
|
||||
|
||||
export default function CalendarPage({ events, onSelectEvent, branding }) {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectedCalendarEvent, setSelectedCalendarEvent] = useState(null);
|
||||
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
|
||||
const monthNames = [
|
||||
'JANUAR', 'FEBRUAR', 'MÄRZ', 'APRIL', 'MAI', 'JUNI',
|
||||
'JULI', 'AUGUST', 'SEPTEMBER', 'OKTOBER', 'NOVEMBER', 'DEZEMBER'
|
||||
];
|
||||
|
||||
const prevMonth = () => setCurrentDate(new Date(year, month - 1, 1));
|
||||
const nextMonth = () => setCurrentDate(new Date(year, month + 1, 1));
|
||||
const todayMonth = () => setCurrentDate(new Date());
|
||||
|
||||
const firstDayOfMonth = new Date(year, month, 1).getDay();
|
||||
const startOffset = (firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1);
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
const formatDateString = (day) => {
|
||||
const mStr = String(month + 1).padStart(2, '0');
|
||||
const dStr = String(day).padStart(2, '0');
|
||||
return `${year}-${mStr}-${dStr}`;
|
||||
};
|
||||
|
||||
const getEventsForDay = (day) => {
|
||||
const dateStr = formatDateString(day);
|
||||
return events.filter((evt) => dateStr >= evt.start_date && dateStr <= evt.end_date);
|
||||
};
|
||||
|
||||
const primaryColor = branding?.primary_color || 'var(--brand-primary)';
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-5xl mx-auto animate-in fade-in duration-200">
|
||||
{/* Calendar Masthead Bar */}
|
||||
<div className="hallmark-panel rounded-sm p-6 sm:p-8 border border-grid flex flex-col sm:flex-row sm:items-center justify-between gap-4 font-mono">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-sm bg-subtle text-main border border-grid flex items-center justify-center font-bold">
|
||||
<CalendarIcon className="w-4 h-4 text-amber-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-bold uppercase tracking-tight text-main leading-none">
|
||||
{monthNames[month]} {year}
|
||||
</h2>
|
||||
<p className="text-[10px] text-muted uppercase tracking-widest mt-1">
|
||||
— INITIATIVE E.V. TERMIN- & SCHICHTÜBERSICHT —
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={todayMonth}
|
||||
className="px-3.5 py-1.5 rounded-sm text-xs font-mono font-bold bg-surface hover:bg-surface-hover text-main border border-grid transition"
|
||||
>
|
||||
[ HEUTE ]
|
||||
</button>
|
||||
<button
|
||||
onClick={prevMonth}
|
||||
className="p-1.5 rounded-sm text-muted hover:text-main bg-surface border border-grid transition"
|
||||
title="Vorheriger Monat"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={nextMonth}
|
||||
className="p-1.5 rounded-sm text-muted hover:text-main bg-surface border border-grid transition"
|
||||
title="Nächster Monat"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar Grid Container */}
|
||||
<div className="hallmark-panel rounded-sm border border-grid overflow-hidden">
|
||||
{/* Days Header */}
|
||||
<div className="grid grid-cols-7 bg-subtle border-b border-grid text-center text-xs font-mono text-muted font-bold py-3 uppercase">
|
||||
<div>MO</div><div>DI</div><div>MI</div><div>DO</div><div>FR</div><div>SA</div><div>SO</div>
|
||||
</div>
|
||||
|
||||
{/* Days Cells */}
|
||||
<div className="grid grid-cols-7 auto-rows-fr divide-x divide-y divide-grid bg-surface text-xs font-mono">
|
||||
{Array.from({ length: startOffset }).map((_, i) => (
|
||||
<div key={`offset-${i}`} className="min-h-[100px] sm:min-h-[110px] p-2 bg-subtle/50 text-muted opacity-30"></div>
|
||||
))}
|
||||
|
||||
{Array.from({ length: daysInMonth }).map((_, i) => {
|
||||
const dayNum = i + 1;
|
||||
const dayEvents = getEventsForDay(dayNum);
|
||||
const isToday =
|
||||
new Date().getFullYear() === year &&
|
||||
new Date().getMonth() === month &&
|
||||
new Date().getDate() === dayNum;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`day-${dayNum}`}
|
||||
className={`min-h-[100px] sm:min-h-[110px] p-2 flex flex-col justify-between transition ${
|
||||
isToday ? 'bg-surface-hover/80 font-bold' : 'hover:bg-surface-hover/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span
|
||||
className={`w-5 h-5 rounded-sm flex items-center justify-center font-mono text-xs ${
|
||||
isToday
|
||||
? 'bg-main text-paper font-bold shadow-sm'
|
||||
: 'text-muted'
|
||||
}`}
|
||||
>
|
||||
{dayNum}
|
||||
</span>
|
||||
{dayEvents.length > 0 && (
|
||||
<span className="text-[9px] font-mono text-muted">
|
||||
{dayEvents.length} {dayEvents.length === 1 ? 'Event' : 'Events'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 overflow-y-auto max-h-[70px] no-scrollbar">
|
||||
{dayEvents.map((evt) => {
|
||||
const isSelected = selectedCalendarEvent?.id === evt.id;
|
||||
return (
|
||||
<button
|
||||
key={`evt-${evt.id}`}
|
||||
onClick={() => setSelectedCalendarEvent(evt)}
|
||||
style={{
|
||||
backgroundColor: isSelected ? primaryColor : `${primaryColor}15`,
|
||||
borderColor: `${primaryColor}40`,
|
||||
color: isSelected ? '#ffffff' : primaryColor
|
||||
}}
|
||||
className={`w-full text-left px-2 py-1 rounded-sm text-[10px] font-mono font-bold border truncate block hover:brightness-110 transition shadow-sm ${
|
||||
evt.is_active === false ? 'opacity-50 line-through' : ''
|
||||
}`}
|
||||
>
|
||||
{evt.title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected Event Detail Drawer */}
|
||||
{selectedCalendarEvent && (
|
||||
<div className="hallmark-panel rounded-sm p-6 border border-grid flex flex-col sm:flex-row sm:items-center justify-between gap-4 font-mono animate-in fade-in duration-200">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
<span className="flex items-center gap-1 bg-subtle px-2 py-0.5 rounded-sm border border-grid font-bold text-main">
|
||||
<Clock className="w-3 h-3 text-muted" />
|
||||
{new Date(selectedCalendarEvent.start_date).toLocaleDateString('de-DE')} — {new Date(selectedCalendarEvent.end_date).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
{selectedCalendarEvent.location && (
|
||||
<span className="flex items-center gap-1 text-muted">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{selectedCalendarEvent.location}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="font-serif text-2xl font-bold uppercase text-main">
|
||||
{selectedCalendarEvent.title}
|
||||
</h3>
|
||||
|
||||
{selectedCalendarEvent.description && (
|
||||
<p className="text-xs font-sans text-muted max-w-xl">
|
||||
{selectedCalendarEvent.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => onSelectEvent(selectedCalendarEvent.id)}
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
className="px-5 py-2 rounded-sm text-xs font-mono font-bold text-white transition flex items-center gap-1.5 hover:brightness-110 shadow-sm"
|
||||
>
|
||||
SCHICHTPLAN ÖFFNEN <ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user