@ -1,10 +1,14 @@
import { getNip52CalendarEventTagExtras , type CalendarEventMeta } from '@/lib/calendar-event'
import {
getNip52CalendarEventTagExtras ,
summarizeNip52DayGranularityTags ,
type CalendarEventMeta
} from '@/lib/calendar-event'
import { useSmartNoteNavigation } from '@/PageManager'
import { useSmartNoteNavigation } from '@/PageManager'
import { Event } from 'nostr-tools'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Calendar , ExternalLink , Link2 , MapPin } from 'lucide-react'
import { Calendar , ExternalLink , Link2 , MapPin } from 'lucide-react'
import { cn } from '@/lib/utils'
type Placement = 'beforeDescription' | 'afterDescription'
type Placement = 'beforeDescription' | 'afterDescription'
@ -35,6 +39,10 @@ export function CalendarEventNip52StructuredMeta({
const { t } = useTranslation ( )
const { t } = useTranslation ( )
const { navigateToNote } = useSmartNoteNavigation ( )
const { navigateToNote } = useSmartNoteNavigation ( )
const extras = useMemo ( ( ) = > getNip52CalendarEventTagExtras ( event ) , [ event ] )
const extras = useMemo ( ( ) = > getNip52CalendarEventTagExtras ( event ) , [ event ] )
const dayGranularitySummary = useMemo (
( ) = > summarizeNip52DayGranularityTags ( extras . dayGranularities ) ,
[ extras . dayGranularities ]
)
if ( placement === 'beforeDescription' ) {
if ( placement === 'beforeDescription' ) {
const summaryTrim = meta . summary ? . trim ( ) ? ? ''
const summaryTrim = meta . summary ? . trim ( ) ? ? ''
@ -42,41 +50,41 @@ export function CalendarEventNip52StructuredMeta({
const hasGeo = ! ! meta . geo ? . trim ( )
const hasGeo = ! ! meta . geo ? . trim ( )
if ( ! hasLocations && ! summaryTrim && ! hasGeo ) return null
if ( ! hasLocations && ! summaryTrim && ! hasGeo ) return null
const linkClass =
'inline-flex shrink-0 items-center gap-1 text-xs font-medium text-primary underline-offset-2 hover:underline'
return (
return (
< div className = "min-w-0 space-y-3 border-t border-border/50 pt-3" >
< div className = "min-w-0 space-y-2.5 border-t border-border/50 pt-2 " >
{ hasLocations ? (
{ hasLocations ? (
< div className = "space-y-1.5 " >
< div className = "space-y-1" >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
< div className = "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ meta . locations . length > 1 ? t ( 'calendarNip52Locations' ) : t ( 'calendarNip52Location' ) }
{ meta . locations . length > 1 ? t ( 'calendarNip52Locations' ) : t ( 'calendarNip52Location' ) }
< / div >
< / div >
{ meta . locations . length === 1 ? (
{ meta . locations . length === 1 ? (
< div className = "flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between" >
< div className = "flex min-w-0 flex-wrap items-baseline gap-x-1.5 gap-y-1 text-sm leading-snug text-foreground" >
< p className = "flex min-w-0 flex-1 gap-2 text-sm leading-snug text-foreground" >
< MapPin className = "mt-0.5 size-3.5 shrink-0 text-muted-foreground" aria - hidden / >
< MapPin className = "mt-0.5 size-4 shrink-0 text-muted-foreground" aria - hidden / >
< span className = "min-w-0" > { meta . locations [ 0 ] } < / span >
< span className = "min-w-0" > { meta . locations [ 0 ] } < / span >
< a
< / p >
href = { googleMapsPlaceUrl ( meta . locations [ 0 ] ) }
< Button variant = "outline" size = "sm" className = "h-8 w-full shrink-0 sm:w-auto" asChild >
target = "_blank"
< a href = { googleMapsPlaceUrl ( meta . locations [ 0 ] ) } target = "_blank" rel = "noopener noreferrer" >
rel = "noopener noreferrer"
< ExternalLink className = "size-3.5" / >
className = { linkClass }
{ t ( 'calendarNip52GoogleMaps' ) }
>
< / a >
< ExternalLink className = "size-3 shrink-0 opacity-80" aria - hidden / >
< / Button >
{ t ( 'calendarNip52GoogleMaps' ) }
< / a >
< / div >
< / div >
) : (
) : (
< ul className = "min-w-0 space-y-3 " >
< ul className = "min-w-0 space-y-2 " >
{ meta . locations . map ( ( loc , i ) = > (
{ meta . locations . map ( ( loc , i ) = > (
< li key = { ` ${ i } - ${ loc . slice ( 0 , 24 ) } ` } >
< li key = { ` ${ i } - ${ loc . slice ( 0 , 24 ) } ` } >
< div className = "flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between" >
< div className = "flex min-w-0 flex-wrap items-baseline gap-x-1.5 gap-y-1 text-sm leading-snug text-foreground" >
< p className = "flex min-w-0 flex-1 gap-2 text-sm leading-snug text-foreground" >
< MapPin className = "mt-0.5 size-3.5 shrink-0 text-muted-foreground" aria - hidden / >
< MapPin className = "mt-0.5 size-4 shrink-0 text-muted-foreground" aria - hidden / >
< span className = "min-w-0" > { loc } < / span >
< span className = "min-w-0" > { loc } < / span >
< a href = { googleMapsPlaceUrl ( loc ) } target = "_blank" rel = "noopener noreferrer" className = { linkClass } >
< / p >
< ExternalLink className = "size-3 shrink-0 opacity-80" aria - hidden / >
< Button variant = "outline" size = "sm" className = "h-8 w-full shrink-0 sm:w-auto" asChild >
{ t ( 'calendarNip52GoogleMaps' ) }
< a href = { googleMapsPlaceUrl ( loc ) } target = "_blank" rel = "noopener noreferrer" >
< / a >
< ExternalLink className = "size-3.5" / >
{ t ( 'calendarNip52GoogleMaps' ) }
< / a >
< / Button >
< / div >
< / div >
< / li >
< / li >
) ) }
) ) }
@ -86,106 +94,104 @@ export function CalendarEventNip52StructuredMeta({
) : null }
) : null }
{ summaryTrim ? (
{ summaryTrim ? (
< div className = "space-y-1" >
< div className = "space-y-1" >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
< div className = "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ t ( 'calendarNip52Summary' ) }
{ t ( 'calendarNip52Summary' ) }
< / div >
< / div >
< blockquote className = "border-l-2 border-primary/40 pl-3 text-sm leading-relaxed text-muted-foreground" >
< blockquote className = "border-l-2 border-primary/40 pl-2.5 text-sm leading-snug text-muted-foreground" >
{ summaryTrim }
{ summaryTrim }
< / blockquote >
< / blockquote >
< / div >
< / div >
) : null }
) : null }
{ hasGeo ? (
{ hasGeo ? (
< div className = "space-y-1" >
< div className = "space-y-1" >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
< div className = "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ t ( 'calendarNip52Geohash' ) }
{ t ( 'calendarNip52Geohash' ) }
< / div >
< / div >
< p className = "flex flex-wrap items-center gap-2 text-sm font-mono text-foreground" >
< div className = "flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5" >
< span className = "break-all" > { meta . geo . trim ( ) } < / span >
< span className = "break-all font-mono text-xs text-foreground" > { meta . geo . trim ( ) } < / span >
< Button variant = "outline" size = "sm" className = "h-8 shrink-0" asChild >
< a
< a href = { nip52GeohashSoftengUrl ( meta . geo ) } target = "_blank" rel = "noopener noreferrer" >
href = { nip52GeohashSoftengUrl ( meta . geo ) }
< ExternalLink className = "size-3.5" / >
target = "_blank"
{ t ( 'calendarNip52ViewGeohash' ) }
rel = "noopener noreferrer"
< / a >
className = { linkClass }
< / Button >
>
< / p >
< ExternalLink className = "size-3 shrink-0 opacity-80" aria - hidden / >
{ t ( 'calendarNip52ViewGeohash' ) }
< / a >
< / div >
< / div >
< / div >
) : null }
) : null }
< / div >
< / div >
)
)
}
}
const hasDayD = ! isDateBased && extras . dayGranularities . length > 0
const hasDayD = ! isDateBased && dayGranularitySummary . length > 0
const hasInclusions = extras . calendarInclusions . length > 0
const hasInclusions = extras . calendarInclusions . length > 0
const hasR = extras . rTags . length > 0
const hasR = extras . rTags . length > 0
const hasD = ! ! meta . d ? . trim ( )
const hasUnknown = extras . unknownTags . length > 0
const hasUnknown = extras . unknownTags . length > 0
if ( ! hasDayD && ! hasInclusions && ! hasR && ! hasD && ! has Unknown ) return null
if ( ! hasDayD && ! hasInclusions && ! hasR && ! hasUnknown ) return null
return (
return (
< div className = "min-w-0 space-y-4 border-t border-border/50 pt-3 " >
< div className = "min-w-0 space-y-2.5 border-t border-border/50 pt-2 " >
{ hasDayD ? (
{ hasDayD ? (
< div className = "space-y-2 " >
< div className = "space-y-1 " >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
< div className = "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ t ( 'calendarNip52DayIndices' ) }
{ t ( 'calendarNip52DayIndices' ) }
< / div >
< / div >
< div className = "flex flex-wrap gap-1.5" >
< p className = "text-sm font-medium leading-snug text-foreground" > { dayGranularitySummary } < / p >
{ extras . dayGranularities . map ( ( d ) = > (
< p className = "text-[10px] leading-snug text-muted-foreground" > { t ( 'calendarNip52DayIndicesHint' ) } < / p >
< span
key = { d }
className = "inline-flex items-center rounded-md bg-muted/80 px-2 py-1 font-mono text-xs font-medium text-foreground ring-1 ring-border/50"
>
D = { d }
< / span >
) ) }
< / div >
< / div >
< / div >
) : null }
) : null }
{ hasInclusions ? (
{ hasInclusions ? (
< div className = "space-y-2 " >
< div className = "space-y-1" >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
< div className = "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ t ( 'calendarNip52CalendarInclusion' ) }
{ t ( 'calendarNip52CalendarInclusion' ) }
< / div >
< / div >
< ul className = "min-w-0 space-y-1.5 " >
< ul className = "min-w-0 space-y-1" >
{ extras . calendarInclusions . map ( ( row ) = > (
{ extras . calendarInclusions . map ( ( row ) = > (
< li key = { row . coordinate } >
< li key = { row . coordinate } >
< B utton
< b utton
type = "button"
type = "button"
variant = "secondary"
size = "sm"
className = "h-auto min-h-9 w-full max-w-full justify-start gap-2 whitespace-normal px-3 py-2 text-left font-normal"
onClick = { ( ) = > navigateToNote ( row . naddr ) }
onClick = { ( ) = > navigateToNote ( row . naddr ) }
className = { cn (
'flex w-full min-w-0 items-center gap-1.5 rounded-md border border-transparent py-0.5 text-left' ,
'text-xs text-primary hover:border-border/60 hover:bg-muted/40'
) }
>
>
< Calendar className = "size-4 shrink-0 text-primary" aria - hidden / >
< Calendar className = "size-3.5 shrink-0 opacity-90 " aria - hidden / >
< span className = "min-w-0 break-all font-mono text-xs leading-snug" > { row . naddr } < / span >
< span className = "min-w-0 break-all font-mono leading-snug" > { row . naddr } < / span >
< / Button >
< / b utton>
< / li >
< / li >
) ) }
) ) }
< / ul >
< / ul >
< p className = "text-[11 px] leading-snug text-muted-foreground" > { t ( 'calendarNip52CalendarInclusionHint' ) } < / p >
< p className = "text-[10 px] leading-snug text-muted-foreground" > { t ( 'calendarNip52CalendarInclusionHint' ) } < / p >
< / div >
< / div >
) : null }
) : null }
{ hasR ? (
{ hasR ? (
< div className = "space-y-2 " >
< div className = "space-y-1 " >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
< div className = "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ t ( 'calendarNip52References' ) }
{ t ( 'calendarNip52References' ) }
< / div >
< / div >
< ul className = "min-w-0 space-y-2 " >
< ul className = "min-w-0 space-y-1 " >
{ extras . rTags . map ( ( r , idx ) = > (
{ extras . rTags . map ( ( r , idx ) = > (
< li key = { ` ${ r . value } - ${ idx } ` } className = "min-w-0" >
< li key = { ` ${ r . value } - ${ idx } ` } className = "min-w-0" >
{ r . isHttpUrl ? (
{ r . isHttpUrl ? (
< Button variant = "outline" size = "sm" className = "h-auto min-h-9 w-full max-w-full justify-start gap-2 px-3 py-2" asChild >
< a
< a href = { r . value } target = "_blank" rel = "noopener noreferrer" >
href = { r . value }
< Link2 className = "size-4 shrink-0" aria - hidden / >
target = "_blank"
< span className = "min-w-0 break-all text-left text-xs font-medium" > { r . value } < / span >
rel = "noopener noreferrer"
< / a >
className = "inline-flex min-w-0 max-w-full items-start gap-1 break-all text-xs font-medium text-primary underline-offset-2 hover:underline"
< / Button >
>
< Link2 className = "mt-0.5 size-3 shrink-0 opacity-80" aria - hidden / >
< span > { r . value } < / span >
< / a >
) : (
) : (
< div className = "rounded-lg border border-border/60 bg-muted/25 px-3 py-2" >
< div className = "rounded-md border border-border/50 bg-muted/20 px-2 py-1 " >
< p className = "flex items-start gap-2 text-xs text-muted-foreground" >
< p className = "flex items-start gap-1.5 text-[11px] text-muted-foreground" >
< Link2 className = "mt-0.5 size-3.5 shrink-0" aria - hidden / >
< Link2 className = "mt-0.5 size-3 shrink-0" aria - hidden / >
< span className = "min-w-0 break-all font-mono text-foreground" > { r . value } < / span >
< span className = "min-w-0 break-all font-mono text-foreground" > { r . value } < / span >
< / p >
< / p >
< / div >
< / div >
@ -196,27 +202,18 @@ export function CalendarEventNip52StructuredMeta({
< / div >
< / div >
) : null }
) : null }
{ hasD ? (
< div className = "space-y-1" >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
{ t ( 'calendarNip52Identifier' ) }
< / div >
< p className = "break-all font-mono text-xs text-foreground" > { meta . d . trim ( ) } < / p >
< / div >
) : null }
{ hasUnknown ? (
{ hasUnknown ? (
< div className = "space-y-2 " >
< div className = "space-y-1" >
< div className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
< div className = "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ t ( 'calendarNip52OtherTags' ) }
{ t ( 'calendarNip52OtherTags' ) }
< / div >
< / div >
< dl className = "min-w-0 space-y-2 rounded-lg border border-border/50 bg-muted/15 p-3 " >
< dl className = "min-w-0 space-y-1.5 rounded-md border border-border/50 bg-muted/15 p-2" >
{ extras . unknownTags . map ( ( tag , idx ) = > (
{ extras . unknownTags . map ( ( tag , idx ) = > (
< div key = { ` ${ tag [ 0 ] } - ${ idx } ` } className = "grid gap-1 sm:grid-cols-[minmax(0,6 rem)_1fr] sm:gap-2" >
< div key = { ` ${ tag [ 0 ] } - ${ idx } ` } className = "grid gap-0.5 sm:grid-cols-[minmax(0,5.5rem)_1fr] sm:gap-2" >
< dt className = "font-mono text-[10px] font-semibold uppercase tracking-wide text-muted-foreground" >
< dt className = "font-mono text-[10px] font-semibold uppercase tracking-wide text-muted-foreground" >
{ tag [ 0 ] || '—' }
{ tag [ 0 ] || '—' }
< / dt >
< / dt >
< dd className = "min-w-0 break-all text-xs text-foreground" >
< dd className = "min-w-0 break-all text-[11px] text-foreground" >
{ tag . length > 1 ? tag . slice ( 1 ) . join ( ' · ' ) : '—' }
{ tag . length > 1 ? tag . slice ( 1 ) . join ( ' · ' ) : '—' }
< / dd >
< / dd >
< / div >
< / div >