feat: Link Preview block (carte aperçu URL) + proxy images
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m29s
CI / Deploy production (on server) (push) Has been skipped

- 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:
Antigravity
2026-06-14 17:43:53 +00:00
parent 5246ed41e9
commit ba3ab3422a
10 changed files with 657 additions and 2 deletions

View 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>
)
}

View File

@@ -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
}