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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user