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
This commit is contained in:
202
memento-note/components/structured-views/notes-calendar-view.tsx
Normal file
202
memento-note/components/structured-views/notes-calendar-view.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { NotebookSchemaPayload, NotePropertyValues, StructuredViewMode } fr
|
||||
import { NotesStructuredTable } from './notes-structured-table'
|
||||
import { NotesKanbanView } from './notes-kanban-view'
|
||||
import { NotesGalleryView } from './notes-gallery-view'
|
||||
import { NotesCalendarView } from './notes-calendar-view'
|
||||
|
||||
type StructuredViewsContainerProps = {
|
||||
mode: StructuredViewMode
|
||||
@@ -101,5 +102,18 @@ export function StructuredViewsContainer({
|
||||
)
|
||||
}
|
||||
|
||||
if (mode === 'calendar') {
|
||||
return (
|
||||
<NotesCalendarView
|
||||
notes={notes}
|
||||
schema={schema}
|
||||
noteValues={noteValues}
|
||||
notebookColor={notebookColor}
|
||||
onOpen={onOpen}
|
||||
onPropertyChange={saveProperty}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user