♻️(front) refactor MiniCalendar component

Update MiniCalendar for integration with CalendarContext
and improved locale support for date navigation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Panchout
2026-01-25 20:34:57 +01:00
parent 8be9b1db52
commit 717029bd96
2 changed files with 140 additions and 58 deletions

View File

@@ -6,16 +6,23 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 0.5rem; margin-bottom: 1rem;
padding: 0 0.25rem;
} }
&__month-title { &__month-title {
font-size: 0.875rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
text-transform: capitalize; text-transform: capitalize;
color: var(--c--theme--colors--greyscale-800); color: var(--c--theme--colors--greyscale-800);
} }
&__nav {
display: flex;
align-items: center;
gap: 0.25rem;
}
&__nav-btn { &__nav-btn {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -26,11 +33,12 @@
background: transparent; background: transparent;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
color: var(--c--theme--colors--greyscale-600); color: var(--c--theme--colors--greyscale-500);
transition: background-color 0.15s; transition: background-color 0.15s, color 0.15s;
&:hover { &:hover {
background-color: var(--c--theme--colors--greyscale-100); background-color: var(--c--theme--colors--greyscale-100);
color: var(--c--theme--colors--greyscale-700);
} }
.material-icons { .material-icons {
@@ -38,9 +46,14 @@
} }
} }
&__grid {
display: flex;
flex-direction: column;
}
&__weekdays { &__weekdays {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: 2rem repeat(7, 1fr);
gap: 0; gap: 0;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
@@ -50,16 +63,37 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 1.5rem; height: 1.5rem;
font-size: 0.625rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
color: var(--c--theme--colors--greyscale-500); color: var(--c--theme--colors--greyscale-800);
text-transform: uppercase; text-transform: lowercase;
&--week-num {
color: var(--c--theme--colors--greyscale-500);
font-weight: 500;
font-size: 0.7rem;
}
} }
&__days { &__body {
display: flex;
flex-direction: column;
gap: 2px;
}
&__week {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: 2rem repeat(7, 1fr);
gap: 1px; gap: 0;
}
&__week-number {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
color: var(--c--theme--colors--greyscale-400);
font-weight: 400;
} }
&__day { &__day {
@@ -70,10 +104,11 @@
aspect-ratio: 1; aspect-ratio: 1;
border: none; border: none;
background: transparent; background: transparent;
border-radius: 50%; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 0.75rem; font-size: 0.8rem;
color: var(--c--theme--colors--greyscale-800); color: var(--c--theme--colors--greyscale-800);
font-weight: 500;
transition: background-color 0.15s; transition: background-color 0.15s;
&:hover { &:hover {
@@ -82,21 +117,22 @@
&--outside { &--outside {
color: var(--c--theme--colors--greyscale-400); color: var(--c--theme--colors--greyscale-400);
font-weight: 400;
} }
&--today { &--today {
background-color: var(--c--theme--colors--primary-100);
color: var(--c--theme--colors--primary-600); color: var(--c--theme--colors--primary-600);
font-weight: 600; font-weight: 600;
} }
&--selected { &--selected {
background-color: var(--c--theme--colors--primary-500); background-color: #8b4513;
color: white; color: white;
font-weight: 600; font-weight: 600;
border-radius: 4px;
&:hover { &:hover {
background-color: var(--c--theme--colors--primary-600); background-color: #7a3d11;
} }
} }
} }

View File

@@ -2,7 +2,7 @@
* MiniCalendar component - A small month calendar for date navigation. * MiniCalendar component - A small month calendar for date navigation.
*/ */
import { useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { import {
addMonths, addMonths,
@@ -10,6 +10,7 @@ import {
endOfMonth, endOfMonth,
endOfWeek, endOfWeek,
format, format,
getWeek,
isSameDay, isSameDay,
isSameMonth, isSameMonth,
startOfMonth, startOfMonth,
@@ -17,18 +18,42 @@ import {
subMonths, subMonths,
} from "date-fns"; } from "date-fns";
import { fr } from "date-fns/locale"; import { fr } from "date-fns/locale";
import { useTranslation } from "react-i18next";
import { useCalendarContext } from "../contexts";
interface MiniCalendarProps { interface MiniCalendarProps {
selectedDate: Date; selectedDate: Date;
onDateSelect: (date: Date) => void; onDateSelect: (date: Date) => void;
} }
// Helper to chunk array into groups of n
const chunkArray = <T,>(arr: T[], size: number): T[][] => {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
export const MiniCalendar = ({ export const MiniCalendar = ({
selectedDate, selectedDate,
onDateSelect, onDateSelect,
}: MiniCalendarProps) => { }: MiniCalendarProps) => {
const { t } = useTranslation();
const { goToDate, currentDate } = useCalendarContext();
const [viewDate, setViewDate] = useState(selectedDate); const [viewDate, setViewDate] = useState(selectedDate);
// Sync viewDate when main calendar navigates (via prev/next buttons)
useEffect(() => {
setViewDate((prevViewDate) => {
// Only update if the month changed to avoid unnecessary re-renders
if (!isSameMonth(prevViewDate, currentDate)) {
return currentDate;
}
return prevViewDate;
});
}, [currentDate]);
const days = useMemo(() => { const days = useMemo(() => {
const monthStart = startOfMonth(viewDate); const monthStart = startOfMonth(viewDate);
const monthEnd = endOfMonth(viewDate); const monthEnd = endOfMonth(viewDate);
@@ -38,7 +63,10 @@ export const MiniCalendar = ({
return eachDayOfInterval({ start: calendarStart, end: calendarEnd }); return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
}, [viewDate]); }, [viewDate]);
const weekDays = ["L", "M", "M", "J", "V", "S", "D"]; // Group days by weeks
const weeks = useMemo(() => chunkArray(days, 7), [days]);
const weekDays = ["lu", "ma", "me", "je", "ve", "sa", "di"];
const handlePrevMonth = () => { const handlePrevMonth = () => {
setViewDate(subMonths(viewDate, 1)); setViewDate(subMonths(viewDate, 1));
@@ -50,58 +78,76 @@ export const MiniCalendar = ({
const handleDayClick = (day: Date) => { const handleDayClick = (day: Date) => {
onDateSelect(day); onDateSelect(day);
goToDate(day);
}; };
return ( return (
<div className="mini-calendar"> <div className="mini-calendar">
<div className="mini-calendar__header"> <div className="mini-calendar__header">
<button
className="mini-calendar__nav-btn"
onClick={handlePrevMonth}
aria-label="Mois précédent"
>
<span className="material-icons">chevron_left</span>
</button>
<span className="mini-calendar__month-title"> <span className="mini-calendar__month-title">
{format(viewDate, "MMMM yyyy", { locale: fr })} {format(viewDate, "MMMM yyyy", { locale: fr })}
</span> </span>
<button <div className="mini-calendar__nav">
className="mini-calendar__nav-btn" <button
onClick={handleNextMonth} className="mini-calendar__nav-btn"
aria-label="Mois suivant" onClick={handlePrevMonth}
> aria-label={t("calendar.miniCalendar.previousMonth")}
<span className="material-icons">chevron_right</span> >
</button> <span className="material-icons">chevron_left</span>
</button>
<button
className="mini-calendar__nav-btn"
onClick={handleNextMonth}
aria-label={t("calendar.miniCalendar.nextMonth")}
>
<span className="material-icons">chevron_right</span>
</button>
</div>
</div> </div>
<div className="mini-calendar__weekdays"> <div className="mini-calendar__grid">
{weekDays.map((day, index) => ( {/* Header row with week number column and days */}
<div key={index} className="mini-calendar__weekday"> <div className="mini-calendar__weekdays">
{day} <div className="mini-calendar__weekday mini-calendar__weekday--week-num">
Sem.
</div> </div>
))} {weekDays.map((day, index) => (
</div> <div key={index} className="mini-calendar__weekday">
{day}
</div>
))}
</div>
<div className="mini-calendar__days"> {/* Calendar body with weeks */}
{days.map((day, index) => { <div className="mini-calendar__body">
const isCurrentMonth = isSameMonth(day, viewDate); {weeks.map((week, weekIndex) => {
const isSelected = isSameDay(day, selectedDate); const weekNumber = getWeek(week[0], { weekStartsOn: 1 });
const isToday = isSameDay(day, new Date()); return (
<div key={weekIndex} className="mini-calendar__week">
<div className="mini-calendar__week-number">{weekNumber}</div>
{week.map((day, dayIndex) => {
const isCurrentMonth = isSameMonth(day, viewDate);
const isSelected = isSameDay(day, selectedDate);
const isToday = isSameDay(day, new Date());
return ( return (
<button <button
key={index} key={dayIndex}
className={`mini-calendar__day ${ className={`mini-calendar__day ${
!isCurrentMonth ? "mini-calendar__day--outside" : "" !isCurrentMonth ? "mini-calendar__day--outside" : ""
} ${isSelected ? "mini-calendar__day--selected" : ""} ${ } ${isSelected ? "mini-calendar__day--selected" : ""} ${
isToday ? "mini-calendar__day--today" : "" isToday && !isSelected ? "mini-calendar__day--today" : ""
}`} }`}
onClick={() => handleDayClick(day)} onClick={() => handleDayClick(day)}
> >
{format(day, "d")} {format(day, "d")}
</button> </button>
); );
})} })}
</div>
);
})}
</div>
</div> </div>
</div> </div>
); );