- 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
203 lines
6.7 KiB
TypeScript
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>
|
|
)
|
|
}
|