Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"useTabs": true,
"useTabs": false,
"tabWidth": 4,
Comment on lines +2 to +3
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Significant formatting change. Switching from tabs to spaces (with 4-space indentation) is a major change that will affect the entire codebase formatting. This should be applied consistently across all files, ideally in a separate commit/PR dedicated to formatting changes only. Mixing formatting changes with functional changes makes code review more difficult.

Copilot uses AI. Check for mistakes.
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
Expand Down
174 changes: 174 additions & 0 deletions src/lib/components/calendar/CalendarGrid.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<script lang="ts">
import { chevronBackOutline, chevronForwardOutline } from 'ionicons/icons';
import type { DayObject } from './calendarUtils';
import { getWeekdayNames, isSelectedDay as checkIsSelectedDay, isToday as checkIsToday } from './calendarUtils';
import type { SelectedDay } from './calendarUtils';

export let weeks: DayObject[][] = [];
export let month: number;
export let year: number;
export let selectedDay: SelectedDay | null = null;
export let onPreviousMonth: () => void;
export let onNextMonth: () => void;
export let onSelectDay: (dayObj: DayObject) => void;

const weekdayNames = getWeekdayNames();

$: isSelectedDay = (dayObj: DayObject) => {
return checkIsSelectedDay(dayObj, selectedDay, month, year);
};

function isToday(dayObj: DayObject) {
return checkIsToday(dayObj, month, year);
}

function hasEvents(dayObj: DayObject) {
if (!dayObj || !dayObj.isCurrentMonth) return false;
return dayObj.hasEvents;
}
</script>

<div class="calendar">
<div class="header">
<ion-card href="" on:click={onPreviousMonth} aria-label="Previous month" class="navButton" aria-hidden="true">
<ion-icon icon={chevronBackOutline} />
</ion-card>
<div class="month-title">
{new Date(year, month).toLocaleDateString(undefined, {
month: 'long',
year: 'numeric'
})}
</div>
<ion-card href="" on:click={onNextMonth} aria-label="Next month" class="navButton" aria-hidden="true">
<ion-icon icon={chevronForwardOutline} />
</ion-card>
</div>

<div class="grid">
{#each weekdayNames as dayName}
<div class="day-name">{dayName}</div>
{/each}

{#each weeks as week}
{#each week as dayObj}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="day {dayObj.isCurrentMonth
? 'selectable'
: 'unselectable'} {isSelectedDay(dayObj) ? 'selected' : ''} {isToday(
dayObj
)
? 'today'
: ''}"
on:click={() => onSelectDay(dayObj)}
>
<span>{dayObj.day}</span>
{#if hasEvents(dayObj)}
<div class="event-badge" />
{/if}
</div>
{/each}
{/each}
</div>
</div>

<style>
.navButton {
background: transparent;
border: none;
color: inherit;
border-radius: 1px;
box-shadow: none;
margin: 0;
padding: 1rem;
}

.calendar {
width: 100%;
padding: 1rem 1rem 0.5rem 1rem;
font-family: sans-serif;
color: var(--ion-color-dark);
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-shrink: 0;
}
.header .month-title {
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 8px;
transition: background 0.15s ease;
font-weight: 500;
}
.header .month-title:hover {
background: var(--ion-color-light);
}

.header ion-icon {
font-size: 1.15rem;
color: var(--ion-color-dark);
display: block;
width: 1em;
height: 1em;
}
.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: minmax(2.5rem, auto);
text-align: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.day-name {
font-weight: bold;
color: var(--ion-color-primary);
padding: 0.25rem;
}
.day {
padding: 0.5rem 0.25rem;
border-radius: 8px;
user-select: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 0;
transition: all 0.15s ease;
gap: 0.15rem;
}
.day.selectable:hover {
background: var(--ion-color-light);
cursor: pointer;
transform: scale(1.05);
}
.day.today {
color: var(--ion-color-primary);
font-weight: 600;
}
.day.selected {
background: var(--ion-color-light);
color: var(--ion-color-dark);
font-weight: 600;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.day.selected.today {
color: var(--ion-color-primary);
}
.day.unselectable {
color: var(--ion-color-medium);
cursor: pointer;
}
.event-badge {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--ion-color-primary);
opacity: 0.8;
}
</style>
44 changes: 44 additions & 0 deletions src/lib/components/calendar/calendarState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { writable, derived } from 'svelte/store';
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import derived.

Suggested change
import { writable, derived } from 'svelte/store';
import { writable } from 'svelte/store';

Copilot uses AI. Check for mistakes.
import type { SelectedDay } from './calendarUtils';

function createCalendarState() {
const currentDate = new Date();
const month = writable(currentDate.getMonth());
const year = writable(currentDate.getFullYear());
const selectedDay = writable<SelectedDay | null>(null);
const activeDate = writable(new Date());

return {
month,
year,
selectedDay,
activeDate,

nextMonth: () => {
month.update(m => {
if (m === 11) {
year.update(y => y + 1);
return 0;
}
return m + 1;
});
},

previousMonth: () => {
month.update(m => {
if (m === 0) {
year.update(y => y - 1);
return 11;
}
return m - 1;
});
},

setMonth: (newMonth: number) => month.set(newMonth),
setYear: (newYear: number) => year.set(newYear),
setSelectedDay: (day: SelectedDay | null) => selectedDay.set(day),
setActiveDate: (date: Date) => activeDate.set(date)
};
}

export const calendarState = createCalendarState();
Comment on lines +1 to +44
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused calendar state module. The calendarState.ts file exports calendar state management functionality but is not imported or used anywhere in the calendar page component. The calendar page implements its own local state management instead. Consider removing this file if it's not needed, or integrate it into the calendar implementation.

Suggested change
import { writable, derived } from 'svelte/store';
import type { SelectedDay } from './calendarUtils';
function createCalendarState() {
const currentDate = new Date();
const month = writable(currentDate.getMonth());
const year = writable(currentDate.getFullYear());
const selectedDay = writable<SelectedDay | null>(null);
const activeDate = writable(new Date());
return {
month,
year,
selectedDay,
activeDate,
nextMonth: () => {
month.update(m => {
if (m === 11) {
year.update(y => y + 1);
return 0;
}
return m + 1;
});
},
previousMonth: () => {
month.update(m => {
if (m === 0) {
year.update(y => y - 1);
return 11;
}
return m - 1;
});
},
setMonth: (newMonth: number) => month.set(newMonth),
setYear: (newYear: number) => year.set(newYear),
setSelectedDay: (day: SelectedDay | null) => selectedDay.set(day),
setActiveDate: (date: Date) => activeDate.set(date)
};
}
export const calendarState = createCalendarState();

Copilot uses AI. Check for mistakes.
139 changes: 139 additions & 0 deletions src/lib/components/calendar/calendarUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import type { Event } from './event/Event';
import { isCurrentDay } from './CalendarFunctions';

export interface DayObject {
day: number;
isCurrentMonth: boolean;
hasEvents: boolean;
}

export interface SelectedDay {
day: number;
month: number;
year: number;
}

/**
* Get localized weekday names (first letter, capitalized)
*/
export function getWeekdayNames(): string[] {
return Array.from({ length: 7 }, (_, i) => {
const date = new Date(2024, 0, i);
Comment on lines +20 to +21
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Hardcoded year in weekday name generation. The function uses new Date(2024, 0, i) to generate weekday names. This will work correctly, but consider using a dynamic approach or adding a comment explaining why 2024 is used (likely just as a reference year for getting weekday names).

Suggested change
return Array.from({ length: 7 }, (_, i) => {
const date = new Date(2024, 0, i);
// Use current year to avoid hardcoding and ensure future-proofing
const currentYear = new Date().getFullYear();
return Array.from({ length: 7 }, (_, i) => {
const date = new Date(currentYear, 0, i);

Copilot uses AI. Check for mistakes.
const fullName = date.toLocaleDateString(undefined, { weekday: 'long' });
return fullName.charAt(0).toUpperCase();
});
}

/**
* Build calendar weeks for a given month and year
*/
export function buildCalendarWeeks(
month: number,
year: number,
events: Event[]
): DayObject[][] {
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const prevMonthDays = new Date(year, month, 0).getDate();

const tempWeeks: DayObject[][] = [];
let week: DayObject[] = [];

// Fill previous month's days
for (let i = firstDay - 1; i >= 0; i--) {
week.push({ day: prevMonthDays - i, isCurrentMonth: false, hasEvents: false });
}

// Fill current month's days
for (let d = 1; d <= daysInMonth; d++) {
const dateToCheck = new Date(year, month, d);
const hasEventOnDay = events.some((event) => isCurrentDay(event, dateToCheck));
week.push({ day: d, isCurrentMonth: true, hasEvents: hasEventOnDay });
if (week.length === 7) {
tempWeeks.push(week);
week = [];
}
}

// Fill next month's days
if (week.length > 0) {
let nextDay = 1;
while (week.length < 7) {
week.push({ day: nextDay++, isCurrentMonth: false, hasEvents: false });
}
tempWeeks.push(week);
}

return tempWeeks;
}

/**
* Compute days with events for the current month (including recurring events)
*/
export function computeDaysWithEvents(
month: number,
year: number,
events: Event[]
): Set<string> {
const days = new Set<string>();
const daysInMonth = new Date(year, month + 1, 0).getDate();

for (let day = 1; day <= daysInMonth; day++) {
const dateToCheck = new Date(year, month, day);
const hasEventOnDay = events.some((event) => isCurrentDay(event, dateToCheck));
if (hasEventOnDay) {
days.add(day.toString());
}
}
return days;
}

/**
* Check if a day object is the selected day
*/
export function isSelectedDay(
dayObj: DayObject | null,
selectedDay: SelectedDay | null,
month: number,
year: number
): boolean {
if (!selectedDay || !dayObj || !dayObj.isCurrentMonth) return false;
return (
selectedDay.day === dayObj.day &&
selectedDay.month === month &&
selectedDay.year === year
);
}

/**
* Check if a day object is today
*/
export function isToday(dayObj: DayObject | null, month: number, year: number): boolean {
if (!dayObj || !dayObj.isCurrentMonth) return false;
const today = new Date();
return (
today.getDate() === dayObj.day &&
today.getMonth() === month &&
today.getFullYear() === year
);
}

/**
* Navigate to the next month
*/
export function getNextMonth(month: number, year: number): { month: number; year: number } {
if (month === 11) {
return { month: 0, year: year + 1 };
}
return { month: month + 1, year };
}

/**
* Navigate to the previous month
*/
export function getPreviousMonth(month: number, year: number): { month: number; year: number } {
if (month === 0) {
return { month: 11, year: year - 1 };
}
return { month: month - 1, year };
}
Loading