import { ExtendedKind } from '@/constants' import { Event } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { ExternalLink } from 'lucide-react' import { nip19 } from 'nostr-tools' function getTagValue(event: Event, tagName: string): string { return event.tags.find(tag => tag[0] === tagName)?.[1] || '' } interface CitationCardProps { event: Event className?: string displayType?: 'end' | 'foot' | 'foot-end' | 'inline' | 'quote' | 'prompt-end' | 'prompt-inline' citationId?: string // The original citation ID (nevent/naddr) for Nostr references } export default function CitationCard({ event, className, displayType = 'end', citationId }: CitationCardProps) { const { t } = useTranslation() const citationData = useMemo(() => { const title = getTagValue(event, 'title') || '' const author = getTagValue(event, 'author') || '' const publishedOn = getTagValue(event, 'published_on') || '' const accessedOn = getTagValue(event, 'accessed_on') || '' const summary = getTagValue(event, 'summary') || '' const location = getTagValue(event, 'location') || '' const publishedBy = getTagValue(event, 'published_by') || '' const version = getTagValue(event, 'version') || '' if (event.kind === ExtendedKind.CITATION_INTERNAL) { const cTag = event.tags.find(tag => tag[0] === 'c')?.[1] || '' const relayHint = event.tags.find(tag => tag[0] === 'c')?.[2] || '' const geohash = getTagValue(event, 'g') || '' return { type: 'internal', title, author, publishedOn, accessedOn, summary, location, geohash, cTag, relayHint } } else if (event.kind === ExtendedKind.CITATION_EXTERNAL) { const url = getTagValue(event, 'u') || '' const openTimestamp = getTagValue(event, 'open_timestamp') || '' const geohash = getTagValue(event, 'g') || '' return { type: 'external', title, author, url, publishedOn, publishedBy, version, accessedOn, summary, location, geohash, openTimestamp } } else if (event.kind === ExtendedKind.CITATION_HARDCOPY) { const pageRange = getTagValue(event, 'page_range') || '' const chapterTitle = getTagValue(event, 'chapter_title') || '' const editor = getTagValue(event, 'editor') || '' const publishedIn = event.tags.find(tag => tag[0] === 'published_in')?.[1] || '' const volume = event.tags.find(tag => tag[0] === 'published_in')?.[2] || '' const doi = getTagValue(event, 'doi') || '' const geohash = getTagValue(event, 'g') || '' return { type: 'hardcopy', title, author, pageRange, chapterTitle, editor, publishedOn, publishedBy, publishedIn, volume, doi, version, accessedOn, summary, location, geohash } } else if (event.kind === ExtendedKind.CITATION_PROMPT) { const llm = getTagValue(event, 'llm') || '' const url = getTagValue(event, 'u') || '' return { type: 'prompt', llm, accessedOn, version, summary, url } } return null }, [event]) if (!citationData) { return null } const formatDate = (dateStr: string) => { if (!dateStr) return '' try { const date = new Date(dateStr) return date.toLocaleDateString() } catch { return dateStr } } const formatYear = (dateStr: string) => { if (!dateStr) return '' try { const date = new Date(dateStr) if (!isNaN(date.getTime())) { return date.getFullYear().toString() } } catch { // Fall through to regex extraction } // Try to extract year from string (YYYY format) const yearMatch = dateStr.match(/\b(19|20)\d{2}\b/) return yearMatch ? yearMatch[0] : '' } // Format citation in academic style (NKBIP-03 format for Nostr references) const formatAcademicCitation = () => { if (citationData.type === 'internal') { // NKBIP-03 format: [author]. Nostr: "[title]". [published on].\nnostr:[npub]\nnostr:[event identifier] const parts: string[] = [] // Author if (citationData.author) { parts.push(citationData.author + '.') } // Nostr: "[title]" if (citationData.title) { parts.push(`Nostr: "${citationData.title}".`) } else if (citationData.summary) { // Use summary if no title const summaryText = citationData.summary.length > 100 ? citationData.summary.substring(0, 100) + '...' : citationData.summary parts.push(`Nostr: "${summaryText}".`) } else { parts.push('Nostr:') } // Published on date if (citationData.publishedOn) { const dateStr = formatDate(citationData.publishedOn) if (dateStr) { parts.push(dateStr + '.') } } // Nostr addresses on separate lines (NKBIP-03 format) const nostrLines: string[] = [] // Extract npub from cTag (format: kind:pubkey:hex) if (citationData.cTag) { const cTagParts = citationData.cTag.split(':') if (cTagParts.length >= 2) { const pubkeyHex = cTagParts[1] // Convert hex pubkey to npub if (pubkeyHex && /^[0-9a-f]{64}$/i.test(pubkeyHex)) { try { const npub = nip19.npubEncode(pubkeyHex) nostrLines.push(`nostr:${npub}`) } catch (error) { // If encoding fails, skip npub line } } else if (pubkeyHex.startsWith('npub') || pubkeyHex.startsWith('nprofile')) { // Already a bech32 address nostrLines.push(`nostr:${pubkeyHex}`) } } else if (citationData.cTag.startsWith('npub') || citationData.cTag.startsWith('nprofile')) { // cTag is already a bech32 address nostrLines.push(`nostr:${citationData.cTag}`) } } // Add citationId (event identifier) if it's a bech32 address (nevent/naddr) if (citationId && (citationId.startsWith('nevent') || citationId.startsWith('naddr') || citationId.startsWith('note'))) { nostrLines.push(`nostr:${citationId}`) } // Join main parts and add nostr addresses on new lines const mainText = parts.join(' ') if (nostrLines.length > 0) { return mainText + '\n' + nostrLines.join('\n') } return mainText } else if (citationData.type === 'external') { // APA format: Author. (Year). Title. Publisher. URL const parts: string[] = [] if (citationData.author) { parts.push(citationData.author) } const year = formatYear(citationData.publishedOn || '') if (year) { parts.push(`(${year})`) } if (citationData.title) { parts.push(citationData.title + '.') } if (citationData.publishedBy) { parts.push(citationData.publishedBy + '.') } if (citationData.url) { parts.push(citationData.url) } const accessedYear = formatYear(citationData.accessedOn) if (accessedYear && accessedYear !== year) { parts.push(`Retrieved ${formatDate(citationData.accessedOn)}`) } return parts.join(' ') } else if (citationData.type === 'hardcopy') { // APA format: Author. (Year). Title. In Editor (Ed.), Published In (Vol. X, pp. Y-Z). Publisher. const parts: string[] = [] if (citationData.author) { parts.push(citationData.author) } const year = formatYear(citationData.publishedOn || '') if (year) { parts.push(`(${year})`) } if (citationData.chapterTitle) { parts.push(citationData.chapterTitle + '.') if (citationData.editor) { parts.push(`In ${citationData.editor} (Ed.),`) } } else if (citationData.title) { parts.push(citationData.title + '.') } if (citationData.publishedIn) { const publishedInText = citationData.volume ? `${citationData.publishedIn} (Vol. ${citationData.volume})` : citationData.publishedIn parts.push(publishedInText + '.') } if (citationData.pageRange) { parts.push(`pp. ${citationData.pageRange}.`) } if (citationData.publishedBy) { parts.push(citationData.publishedBy + '.') } if (citationData.doi) { parts.push(`https://doi.org/${citationData.doi}`) } return parts.join(' ') } else if (citationData.type === 'prompt') { // APA format for AI: LLM. (Year). [Prompt interaction]. URL const parts: string[] = [] if (citationData.llm) { parts.push(citationData.llm) } const year = formatYear(citationData.accessedOn) if (year) { parts.push(`(${year})`) } parts.push('[Prompt interaction].') if (citationData.url) { parts.push(citationData.url) } return parts.join(' ') } return '' } const renderCitationContent = () => { if (citationData.type === 'internal') { // NKBIP-03 format: [author]. Nostr: "[title]". [published on].\nnostr:[npub]\nnostr:[event identifier] const nostrAddresses: string[] = [] // Extract npub from cTag (format: kind:pubkey:hex) if (citationData.cTag) { const cTagParts = citationData.cTag.split(':') if (cTagParts.length >= 2) { const pubkeyHex = cTagParts[1] // Convert hex pubkey to npub if (pubkeyHex && /^[0-9a-f]{64}$/i.test(pubkeyHex)) { try { const npub = nip19.npubEncode(pubkeyHex) nostrAddresses.push(`nostr:${npub}`) } catch (error) { // If encoding fails, skip npub line } } else if (pubkeyHex.startsWith('npub') || pubkeyHex.startsWith('nprofile')) { // Already a bech32 address nostrAddresses.push(`nostr:${pubkeyHex}`) } } else if (citationData.cTag.startsWith('npub') || citationData.cTag.startsWith('nprofile')) { // cTag is already a bech32 address nostrAddresses.push(`nostr:${citationData.cTag}`) } } // Add citationId (event identifier) if it's a bech32 address (nevent/naddr) if (citationId && (citationId.startsWith('nevent') || citationId.startsWith('naddr') || citationId.startsWith('note'))) { nostrAddresses.push(`nostr:${citationId}`) } return (
) } // Default: render in academic format const academicText = formatAcademicCitation() return ({renderCitationContent()}