Skip to content

Commit 67cfff1

Browse files
committed
feat(calendar): add configurable first day of week for calendar widget
- Introduce CALENDAR_WIDGET_WEEKDAY_ABBREVIATIONS constant for weekday labels - Implement generateWeekdayHeaders() to create dynamic weekday headers - Update calendar modal template to render weekday headers based on configured first day - Fix showCalendarModal call to pass correct first day of week index - Enhance renderCalendarDays function to calculate day offset by first day setting - Modify calendar grid alignment to reflect user-selected week start day - Highlight weekend days in red regardless of the first day configuration - Test with multiple first day settings (Sunday, Monday, Saturday) confirming correct display - Prepare calendar widget for internationalization and future localization support
1 parent 4342cfe commit 67cfff1

File tree

4 files changed

+139
-26
lines changed

4 files changed

+139
-26
lines changed

CALENDAR_WEEKDAY_IMPLEMENTATION.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Calendar Widget First Day of Week Implementation Summary
2+
3+
## Overview
4+
This document summarizes the implementation of configurable first day of the week for the calendar widget. Users can now configure the calendar to start the week on any day (Sunday, Monday, Tuesday, etc.) and the calendar will properly display weekday headers and align the days accordingly.
5+
6+
## Changes Made
7+
8+
### 1. Calendar Widget (`calendar-widget.ts`)
9+
10+
#### New Constants:
11+
- `CALENDAR_WIDGET_WEEKDAY_ABBREVIATIONS`: Maps weekday names to their abbreviations (Su, Mo, Tu, etc.)
12+
13+
#### New Function:
14+
- `generateWeekdayHeaders(firstDayOfWeek: string)`: Generates HTML for weekday headers based on the selected first day of the week
15+
16+
#### Modified Logic:
17+
- Updated the calendar modal template to dynamically generate weekday headers using the new function
18+
- Fixed the `showCalendarModal` call to use the correct first day of week index
19+
20+
### 2. Calendar Widget Utilities (`calendar-widget.utils.ts`)
21+
22+
#### Improved Logic:
23+
- Fixed the `renderCalendarDays` function to properly calculate the starting day offset based on the first day of week setting
24+
- The calculation now correctly handles cases where the first day of the week is different from Sunday
25+
26+
## How It Works
27+
28+
### Weekday Headers Generation:
29+
1. The system determines the first day of the week from widget options
30+
2. It creates an ordered array of weekdays starting from the selected first day
31+
3. It generates HTML div elements for each weekday abbreviation
32+
4. Weekend days (Saturday and Sunday) are styled with red text
33+
34+
### Calendar Grid Alignment:
35+
1. The system calculates the offset for the first day of the month based on the selected first day of the week
36+
2. It adds empty cells to properly align the calendar grid
37+
3. The days are then filled in sequence
38+
39+
## Test Results
40+
41+
The implementation was tested with three configurations:
42+
1. **Sunday as first day**: Su Mo Tu We Th Fr Sa
43+
2. **Monday as first day**: Mo Tu We Th Fr Sa Su
44+
3. **Saturday as first day**: Sa Su Mo Tu We Th Fr
45+
46+
All configurations correctly display the weekday headers and align the calendar grid properly.
47+
48+
## Benefits
49+
50+
1. **User Configurability**: Users can choose their preferred starting day of the week
51+
2. **Proper Visualization**: Weekend days are highlighted in red regardless of the starting day
52+
3. **Correct Alignment**: Calendar grid correctly aligns with the selected starting day
53+
4. **Internationalization Ready**: The implementation can easily support different weekday preferences from various cultures
54+
55+
## Future Improvements
56+
57+
1. Add localization support for weekday abbreviations
58+
2. Implement additional styling options for different weekend days in various cultures
59+
3. Add validation for the first day of week setting

web/src/server/widgets/calendar-widget.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ export const CALENDAR_WIDGET_WEEKDAY_INDEXES: Record<string, number> = {
4040
[CalendarWidgetWeekday.saturday]: 6,
4141
};
4242

43+
// Weekday abbreviations for display in calendar header
44+
export const CALENDAR_WIDGET_WEEKDAY_ABBREVIATIONS: Record<string, string> = {
45+
[CalendarWidgetWeekday.sunday]: 'Su',
46+
[CalendarWidgetWeekday.monday]: 'Mo',
47+
[CalendarWidgetWeekday.tuesday]: 'Tu',
48+
[CalendarWidgetWeekday.wednesday]: 'We',
49+
[CalendarWidgetWeekday.thursday]: 'Th',
50+
[CalendarWidgetWeekday.friday]: 'Fr',
51+
[CalendarWidgetWeekday.saturday]: 'Sa',
52+
};
53+
4354
export const CalendarWidgetWeekdayKeys = Object.keys(
4455
CALENDAR_WIDGET_WEEKDAY_TITLE
4556
);
@@ -72,13 +83,13 @@ export const CALENDAR_FORMLY_FIELDS: FormlyFieldConfig[] = [
7283
label: CALENDAR_WIDGET_WEEKDAY_TITLE[key],
7384
})),
7485
placeholder: 'Select first day of week',
75-
attributes: {
86+
attributes: {
7687
'aria-label': 'Select first day of week',
7788
class: 'flat-input',
7889
},
7990
},
8091
},
81-
];// Helper function to get monthly progress
92+
]; // Helper function to get monthly progress
8293
function getMonthlyProgress() {
8394
const today = new Date();
8495
const currentDay = today.getDate();
@@ -91,6 +102,34 @@ function getMonthlyProgress() {
91102
return { currentDay, lastDay, progress: progress.toFixed(1) };
92103
}
93104

105+
// Function to generate weekday headers based on firstDayOfWeek
106+
function generateWeekdayHeaders(firstDayOfWeek: string): string {
107+
const weekdays = Object.keys(CALENDAR_WIDGET_WEEKDAY_TITLE);
108+
const abbreviations = CALENDAR_WIDGET_WEEKDAY_ABBREVIATIONS;
109+
110+
// Find the starting index based on firstDayOfWeek
111+
const startIndex = CALENDAR_WIDGET_WEEKDAY_INDEXES[firstDayOfWeek];
112+
113+
// Create an array of weekday abbreviations starting from firstDayOfWeek
114+
const orderedWeekdays = [];
115+
for (let i = 0; i < 7; i++) {
116+
const index = (startIndex + i) % 7;
117+
orderedWeekdays.push(weekdays[index]);
118+
}
119+
120+
// Generate the HTML for weekday headers
121+
let html = '';
122+
orderedWeekdays.forEach((day, index) => {
123+
const isWeekend =
124+
day === CalendarWidgetWeekday.saturday ||
125+
day === CalendarWidgetWeekday.sunday;
126+
const textColorClass = isWeekend ? 'text-red-500' : '';
127+
html += `<div class="${textColorClass}">${abbreviations[day]}</div>`;
128+
});
129+
130+
return html;
131+
}
132+
94133
export class CalendarWidgetRender implements WidgetRender<CalendarWidgetType> {
95134
private inited = false;
96135
init(
@@ -127,6 +166,13 @@ export class CalendarWidgetRender implements WidgetRender<CalendarWidgetType> {
127166
// Generate unique IDs for this widget instance
128167
const modalId = `calendar-modal-${widget.id}`;
129168

169+
// Get the first day of week setting or default to sunday
170+
const firstDayOfWeek =
171+
widget.options?.firstDayOfWeek || CalendarWidgetWeekday.sunday;
172+
173+
// Generate weekday headers based on firstDayOfWeek
174+
const weekdayHeaders = generateWeekdayHeaders(firstDayOfWeek);
175+
130176
return `
131177
<div class="bg-white p-6 rounded-2xl long-shadow transition-all duration-300 relative overflow-hidden h-40 flex flex-col justify-between border-l-4 border-pastel-blue">
132178
<div class="flex justify-between items-start">
@@ -135,8 +181,8 @@ export class CalendarWidgetRender implements WidgetRender<CalendarWidgetType> {
135181
<p class="text-2xl font-extrabold text-gray-800">${dateElement}</p>
136182
</div>
137183
<button class="text-pastel-blue hover:text-pastel-blue/80 p-2 rounded-full transition-colors" style="background-color: rgba(138, 137, 240, 0.1);"
138-
onclick="showCalendarModal('${modalId}')">
139-
<i ngSkipHydration="calendar" class="w-6 h-6"></i>
184+
onclick="showCalendarModal('${modalId}', ${CALENDAR_WIDGET_WEEKDAY_INDEXES[firstDayOfWeek]})">
185+
<i data-lucide="calendar" class="w-6 h-6"></i>
140186
</button>
141187
</div>
142188
@@ -159,13 +205,7 @@ export class CalendarWidgetRender implements WidgetRender<CalendarWidgetType> {
159205
</div>
160206
161207
<div class="grid grid-cols-7 gap-1 text-sm font-bold uppercase text-gray-500 mb-2 text-center">
162-
<div>Mo</div>
163-
<div>Tu</div>
164-
<div>We</div>
165-
<div>Th</div>
166-
<div>Fr</div>
167-
<div class="text-red-500">Sa</div>
168-
<div class="text-red-500">Su</div>
208+
${weekdayHeaders}
169209
</div>
170210
171211
<div id="${modalId}-calendar-grid" class="grid grid-cols-7 gap-1">

web/src/server/widgets/calendar-widget.utils.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,12 @@ function updateDateWidget(scope: Document | HTMLElement = document): void {
7575
/**
7676
* Renders calendar days in the modal
7777
* @param modalId - ID of the modal element
78+
* @param startDayNumberOfWeek - Index of the first day of the week (0 = Sunday, 1 = Monday, etc.)
7879
* @param scope - Document or element scope for DOM operations
7980
*/
8081
function renderCalendarDays(
8182
modalId: string,
83+
startDayNumberOfWeek: number,
8284
scope: Document | HTMLElement = document
8385
): void {
8486
const calendarContainer = getElementById(scope, `${modalId}-calendar-grid`);
@@ -98,11 +100,18 @@ function renderCalendarDays(
98100
setTextContent(monthTitle, monthName);
99101

100102
const firstDayOfMonth = new Date(currentYear, currentMonth, 1);
101-
const startDayOfWeek = (firstDayOfMonth.getDay() + 6) % 7;
103+
// Adjust the starting day calculation based on the first day of week setting
104+
const firstDayOfWeek = firstDayOfMonth.getDay(); // 0 = Sunday, 1 = Monday, etc.
105+
// Calculate how many days to shift based on the startDayNumberOfWeek
106+
let startDayOffset = firstDayOfWeek - startDayNumberOfWeek;
107+
if (startDayOffset < 0) {
108+
startDayOffset += 7;
109+
}
110+
102111
const lastDay = new Date(currentYear, currentMonth + 1, 0).getDate();
103112

104113
// Add empty cells for days before the first day of the month
105-
for (let i = 0; i < startDayOfWeek; i++) {
114+
for (let i = 0; i < startDayOffset; i++) {
106115
const emptyCell = createElement(document, 'div');
107116
emptyCell.className = 'text-center p-2';
108117
appendChild(calendarContainer, emptyCell);
@@ -117,9 +126,7 @@ function renderCalendarDays(
117126
setTextContent(dayCell, day.toString());
118127

119128
if (day < currentDay) {
120-
dayCell.classList.add(
121-
'text-gray-800'
122-
);
129+
dayCell.classList.add('text-gray-800');
123130
dayCell.style.backgroundColor = 'rgba(138, 137, 240, 0.2)';
124131
} else if (day === currentDay) {
125132
dayCell.classList.add(
@@ -140,10 +147,12 @@ function renderCalendarDays(
140147
/**
141148
* Shows the calendar modal for a specific widget
142149
* @param modalId - ID of the modal element
150+
* @param startDayNumberOfWeek - Index of the first day of the week (0 = Sunday, 1 = Monday, etc.)
143151
* @param scope - Document or element scope for DOM operations
144152
*/
145153
export function showCalendarModal(
146154
modalId: string,
155+
startDayNumberOfWeek: number,
147156
scope: Document | HTMLElement = document
148157
): void {
149158
const modal = getElementById(scope, modalId);
@@ -158,15 +167,15 @@ export function showCalendarModal(
158167
hideCalendarModal(modalId, scope);
159168
}
160169
};
161-
170+
162171
// Store the event listener so we can remove it later
163172
(modal as any).closeModalOnBackgroundClick = closeModalOnBackgroundClick;
164173
modal.addEventListener('click', closeModalOnBackgroundClick);
165174

166-
renderCalendarDays(modalId, scope);
167-
if (!isSSR) {
168-
createIcons({ icons });
169-
}
175+
renderCalendarDays(modalId, startDayNumberOfWeek, scope);
176+
if (!isSSR) {
177+
createIcons({ icons });
178+
}
170179
}
171180
}
172181

@@ -183,10 +192,13 @@ export function hideCalendarModal(
183192
if (modal) {
184193
// Remove the event listener
185194
if ((modal as any).closeModalOnBackgroundClick) {
186-
modal.removeEventListener('click', (modal as any).closeModalOnBackgroundClick);
195+
modal.removeEventListener(
196+
'click',
197+
(modal as any).closeModalOnBackgroundClick
198+
);
187199
delete (modal as any).closeModalOnBackgroundClick;
188200
}
189-
201+
190202
modal.classList.remove('opacity-100');
191203
modal.classList.add('opacity-0');
192204
setTimeout(() => modal.classList.add('hidden'), 300);

web/src/server/widgets/habits-widget.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export class HabitsWidgetRender
286286
<i data-lucide="pencil" class="w-5 h-5"></i>
287287
</button>
288288
<div class="flex items-center">
289-
<i ngSkipHydration="activity" class="w-8 h-8 text-pastel-green mr-3"></i>
289+
<i data-lucide="activity" class="w-8 h-8 text-pastel-green mr-3"></i>
290290
<p class="text-lg font-medium text-gray-600">Habits</p>
291291
</div>
292292
<div id="habits-widget-content">
@@ -344,7 +344,7 @@ export class HabitsWidgetRender
344344
// Helper function to render widget content
345345
function renderWidgetContent(widgetId: string): string {
346346
const items = getHabitItems(widgetId);
347-
347+
348348
if (items?.length === 0) {
349349
return '<p class="text-gray-500 text-sm">No habits configured</p>';
350350
}
@@ -358,7 +358,9 @@ export class HabitsWidgetRender
358358
<div class="grid grid-cols-3 gap-2 mt-2">
359359
${topItems
360360
.map(item => {
361-
const percentage = calculateProgressPercentage(item as HabitsWidgetItemType & HabitsWidgetStateType);
361+
const percentage = calculateProgressPercentage(
362+
item as HabitsWidgetItemType & HabitsWidgetStateType
363+
);
362364
const progressBarColor = getProgressBarColor(percentage);
363365
364366
return `

0 commit comments

Comments
 (0)