first commit
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<header class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-wide text-obs-l-text-muted dark:text-obs-d-text-muted">Calendrier</p>
|
||||
<h2 class="text-lg font-semibold text-obs-l-text-main dark:text-obs-d-text-main">
|
||||
{{ viewDate() | calendarDate: 'monthViewTitle' : 'fr' }}
|
||||
</h2>
|
||||
@if (selectionLabel(); as label) {
|
||||
<span class="mt-1 inline-flex items-center rounded-full bg-obs-l-bg-secondary/70 px-2 py-0.5 text-xs font-medium text-obs-l-text-muted dark:bg-obs-d-bg-secondary/60 dark:text-obs-d-text-muted">
|
||||
{{ label }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
(click)="shiftMonth(-1)"
|
||||
aria-label="Mois précédent"
|
||||
class="h-9 w-9 rounded-full border border-obs-l-border bg-obs-l-bg-main text-lg leading-none text-obs-l-text-main transition hover:bg-obs-l-bg-secondary dark:border-obs-d-border dark:bg-obs-d-bg-main dark:text-obs-d-text-main dark:hover:bg-obs-d-bg-secondary"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="goToToday()"
|
||||
aria-label="Revenir à aujourd'hui"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-obs-l-border bg-obs-l-bg-main px-3 py-1.5 text-sm font-medium text-obs-l-text-main transition hover:bg-obs-l-bg-secondary dark:border-obs-d-border dark:bg-obs-d-bg-main dark:text-obs-d-text-main dark:hover:bg-obs-d-bg-secondary"
|
||||
>
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="shiftMonth(1)"
|
||||
aria-label="Mois suivant"
|
||||
class="h-9 w-9 rounded-full border border-obs-l-border bg-obs-l-bg-main text-lg leading-none text-obs-l-text-main transition hover:bg-obs-l-bg-secondary dark:border-obs-d-border dark:bg-obs-d-bg-main dark:text-obs-d-text-main dark:hover:bg-obs-d-bg-secondary"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="rounded-2xl border border-obs-l-border bg-obs-l-bg-main/80 shadow-panel backdrop-blur-xs dark:border-obs-d-border dark:bg-obs-d-bg-secondary/80">
|
||||
<mwl-calendar-month-view
|
||||
class="calendar-compact"
|
||||
[viewDate]="viewDate()"
|
||||
[refresh]="refresh$"
|
||||
[cellTemplate]="dayCellTemplate"
|
||||
[weekStartsOn]="1"
|
||||
[events]="[]"
|
||||
(pointerup)="onDatePointerUp()"
|
||||
></mwl-calendar-month-view>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-3 text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">
|
||||
<span class="inline-flex items-center gap-2 rounded-full bg-obs-l-bg-secondary/60 px-3 py-1 dark:bg-obs-d-bg-secondary/60">
|
||||
<span class="h-2 w-2 rounded-full bg-gradient-to-r from-green-500 to-emerald-400"></span>
|
||||
Création
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2 rounded-full bg-obs-l-bg-secondary/60 px-3 py-1 dark:bg-obs-d-bg-secondary/60">
|
||||
<span class="h-2 w-2 rounded-full bg-gradient-to-r from-indigo-400 to-indigo-600"></span>
|
||||
Modification
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #dayCellTemplate let-day="day">
|
||||
<button
|
||||
type="button"
|
||||
class="day-cell"
|
||||
[ngClass]="getCellClasses(day)"
|
||||
(pointerdown)="onDatePointerDown(day, $event)"
|
||||
(pointerenter)="onDatePointerEnter(day)"
|
||||
>
|
||||
<span class="day-number">{{ day.date | date: 'd' }}</span>
|
||||
<div class="indicators">
|
||||
@if (hasCreated(day.date)) {
|
||||
<span class="indicator cal-event-created" aria-hidden="true"></span>
|
||||
}
|
||||
@if (hasUpdated(day.date)) {
|
||||
<span class="indicator cal-event-updated" aria-hidden="true"></span>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,272 @@
|
||||
.calendar-card {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
background: rgba(248, 250, 252, 0.94);
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.6rem 0.65rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
box-shadow: 0 12px 32px -20px rgba(15, 23, 42, 0.28);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.toolbar-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.55rem;
|
||||
border: 1px solid rgba(184, 162, 148, 0.45);
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: rgba(148, 163, 184, 0.22);
|
||||
border-color: rgba(148, 163, 184, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.6rem;
|
||||
color: rgba(47, 60, 86, 0.65);
|
||||
margin-top: 3.2rem;
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.18);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-dot.legend-created {
|
||||
background: linear-gradient(135deg, #22c55e, #34d399);
|
||||
}
|
||||
|
||||
.legend-dot.legend-updated {
|
||||
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||
}
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 1.4rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.15rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.is-outside {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&.is-today {
|
||||
border-color: rgba(99, 102, 241, 0.55);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: rgba(129, 140, 248, 0.16);
|
||||
}
|
||||
|
||||
&.is-range-start,
|
||||
&.is-range-end {
|
||||
border-color: rgba(129, 140, 248, 0.75);
|
||||
background: rgba(129, 140, 248, 0.28);
|
||||
}
|
||||
|
||||
.day-number {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border-radius: 0.45rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.indicators {
|
||||
margin-top: 0.12rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
width: 100%;
|
||||
|
||||
.indicator {
|
||||
display: block;
|
||||
height: 2px;
|
||||
border-radius: 9999px;
|
||||
margin: 0.25px 0;
|
||||
box-shadow: 0 0 4px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.indicator.cal-event-created {
|
||||
background: linear-gradient(90deg, #22c55e, #34d399);
|
||||
}
|
||||
|
||||
.indicator.cal-event-updated {
|
||||
background: linear-gradient(90deg, #8b5cf6, #6366f1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.dark) {
|
||||
.calendar-card {
|
||||
// background: rgba(34, 39, 54, 0.96);
|
||||
background: #202123;
|
||||
border-color: rgba(89, 102, 126, 0.5);
|
||||
|
||||
max-width: 280px;
|
||||
padding: 0.6rem 0.65rem 1rem;
|
||||
}
|
||||
|
||||
.toolbar-controls button {
|
||||
background: #232527;
|
||||
border-color: rgba(99, 102, 241, 0.35);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: rgba(129, 140, 248, 0.22);
|
||||
border-color: rgba(129, 140, 248, 0.55);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-controls {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
gap: 0.1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.toolbar-title .selection-label {
|
||||
color: rgba(191, 214, 255, 0.75);
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
color: rgba(210, 219, 237, 0.8);
|
||||
gap: 0.85rem;
|
||||
|
||||
.legend-item {
|
||||
background: rgba(99, 102, 241, 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
min-height: 1.25rem;
|
||||
padding: 0.12rem;
|
||||
|
||||
&.is-selected {
|
||||
background: rgba(129, 140, 248, 0.28);
|
||||
}
|
||||
|
||||
&.is-range-start,
|
||||
&.is-range-end {
|
||||
background: rgba(129, 140, 248, 0.38);
|
||||
border-color: rgba(167, 139, 250, 0.75);
|
||||
box-shadow: 0 0 0 1px rgba(22, 163, 74, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .calendar-compact {
|
||||
max-height: 172px;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .calendar-compact .cal-week {
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
:host ::ng-deep .calendar-compact .cal-month-view {
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border: 1px solid rgba(203, 213, 225, 0.45);
|
||||
}
|
||||
|
||||
:host ::ng-deep .calendar-compact .cal-header {
|
||||
padding: 0.15rem 0.3rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:host ::ng-deep .calendar-compact .cal-header .cal-cell {
|
||||
font-size: 0;
|
||||
text-transform: uppercase;
|
||||
padding: 0.15rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:host ::ng-deep .calendar-compact .cal-header .cal-cell::first-letter {
|
||||
font-size: 0.55rem;
|
||||
font-weight: 700;
|
||||
display: inline-block;
|
||||
color: rgba(79, 70, 229, 0.85);
|
||||
}
|
||||
|
||||
:host ::ng-deep .calendar-compact .cal-cell {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(226, 232, 240, 0.6);
|
||||
padding: 0.15rem;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .calendar-compact .cal-month-view {
|
||||
// background: rgba(37, 44, 63, 0.95);
|
||||
background: #2B2D46;
|
||||
border-color: rgba(88, 103, 132, 0.55);
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .calendar-compact .cal-cell {
|
||||
// background: rgba(48, 58, 78, 0.92);
|
||||
background: #2D2E30;
|
||||
border-color: rgba(107, 114, 128, 0.4);
|
||||
color: rgba(220, 230, 246, 0.95);
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .calendar-compact .cal-day-number {
|
||||
color: rgba(236, 253, 245, 0.9);
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .calendar-compact .cal-header .cal-cell::first-letter {
|
||||
color: rgba(167, 139, 250, 0.85);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
HostListener,
|
||||
computed,
|
||||
inject,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CalendarMonthViewComponent,
|
||||
CalendarMonthViewDay,
|
||||
CalendarDatePipe,
|
||||
DateAdapter,
|
||||
provideCalendar,
|
||||
} from 'angular-calendar';
|
||||
import { adapterFactory } from 'angular-calendar/date-adapters/date-fns';
|
||||
import { MarkdownCalendarService } from '../../services/markdown-calendar.service';
|
||||
import { FileMetadata } from '../../types';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
interface DayBucket {
|
||||
created: FileMetadata[];
|
||||
updated: FileMetadata[];
|
||||
}
|
||||
|
||||
interface DateRange {
|
||||
start: Date | null;
|
||||
end: Date | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-markdown-calendar',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CalendarMonthViewComponent, CalendarDatePipe],
|
||||
templateUrl: './markdown-calendar.component.html',
|
||||
styleUrls: ['./markdown-calendar.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
provideCalendar({
|
||||
provide: DateAdapter,
|
||||
useFactory: adapterFactory,
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class MarkdownCalendarComponent {
|
||||
private readonly calendarService = inject(MarkdownCalendarService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
readonly fileSelected = output<string>();
|
||||
readonly searchResultsChange = output<FileMetadata[]>();
|
||||
readonly searchStateChange = output<'idle' | 'loading' | 'error'>();
|
||||
readonly searchErrorChange = output<string | null>();
|
||||
readonly selectionSummaryChange = output<string | null>();
|
||||
readonly requestSearchPanel = output<void>();
|
||||
|
||||
readonly viewDate = signal(this.normalizeDay(new Date()));
|
||||
readonly metadata = signal<FileMetadata[]>([]);
|
||||
readonly dayBuckets = computed<Map<string, DayBucket>>(() =>
|
||||
this.calendarService.groupMetadataByDay(this.metadata())
|
||||
);
|
||||
|
||||
readonly isLoading = signal<boolean>(false);
|
||||
readonly loadError = signal<string | null>(null);
|
||||
|
||||
readonly searchResults = signal<FileMetadata[]>([]);
|
||||
readonly isSearching = signal<boolean>(false);
|
||||
readonly searchError = signal<string | null>(null);
|
||||
|
||||
readonly selectedRange = signal<DateRange>({ start: null, end: null });
|
||||
readonly selectionLabel = computed(() => this.buildSelectionLabel());
|
||||
|
||||
readonly refresh$ = new Subject<void>();
|
||||
|
||||
private dragAnchor: Date | null = null;
|
||||
private searchRequestId = 0;
|
||||
|
||||
constructor() {
|
||||
this.loadMetadata();
|
||||
}
|
||||
|
||||
// --- Lifecycle & data loading ------------------------------------------------
|
||||
|
||||
private loadMetadata(): void {
|
||||
this.isLoading.set(true);
|
||||
this.loadError.set(null);
|
||||
|
||||
this.calendarService
|
||||
.getFilesMetadata()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
this.metadata.set(data);
|
||||
this.isLoading.set(false);
|
||||
this.refresh$.next();
|
||||
},
|
||||
error: () => {
|
||||
this.isLoading.set(false);
|
||||
this.loadError.set('Impossible de charger les métadonnées des fichiers.');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Calendar interactions ---------------------------------------------------
|
||||
|
||||
shiftMonth(offset: number): void {
|
||||
const current = this.viewDate();
|
||||
const shifted = new Date(current);
|
||||
shifted.setMonth(current.getMonth() + offset, 1);
|
||||
this.viewDate.set(this.normalizeDay(shifted));
|
||||
this.refresh$.next();
|
||||
}
|
||||
|
||||
goToToday(): void {
|
||||
this.viewDate.set(this.normalizeDay(new Date()));
|
||||
this.refresh$.next();
|
||||
}
|
||||
|
||||
onDatePointerDown(day: CalendarMonthViewDay<Date>, event: PointerEvent): void {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const date = this.normalizeDay(day.date);
|
||||
this.dragAnchor = date;
|
||||
this.selectedRange.set({ start: date, end: date });
|
||||
this.isSearching.set(false);
|
||||
this.searchError.set(null);
|
||||
this.searchResults.set([]);
|
||||
this.searchRequestId++;
|
||||
this.searchResultsChange.emit([]);
|
||||
this.searchStateChange.emit('idle');
|
||||
this.searchErrorChange.emit(null);
|
||||
this.emitSelectionSummary();
|
||||
this.requestSearchPanel.emit();
|
||||
this.refresh$.next();
|
||||
}
|
||||
|
||||
onDatePointerEnter(day: CalendarMonthViewDay<Date>): void {
|
||||
if (!this.dragAnchor) {
|
||||
return;
|
||||
}
|
||||
const hovered = this.normalizeDay(day.date);
|
||||
const range = this.normalizeRange(this.dragAnchor, hovered);
|
||||
this.selectedRange.set(range);
|
||||
this.emitSelectionSummary();
|
||||
this.requestSearchPanel.emit();
|
||||
this.refresh$.next();
|
||||
}
|
||||
|
||||
@HostListener('document:pointerup')
|
||||
finishRangeSelection(): void {
|
||||
this.finalizeSelection();
|
||||
}
|
||||
|
||||
onDatePointerUp(): void {
|
||||
this.finalizeSelection();
|
||||
}
|
||||
|
||||
private finalizeSelection(): void {
|
||||
if (!this.dragAnchor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRange = this.selectedRange();
|
||||
const { start, end } = currentRange;
|
||||
this.dragAnchor = null;
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (start.getTime() === end.getTime()) {
|
||||
this.fetchResultsForSingleDate(start);
|
||||
} else {
|
||||
this.fetchResultsForRange(start, end);
|
||||
}
|
||||
this.emitSelectionSummary();
|
||||
this.refresh$.next();
|
||||
}
|
||||
|
||||
// --- Result handling ---------------------------------------------------------
|
||||
|
||||
private fetchResultsForSingleDate(date: Date): void {
|
||||
this.runSearch(this.calendarService.searchFilesByDate(date));
|
||||
}
|
||||
|
||||
private fetchResultsForRange(start: Date, end: Date): void {
|
||||
this.runSearch(this.calendarService.searchFilesByDateRange(start, end));
|
||||
}
|
||||
|
||||
private runSearch(source$: Observable<FileMetadata[]>): void {
|
||||
const token = ++this.searchRequestId;
|
||||
this.isSearching.set(true);
|
||||
this.searchError.set(null);
|
||||
this.searchStateChange.emit('loading');
|
||||
this.searchErrorChange.emit(null);
|
||||
|
||||
source$.subscribe({
|
||||
next: (files: FileMetadata[]) => {
|
||||
if (token !== this.searchRequestId) {
|
||||
return;
|
||||
}
|
||||
this.searchResults.set(files);
|
||||
this.isSearching.set(false);
|
||||
this.searchStateChange.emit('idle');
|
||||
this.searchResultsChange.emit(files);
|
||||
},
|
||||
error: () => {
|
||||
if (token !== this.searchRequestId) {
|
||||
return;
|
||||
}
|
||||
this.isSearching.set(false);
|
||||
this.searchError.set('Recherche impossible pour cette période.');
|
||||
this.searchStateChange.emit('error');
|
||||
this.searchErrorChange.emit('Recherche impossible pour cette période.');
|
||||
this.searchResultsChange.emit([]);
|
||||
this.searchResults.set([]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openFile(file: FileMetadata): void {
|
||||
this.fileSelected.emit(file.id);
|
||||
}
|
||||
|
||||
trackByFileId = (_: number, file: FileMetadata) => file.id;
|
||||
|
||||
// --- Template helpers --------------------------------------------------------
|
||||
|
||||
hasCreated(date: Date): boolean {
|
||||
const bucket = this.getBucketForDate(date);
|
||||
return !!bucket && bucket.created.length > 0;
|
||||
}
|
||||
|
||||
hasUpdated(date: Date): boolean {
|
||||
const bucket = this.getBucketForDate(date);
|
||||
return !!bucket && bucket.updated.length > 0;
|
||||
}
|
||||
|
||||
isInSelectedRange(date: Date): boolean {
|
||||
const { start, end } = this.selectedRange();
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
const value = this.normalizeDay(date).getTime();
|
||||
return value >= start.getTime() && value <= end.getTime();
|
||||
}
|
||||
|
||||
isRangeStart(date: Date): boolean {
|
||||
const { start } = this.selectedRange();
|
||||
return !!start && this.normalizeDay(date).getTime() === start.getTime();
|
||||
}
|
||||
|
||||
isRangeEnd(date: Date): boolean {
|
||||
const { end } = this.selectedRange();
|
||||
return !!end && this.normalizeDay(date).getTime() === end.getTime();
|
||||
}
|
||||
|
||||
getCellClasses(day: CalendarMonthViewDay<Date>): Record<string, boolean> {
|
||||
const date = this.normalizeDay(day.date);
|
||||
return {
|
||||
'is-outside': !day.inMonth,
|
||||
'is-today': this.isToday(date),
|
||||
'is-selected': this.isInSelectedRange(date),
|
||||
'is-range-start': this.isRangeStart(date),
|
||||
'is-range-end': this.isRangeEnd(date),
|
||||
};
|
||||
}
|
||||
|
||||
// --- Utilities ---------------------------------------------------------------
|
||||
|
||||
private buildSelectionLabel(): string | null {
|
||||
const { start, end } = this.selectedRange();
|
||||
if (!start || !end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const format = (date: Date) =>
|
||||
new Intl.DateTimeFormat('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).format(date);
|
||||
|
||||
const startLabel = format(start);
|
||||
const endLabel = format(end);
|
||||
|
||||
return start.getTime() === end.getTime()
|
||||
? startLabel
|
||||
: `${startLabel} – ${endLabel}`;
|
||||
}
|
||||
|
||||
private isToday(date: Date): boolean {
|
||||
const today = this.normalizeDay(new Date());
|
||||
return date.getTime() === today.getTime();
|
||||
}
|
||||
|
||||
private normalizeDay(date: Date): Date {
|
||||
const normalized = new Date(date);
|
||||
normalized.setHours(0, 0, 0, 0);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private normalizeRange(start: Date, end: Date): DateRange {
|
||||
const normalizedStart = this.normalizeDay(start);
|
||||
const normalizedEnd = this.normalizeDay(end);
|
||||
if (normalizedStart.getTime() <= normalizedEnd.getTime()) {
|
||||
return { start: normalizedStart, end: normalizedEnd };
|
||||
}
|
||||
return { start: normalizedEnd, end: normalizedStart };
|
||||
}
|
||||
|
||||
private getBucketForDate(date: Date): DayBucket | undefined {
|
||||
const key = this.normalizeDay(date).toDateString();
|
||||
return this.dayBuckets().get(key);
|
||||
}
|
||||
|
||||
private emitSelectionSummary(): void {
|
||||
this.selectionSummaryChange.emit(this.selectionLabel());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user