You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
172 lines
5.8 KiB
172 lines
5.8 KiB
import { Button } from '@/components/ui/button' |
|
import { Input } from '@/components/ui/input' |
|
import { Label } from '@/components/ui/label' |
|
import { Textarea } from '@/components/ui/textarea' |
|
import { cleanUrl } from '@/lib/url' |
|
import { X } from 'lucide-react' |
|
import { useState, useEffect } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import { nip19 } from 'nostr-tools' |
|
|
|
export interface HighlightData { |
|
sourceType: 'nostr' | 'url' |
|
sourceValue: string // nevent/naddr/note/hex for nostr, https:// URL for url |
|
sourceHexId?: string // converted hex ID for nostr sources |
|
context?: string // the full text/quote that the highlight is from |
|
} |
|
|
|
interface HighlightEditorProps { |
|
highlightData: HighlightData |
|
setHighlightData: (data: HighlightData) => void |
|
setIsHighlight: (value: boolean) => void |
|
} |
|
|
|
export default function HighlightEditor({ |
|
highlightData, |
|
setHighlightData, |
|
setIsHighlight |
|
}: HighlightEditorProps) { |
|
const { t } = useTranslation() |
|
const [sourceInput, setSourceInput] = useState(highlightData.sourceValue) |
|
const [context, setContext] = useState(highlightData.context || '') |
|
const [error, setError] = useState<string>('') |
|
|
|
// Validate and parse the source input |
|
useEffect(() => { |
|
if (!sourceInput.trim()) { |
|
setError('') |
|
return |
|
} |
|
|
|
// Check if it's a URL |
|
if (sourceInput.startsWith('https://') || sourceInput.startsWith('http://')) { |
|
// Clean tracking parameters from the URL before publishing |
|
const cleanedUrl = cleanUrl(sourceInput) |
|
setError('') |
|
setHighlightData({ |
|
sourceType: 'url', |
|
sourceValue: cleanedUrl, |
|
context |
|
}) |
|
return |
|
} |
|
|
|
// Try to parse as nostr identifier |
|
try { |
|
let hexId: string | undefined |
|
|
|
// Check if it's already a hex ID (64 char hex string) |
|
if (/^[a-f0-9]{64}$/i.test(sourceInput)) { |
|
hexId = sourceInput.toLowerCase() |
|
setError('') |
|
setHighlightData({ |
|
sourceType: 'nostr', |
|
sourceValue: sourceInput, |
|
sourceHexId: hexId, |
|
context |
|
}) |
|
return |
|
} |
|
|
|
// Try to decode as nip19 identifier |
|
const decoded = nip19.decode(sourceInput) |
|
|
|
if (decoded.type === 'note') { |
|
hexId = decoded.data |
|
setError('') |
|
setHighlightData({ |
|
sourceType: 'nostr', |
|
sourceValue: sourceInput, // Keep original for reference |
|
sourceHexId: hexId, // Store the hex ID |
|
context |
|
}) |
|
} else if (decoded.type === 'nevent') { |
|
hexId = decoded.data.id |
|
setError('') |
|
setHighlightData({ |
|
sourceType: 'nostr', |
|
sourceValue: sourceInput, // Keep the nevent for relay info |
|
sourceHexId: hexId, // Store the hex ID |
|
context |
|
}) |
|
} else if (decoded.type === 'naddr') { |
|
// For naddr, we need to keep the full naddr string to extract kind:pubkey:identifier |
|
setError('') |
|
setHighlightData({ |
|
sourceType: 'nostr', |
|
sourceValue: sourceInput, // Keep the naddr for a-tag building |
|
sourceHexId: undefined, // No hex ID for addressable events |
|
context |
|
}) |
|
} else { |
|
setError(t('Invalid source. Please enter a note ID, nevent, naddr, hex ID, or URL.')) |
|
return |
|
} |
|
} catch { |
|
setError(t('Invalid source. Please enter a note ID, nevent, naddr, hex ID, or URL.')) |
|
} |
|
}, [sourceInput, context, setHighlightData, t]) |
|
|
|
return ( |
|
<div className="rounded-lg border bg-muted/40 p-4 space-y-4"> |
|
<div className="flex items-center justify-between"> |
|
<div className="text-sm font-medium">{t('Highlight Settings')}</div> |
|
<Button |
|
type="button" |
|
variant="ghost" |
|
size="icon" |
|
title={t('Close highlight editor')} |
|
className="h-6 w-6" |
|
onClick={() => setIsHighlight(false)} |
|
> |
|
<X className="h-4 w-4" /> |
|
</Button> |
|
</div> |
|
|
|
<div className="space-y-2"> |
|
<Label htmlFor="highlight-source"> |
|
{t('Source')} <span className="text-destructive">*</span> |
|
</Label> |
|
<Input |
|
id="highlight-source" |
|
value={sourceInput} |
|
onChange={(e) => setSourceInput(e.target.value)} |
|
placeholder={t('nevent1..., naddr1..., note1..., hex ID, or https://...')} |
|
className={error ? 'border-destructive' : ''} |
|
/> |
|
{error && ( |
|
<p className="text-sm text-destructive">{error}</p> |
|
)} |
|
<p className="text-xs text-muted-foreground"> |
|
{t('Enter a Nostr event identifier (nevent, naddr, note, or hex ID) OR a web URL (https://). Not both.')} |
|
</p> |
|
</div> |
|
|
|
<div className="space-y-2"> |
|
<Label htmlFor="highlight-context"> |
|
{t('Full Quote/Context')} <span className="text-muted-foreground text-xs">({t('optional')})</span> |
|
</Label> |
|
<Textarea |
|
id="highlight-context" |
|
value={context} |
|
onChange={(e) => setContext(e.target.value)} |
|
placeholder={t('Paste the entire original passage that contains your highlight')} |
|
rows={12} |
|
/> |
|
<p className="text-xs text-muted-foreground"> |
|
{t('The main editor above should contain only the text you want to highlight. This field should contain the full quote or paragraph for context.')} |
|
</p> |
|
</div> |
|
|
|
<div className="text-xs text-muted-foreground bg-background/50 rounded p-2"> |
|
<p className="font-medium mb-1">{t('How to Create a Highlight (NIP-84)')}</p> |
|
<ol className="list-decimal list-inside space-y-1 mt-2"> |
|
<li>{t('Enter the specific text you want to highlight in the main content area above')}</li> |
|
<li>{t('Add the source (where this text is from)')}</li> |
|
<li>{t('Optionally, add the full quote/context to show your highlight within it')}</li> |
|
</ol> |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
|