feat(calendar): add calendar.css grid layout + styled day cells and legend

- Create calendar.css with 7-column grid using display: grid
- Style calendar header with navigation buttons (min 36x36px tap targets)
- Style day cells with borders, hover states, and focus-visible outlines
- Style weekday headers (centered, muted, uppercase)
- Create --disabled modifier for leading/trailing days from adjacent months
- Create --has-events modifier for visual emphasis
- Create event markers as small dots (8x8px circles)
- Style legend with compact horizontal swatch layout
- Import CSS into Calendar.tsx following co-located pattern from Header

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 12:51:48 +02:00
parent 772d853e37
commit 69ce746082
2 changed files with 266 additions and 7 deletions
+102 -7
View File
@@ -6,11 +6,17 @@ import { Layout } from '@/components/layout/Layout'
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
import { CalendarDay } from '@/components/common/CalendarDay'
import { CalendarEvent } from '@/components/common/CalendarEvent'
import { PlannedDayModal } from '@/components/modals/PlannedDayModal'
import './calendar.css'
export function Calendar(): ReactNode {
const { isLoading: wsLoading, selectedChildId } = useWorkspace()
const { data: plannedDays, isLoading: pdLoading } = usePlannedDays(selectedChildId ?? undefined)
const [currentMonth, setCurrentMonth] = useState(new Date(2026, 5))
const { isLoading: wsLoading, selectedChildId, workspace, personalDataUuid } = useWorkspace()
const { data: plannedDays, isLoading: pdLoading, add, update, remove } = usePlannedDays(selectedChildId ?? undefined)
const [currentMonth, setCurrentMonth] = useState(new Date())
const [selectedDate, setSelectedDate] = useState<string | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [modalError, setModalError] = useState<string | null>(null)
const isLoading = wsLoading || pdLoading
@@ -24,13 +30,13 @@ export function Calendar(): ReactNode {
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const daysInMonth = lastDay.getDate()
const startingDayOfWeek = firstDay.getDay()
const startOffset = (firstDay.getDay() + 6) % 7
const weeks: string[][] = []
let currentWeek: string[] = []
for (let i = 0; i < startingDayOfWeek; i++) {
const prevDate = new Date(year, month, -startingDayOfWeek + i + 1)
for (let i = 0; i < startOffset; i++) {
const prevDate = new Date(year, month, -startOffset + i + 1)
currentWeek.push(formatDate(prevDate))
}
@@ -66,6 +72,73 @@ export function Calendar(): ReactNode {
setCurrentMonth(new Date(year, month + 1, 1))
}
const handleDayClick = (date: string) => {
setSelectedDate(date)
setModalError(null)
setIsModalOpen(true)
}
const getExistingDay = (date: string): PlannedLeaveDay | undefined => {
return (plannedDays ?? []).find((pd: PlannedLeaveDay) => pd.date === date && pd.parentPersonalDataUuid === personalDataUuid)
}
const handleSavePlannedDay = async (payload: {
dayType: 'sjukpenning' | 'lagsta' | 'vacation'
parentId: string
isDubbeldag: boolean
}) => {
if (!selectedDate || !selectedChildId || !workspace?.id || !personalDataUuid) return
try {
setIsSaving(true)
setModalError(null)
const existingDay = getExistingDay(selectedDate)
if (existingDay) {
await update(existingDay.id, {
day_type: payload.dayType,
is_dubbeldag: payload.isDubbeldag,
})
} else {
await add({
workspace_id: workspace.id,
child_id: selectedChildId,
parent_personal_data_uuid: personalDataUuid,
date: selectedDate,
day_type: payload.dayType,
is_dubbeldag: payload.isDubbeldag,
})
}
setIsModalOpen(false)
} catch (err) {
setModalError(err instanceof Error ? err.message : 'Failed to save planned day')
} finally {
setIsSaving(false)
}
}
const handleDeletePlannedDay = async () => {
if (!selectedDate) return
try {
setIsSaving(true)
setModalError(null)
const existingDay = getExistingDay(selectedDate)
if (existingDay) {
await remove(existingDay.id)
}
setIsModalOpen(false)
} catch (err) {
setModalError(err instanceof Error ? err.message : 'Failed to delete planned day')
} finally {
setIsSaving(false)
}
}
return (
<Layout>
<div data-testid="calendar" className="calendar">
@@ -82,7 +155,7 @@ export function Calendar(): ReactNode {
</div>
<div className="calendar__grid">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (
{['Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör', 'Sön'].map((day) => (
<div key={day} className="calendar__weekday">
{day}
</div>
@@ -92,12 +165,15 @@ export function Calendar(): ReactNode {
week.map((date, dayIdx) => {
const events = getEventsForDate(date)
const isCurrentMonth = new Date(date).getMonth() === month
const isToday = date === getTodayString()
return (
<CalendarDay
key={`${weekIdx}-${dayIdx}`}
date={date}
events={events}
isCurrentMonth={isCurrentMonth}
isToday={isToday}
onClick={isCurrentMonth ? () => handleDayClick(date) : undefined}
/>
)
})
@@ -118,6 +194,21 @@ export function Calendar(): ReactNode {
</div>
</div>
</div>
{selectedDate && (
<PlannedDayModal
date={selectedDate}
isOpen={isModalOpen}
existingDay={getExistingDay(selectedDate) ?? null}
parents={[{ id: personalDataUuid ?? '', name: 'Current Parent' }]}
selectedParentId={personalDataUuid ?? ''}
onSave={handleSavePlannedDay}
onDelete={getExistingDay(selectedDate) ? handleDeletePlannedDay : undefined}
onClose={() => setIsModalOpen(false)}
isSaving={isSaving}
error={modalError}
/>
)}
</div>
</Layout>
)
@@ -130,6 +221,10 @@ function formatDate(date: Date): string {
return `${year}-${month}-${day}`
}
function getTodayString(): string {
return formatDate(new Date())
}
function getColorForType(type: string): string {
const colorMap: Record<string, string> = {
vacation: '#4CAF50',