+100
src/Den.Client.Web/src/components/views/calendar/calendar-view.tsx
+100
src/Den.Client.Web/src/components/views/calendar/calendar-view.tsx
···
1
+
import { cn } from "@/lib/utils";
2
+
import { useState } from "react";
3
+
4
+
export type CalendarEvent = {
5
+
id: string;
6
+
title: string;
7
+
date: Date;
8
+
};
9
+
10
+
export type CalendarProps = {
11
+
events: CalendarEvent[];
12
+
onEventClick: (event: CalendarEvent) => void;
13
+
};
14
+
15
+
export const CalendarView = ({ events, onEventClick }: CalendarProps) => {
16
+
const [currentDate, setCurrentDate] = useState(new Date())
17
+
18
+
const year = currentDate.getFullYear()
19
+
const month = currentDate.getMonth()
20
+
21
+
const firstDayOfMonth = new Date(year, month, 1)
22
+
const lastDayOfMonth = new Date(year, month + 1, 0)
23
+
const startingDayOfWeek = firstDayOfMonth.getDay()
24
+
const daysInMonth = lastDayOfMonth.getDate()
25
+
26
+
const previousMonth = () => {
27
+
setCurrentDate(new Date(year, month - 1, 1))
28
+
}
29
+
30
+
const nextMonth = () => {
31
+
setCurrentDate(new Date(year, month + 1, 1))
32
+
}
33
+
34
+
const getEventsForDay = (day: number) => {
35
+
return events.filter((event) => {
36
+
const eventDate = new Date(event.date)
37
+
return eventDate.getDate() === day && eventDate.getMonth() === month && eventDate.getFullYear() === year
38
+
})
39
+
}
40
+
41
+
const isToday = (day: number) => {
42
+
const today = new Date()
43
+
return today.getDate() === day && today.getMonth() === month && today.getFullYear() === year
44
+
}
45
+
46
+
// Create array of day cells including empty cells for alignment
47
+
const calendarDays = []
48
+
49
+
// Add empty cells for days before the first day of month
50
+
for (let i = 0; i < startingDayOfWeek; i++) {
51
+
calendarDays.push(null)
52
+
}
53
+
54
+
// Add cells for each day of the month
55
+
for (let day = 1; day <= daysInMonth; day++) {
56
+
calendarDays.push(day)
57
+
}
58
+
59
+
return (
60
+
<div className="grid grid-cols-7 gap-2">
61
+
{calendarDays.map((day, index) => {
62
+
if (day === null) {
63
+
return <div key={`empty-${index}`} className="aspect-square" />
64
+
}
65
+
66
+
const dayEvents = getEventsForDay(day);
67
+
const isTodayDate = isToday(day);
68
+
69
+
return (
70
+
<div
71
+
key={day}
72
+
className={cn(
73
+
"aspect-square border rounded-lg p-2 hover:border-primary/50 transition-colors",
74
+
isTodayDate && "bg-primary/5 border-primary"
75
+
)}
76
+
>
77
+
<div className="h-full flex flex-col">
78
+
<span className={cn("text-sm font-medium mb-1", isTodayDate && "text-primary font-semibold")}>{day}</span>
79
+
80
+
<div className="flex-1 flex flex-col gap-1 overflow-hidden">
81
+
{dayEvents.slice(0, 3).map((event) => (
82
+
<button
83
+
key={event.id}
84
+
onClick={() => onEventClick(event)}
85
+
className={cn(
86
+
"text-left text-xs px-2 py-1 rounded truncate transition-all hover:scale-105",
87
+
)}
88
+
>
89
+
{event.title}
90
+
</button>
91
+
))}
92
+
</div>
93
+
</div>
94
+
95
+
</div>
96
+
);
97
+
})}
98
+
</div>
99
+
);
100
+
};
+24
src/Den.Client.Web/src/components/views/calendar/event-card.tsx
+24
src/Den.Client.Web/src/components/views/calendar/event-card.tsx
···
1
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2
+
import type { CalendarEvent } from "./calendar-view";
3
+
4
+
export const EventCard = ({ event }: { event: CalendarEvent; }) => {
5
+
return (
6
+
<Card>
7
+
<CardHeader>
8
+
<CardTitle>{event.title}</CardTitle>
9
+
<CardDescription>{event.date.toLocaleString()}</CardDescription>
10
+
</CardHeader>
11
+
<CardContent>
12
+
<dl className="grid grid-cols-1 gap-4">
13
+
<div className="flex flex-col gap-2">
14
+
<dt className="font-medium">Location</dt>
15
+
<dd>123 Flower St, Springfield</dd>
16
+
</div>
17
+
18
+
<dt className="font-medium">Notes</dt>
19
+
<dd>Don't forget to bring the flowers!</dd>
20
+
</dl>
21
+
</CardContent>
22
+
</Card>
23
+
);
24
+
}
+27
-1
src/Den.Client.Web/src/routeTree.gen.ts
+27
-1
src/Den.Client.Web/src/routeTree.gen.ts
···
12
12
import { Route as authAuthRouteImport } from './routes/(auth)/_auth'
13
13
import { Route as appHomeRouteImport } from './routes/(app)/_home'
14
14
import { Route as appHomeIndexRouteImport } from './routes/(app)/_home.index'
15
+
import { Route as appHomeCalendarIndexRouteImport } from './routes/(app)/_home/calendar/index'
15
16
import { Route as appHomeBudgetsIndexRouteImport } from './routes/(app)/_home/budgets/index'
16
17
import { Route as authAuthAuthSignupRouteImport } from './routes/(auth)/_auth.auth.signup'
17
18
import { Route as authAuthAuthLoginRouteImport } from './routes/(auth)/_auth.auth.login'
···
28
29
const appHomeIndexRoute = appHomeIndexRouteImport.update({
29
30
id: '/',
30
31
path: '/',
32
+
getParentRoute: () => appHomeRoute,
33
+
} as any)
34
+
const appHomeCalendarIndexRoute = appHomeCalendarIndexRouteImport.update({
35
+
id: '/calendar/',
36
+
path: '/calendar/',
31
37
getParentRoute: () => appHomeRoute,
32
38
} as any)
33
39
const appHomeBudgetsIndexRoute = appHomeBudgetsIndexRouteImport.update({
···
57
63
'/auth/login': typeof authAuthAuthLoginRoute
58
64
'/auth/signup': typeof authAuthAuthSignupRoute
59
65
'/budgets': typeof appHomeBudgetsIndexRoute
66
+
'/calendar': typeof appHomeCalendarIndexRoute
60
67
}
61
68
export interface FileRoutesByTo {
62
69
'/': typeof appHomeIndexRoute
···
64
71
'/auth/login': typeof authAuthAuthLoginRoute
65
72
'/auth/signup': typeof authAuthAuthSignupRoute
66
73
'/budgets': typeof appHomeBudgetsIndexRoute
74
+
'/calendar': typeof appHomeCalendarIndexRoute
67
75
}
68
76
export interface FileRoutesById {
69
77
__root__: typeof rootRouteImport
···
74
82
'/(auth)/_auth/auth/login': typeof authAuthAuthLoginRoute
75
83
'/(auth)/_auth/auth/signup': typeof authAuthAuthSignupRoute
76
84
'/(app)/_home/budgets/': typeof appHomeBudgetsIndexRoute
85
+
'/(app)/_home/calendar/': typeof appHomeCalendarIndexRoute
77
86
}
78
87
export interface FileRouteTypes {
79
88
fileRoutesByFullPath: FileRoutesByFullPath
···
83
92
| '/auth/login'
84
93
| '/auth/signup'
85
94
| '/budgets'
95
+
| '/calendar'
86
96
fileRoutesByTo: FileRoutesByTo
87
-
to: '/' | '/budgets/create' | '/auth/login' | '/auth/signup' | '/budgets'
97
+
to:
98
+
| '/'
99
+
| '/budgets/create'
100
+
| '/auth/login'
101
+
| '/auth/signup'
102
+
| '/budgets'
103
+
| '/calendar'
88
104
id:
89
105
| '__root__'
90
106
| '/(app)/_home'
···
94
110
| '/(auth)/_auth/auth/login'
95
111
| '/(auth)/_auth/auth/signup'
96
112
| '/(app)/_home/budgets/'
113
+
| '/(app)/_home/calendar/'
97
114
fileRoutesById: FileRoutesById
98
115
}
99
116
export interface RootRouteChildren {
···
124
141
preLoaderRoute: typeof appHomeIndexRouteImport
125
142
parentRoute: typeof appHomeRoute
126
143
}
144
+
'/(app)/_home/calendar/': {
145
+
id: '/(app)/_home/calendar/'
146
+
path: '/calendar'
147
+
fullPath: '/calendar'
148
+
preLoaderRoute: typeof appHomeCalendarIndexRouteImport
149
+
parentRoute: typeof appHomeRoute
150
+
}
127
151
'/(app)/_home/budgets/': {
128
152
id: '/(app)/_home/budgets/'
129
153
path: '/budgets'
···
159
183
appHomeIndexRoute: typeof appHomeIndexRoute
160
184
appHomeBudgetsCreateRoute: typeof appHomeBudgetsCreateRoute
161
185
appHomeBudgetsIndexRoute: typeof appHomeBudgetsIndexRoute
186
+
appHomeCalendarIndexRoute: typeof appHomeCalendarIndexRoute
162
187
}
163
188
164
189
const appHomeRouteChildren: appHomeRouteChildren = {
165
190
appHomeIndexRoute: appHomeIndexRoute,
166
191
appHomeBudgetsCreateRoute: appHomeBudgetsCreateRoute,
167
192
appHomeBudgetsIndexRoute: appHomeBudgetsIndexRoute,
193
+
appHomeCalendarIndexRoute: appHomeCalendarIndexRoute,
168
194
}
169
195
170
196
const appHomeRouteWithChildren =
+68
src/Den.Client.Web/src/routes/(app)/_home/calendar/index.tsx
+68
src/Den.Client.Web/src/routes/(app)/_home/calendar/index.tsx
···
1
+
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage } from '@/components/ui/breadcrumb';
2
+
import { SidebarTrigger } from '@/components/ui/sidebar';
3
+
import { Separator } from '@/components/ui/separator';
4
+
import { createFileRoute, Link } from '@tanstack/react-router'
5
+
import { useTranslation } from 'react-i18next';
6
+
import { CalendarView, type CalendarEvent } from '@/components/views/calendar/calendar-view';
7
+
import { EventCard } from '@/components/views/calendar/event-card';
8
+
import { useState } from 'react';
9
+
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
10
+
import { CalendarIcon } from 'lucide-react';
11
+
12
+
export const Route = createFileRoute('/(app)/_home/calendar/')({
13
+
component: () => {
14
+
const [events, _setEvents] = useState<CalendarEvent[]>([
15
+
{ id: 'abc123', title: 'Flower Delivery', date: new Date() },
16
+
{ id: 'def456', title: 'Meeting with Bob', date: new Date() },
17
+
]);
18
+
const { t } = useTranslation();
19
+
20
+
return (
21
+
<>
22
+
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
23
+
<SidebarTrigger />
24
+
<Separator orientation="vertical" className="data-[orientation=vertical]:h-4" />
25
+
<Breadcrumb>
26
+
<BreadcrumbList>
27
+
<BreadcrumbItem className="hidden md:block">
28
+
<BreadcrumbLink asChild>
29
+
<Link to="/budgets">{t('menu.budgeting.title')}</Link>
30
+
</BreadcrumbLink>
31
+
</BreadcrumbItem>
32
+
<BreadcrumbSeparator />
33
+
<BreadcrumbItem>
34
+
<BreadcrumbPage>
35
+
{t('menu.budgeting.allBudgets')}
36
+
</BreadcrumbPage>
37
+
</BreadcrumbItem>
38
+
</BreadcrumbList>
39
+
</Breadcrumb>
40
+
</header>
41
+
42
+
<div className="flex flex-1 flex-col gap-4 p-4">
43
+
<div className="grid flex-1 grid-cols-1 lg:grid-cols-3 gap-4">
44
+
<div className="flex flex-col gap-6">
45
+
{events.length === 0 && (
46
+
<Empty>
47
+
<EmptyHeader>
48
+
<EmptyMedia variant="icon">
49
+
<CalendarIcon />
50
+
</EmptyMedia>
51
+
<EmptyTitle>No Events</EmptyTitle>
52
+
<EmptyDescription>
53
+
You have no events scheduled. Your events will appear here.
54
+
</EmptyDescription>
55
+
</EmptyHeader>
56
+
</Empty>
57
+
)}
58
+
{events.map((ev, idx) => <EventCard key={idx} event={ev} />)}
59
+
</div>
60
+
<div className="lg:col-span-2">
61
+
<CalendarView events={events} onEventClick={() => {}} />
62
+
</div>
63
+
</div>
64
+
</div>
65
+
</>
66
+
);
67
+
}
68
+
});