Files
Momento/memento-note/components/structured-views/notes-calendar-view.tsx
Antigravity ba3ab3422a
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m29s
CI / Deploy production (on server) (push) Has been skipped
feat: Link Preview block (carte aperçu URL) + proxy images
- Bloc Link Preview : colle une URL → carte avec titre, description, image, favicon
- API /api/link-preview : extraction OpenGraph + meta tags
- API /api/image-proxy : contourne le hotlinking (Referer spoofing)
- Métadonnées persistées en HTML (data-preview JSON) — pas de refetch au reload
- Texte indexable : titre + description + URL inclus pour recherche/embeddings
- Modal propre pour saisir l'URL (plus de prompt())
- Slash menu + smart paste 'Coller comme carte aperçu'
- i18n FR/EN complet
- Fix: bouton calendrier retiré du sélecteur de vue
2026-06-14 17:43:53 +00:00

203 lines
6.7 KiB
TypeScript

'use client'
import { useState, useMemo, useCallback } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { cn } from '@/lib/utils'
import type { Note } from '@/lib/types'
import type { NotebookSchemaPayload, NotePropertyValues } from '@/lib/structured-views/types'
type NotesCalendarViewProps = {
notes: Note[]
schema: NotebookSchemaPayload
noteValues: Record<string, NotePropertyValues>
notebookColor?: string | null
onOpen: (note: Note) => void
onPropertyChange: (noteId: string, propertyId: string, value: unknown) => void
}
const WEEKDAYS_FR = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']
const WEEKDAYS_EN = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const MONTHS_FR = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
const MONTHS_EN = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
function getDateString(value: unknown): string | null {
if (!value || typeof value !== 'string') return null
const d = new Date(value)
if (isNaN(d.getTime())) return null
return d.toISOString().slice(0, 10)
}
export function NotesCalendarView({
notes,
schema,
noteValues,
notebookColor,
onOpen,
onPropertyChange,
}: NotesCalendarViewProps) {
const { t, language } = useLanguage()
const isFR = language === 'fr'
const weekdays = isFR ? WEEKDAYS_FR : WEEKDAYS_EN
const months = isFR ? MONTHS_FR : MONTHS_EN
const [currentMonth, setCurrentMonth] = useState(() => {
const now = new Date()
return new Date(now.getFullYear(), now.getMonth(), 1)
})
const dateProperty = useMemo(() => {
return schema.properties.find(p => p.type === 'date')
}, [schema.properties])
const notesByDate = useMemo(() => {
const map = new Map<string, Note[]>()
if (!dateProperty) return map
for (const note of notes) {
const vals = noteValues[note.id]
if (!vals) continue
const raw = vals[dateProperty.id]
const dateStr = getDateString(raw)
if (!dateStr) continue
const existing = map.get(dateStr) || []
existing.push(note)
map.set(dateStr, existing)
}
return map
}, [notes, noteValues, dateProperty])
const calendarDays = useMemo(() => {
const year = currentMonth.getFullYear()
const month = currentMonth.getMonth()
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const startWeekday = (firstDay.getDay() + 6) % 7
const daysInMonth = lastDay.getDate()
const days: Array<{ date: string | null; day: number | null; isToday: boolean }> = []
for (let i = 0; i < startWeekday; i++) {
days.push({ date: null, day: null, isToday: false })
}
const todayStr = new Date().toISOString().slice(0, 10)
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = new Date(year, month, d).toISOString().slice(0, 10)
days.push({ date: dateStr, day: d, isToday: dateStr === todayStr })
}
const remaining = (7 - (days.length % 7)) % 7
for (let i = 0; i < remaining; i++) {
days.push({ date: null, day: null, isToday: false })
}
return days
}, [currentMonth])
const prevMonth = useCallback(() => {
setCurrentMonth(m => new Date(m.getFullYear(), m.getMonth() - 1, 1))
}, [])
const nextMonth = useCallback(() => {
setCurrentMonth(m => new Date(m.getFullYear(), m.getMonth() + 1, 1))
}, [])
const todayMonth = useCallback(() => {
const now = new Date()
setCurrentMonth(new Date(now.getFullYear(), now.getMonth(), 1))
}, [])
if (!dateProperty) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center" dir="auto">
<p className="text-sm text-muted-foreground">
{t('structuredViews.calendarNoDateProperty')}
</p>
</div>
)
}
return (
<div className="w-full" dir="auto">
<div className="flex items-center justify-between mb-4 px-1">
<div className="flex items-center gap-2">
<h3 className="text-base font-semibold">
{months[currentMonth.getMonth()]} {currentMonth.getFullYear()}
</h3>
</div>
<div className="flex items-center gap-1">
<button
onClick={todayMonth}
className="px-2.5 py-1 text-xs font-medium rounded-md hover:bg-muted transition-colors"
>
{isFR ? "Aujourd'hui" : 'Today'}
</button>
<button
onClick={prevMonth}
className="p-1 rounded-md hover:bg-muted transition-colors"
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
onClick={nextMonth}
className="p-1 rounded-md hover:bg-muted transition-colors"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-px bg-border/30 rounded-lg overflow-hidden border border-border/30">
{weekdays.map(wd => (
<div key={wd} className="bg-muted/40 px-2 py-1.5 text-center text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
{wd}
</div>
))}
{calendarDays.map((day, i) => {
const dayNotes = day.date ? (notesByDate.get(day.date) || []) : []
return (
<div
key={i}
className={cn(
'min-h-[80px] bg-background p-1 flex flex-col gap-0.5',
!day.date && 'bg-muted/20',
)}
>
{day.day && (
<span
className={cn(
'text-xs font-medium w-6 h-6 flex items-center justify-center rounded-full',
day.isToday
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground',
)}
>
{day.day}
</span>
)}
{dayNotes.slice(0, 3).map(note => (
<button
key={note.id}
onClick={() => onOpen(note)}
className="text-left text-[11px] leading-tight px-1.5 py-0.5 rounded bg-foreground/[0.04] hover:bg-primary/10 text-foreground/70 hover:text-primary transition-colors truncate"
>
{note.title || (isFR ? 'Sans titre' : 'Untitled')}
</button>
))}
{dayNotes.length > 3 && (
<span className="text-[10px] text-muted-foreground px-1.5">
+{dayNotes.length - 3}
</span>
)}
</div>
)
})}
</div>
</div>
)
}