import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { X, ShieldAlert } from "lucide-react"; import { updateAnnotation, updateHighlight, updateBookmark, sessionAtom, getUserTags, getTrendingTags, } from "../../api/client"; import type { AnnotationItem, ContentLabelValue } from "../../types"; import TagInput from "../ui/TagInput"; const SELF_LABEL_VALUES: ContentLabelValue[] = [ "sexual", "nudity", "violence", "gore", "spam", "misleading", ]; const HIGHLIGHT_COLORS = [ { value: "yellow", bg: "bg-yellow-400", ring: "ring-yellow-500" }, { value: "green", bg: "bg-green-400", ring: "ring-green-500" }, { value: "blue", bg: "bg-blue-400", ring: "ring-blue-500" }, { value: "red", bg: "bg-red-400", ring: "ring-red-500" }, ]; interface EditItemModalProps { isOpen: boolean; onClose: () => void; item: AnnotationItem; type: "annotation" | "highlight" | "bookmark"; onSaved?: (item: AnnotationItem) => void; } export default function EditItemModal({ isOpen, onClose, item, type, onSaved, }: EditItemModalProps) { if (!isOpen) return null; return ( ); } function EditItemModalContent({ item, type, onClose, onSaved, }: Omit) { const { t } = useTranslation(); const [text, setText] = useState(item.body?.value || ""); const [tags, setTags] = useState(item.tags || []); const [tagSuggestions, setTagSuggestions] = useState([]); const [color, setColor] = useState(item.color || "yellow"); const [title, setTitle] = useState(item.title || item.target?.title || ""); const [description, setDescription] = useState(item.description || ""); const existingLabels = (item.labels || []) .filter((l) => l.src === item.author?.did) .map((l) => l.val as ContentLabelValue); const [selfLabels, setSelfLabels] = useState(existingLabels); const [showLabelPicker, setShowLabelPicker] = useState( existingLabels.length > 0, ); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); useEffect(() => { const session = sessionAtom.get(); if (session?.did) { Promise.all([ getUserTags(session.did).catch(() => [] as string[]), getTrendingTags(50) .then((tags) => tags.map((t) => t.tag)) .catch(() => [] as string[]), ]).then(([userTags, trendingTags]) => { const seen = new Set(userTags); const merged = [...userTags]; for (const t of trendingTags) { if (!seen.has(t)) { merged.push(t); seen.add(t); } } setTagSuggestions(merged); }); } }, []); const toggleLabel = (val: ContentLabelValue) => { setSelfLabels((prev) => prev.includes(val) ? prev.filter((l) => l !== val) : [...prev, val], ); }; const handleSave = async () => { setSaving(true); setError(null); let success = false; const labels = selfLabels.length > 0 ? selfLabels : []; try { if (type === "annotation") { success = await updateAnnotation( item.uri, text, tags.length > 0 ? tags : undefined, labels, ); } else if (type === "highlight") { success = await updateHighlight( item.uri, color, tags.length > 0 ? tags : undefined, labels, ); } else if (type === "bookmark") { success = await updateBookmark( item.uri, title || undefined, description || undefined, tags.length > 0 ? tags : undefined, labels, ); } } catch (e) { console.error("Edit save error:", e); setError(e instanceof Error ? e.message : t("editItem.failedSave")); setSaving(false); return; } setSaving(false); if (!success) { setError(t("editItem.failedSave")); return; } const updated = { ...item }; if (type === "annotation") { updated.body = { type: "TextualBody", value: text, format: "text/plain" }; } else if (type === "highlight") { updated.color = color; } else if (type === "bookmark") { updated.title = title; updated.description = description; } updated.tags = tags; const otherLabels = (item.labels || []).filter( (l) => l.src !== item.author?.did, ); const newSelfLabels = selfLabels.map((val) => ({ val, src: item.author?.did || "", scope: "content" as const, })); updated.labels = [...otherLabels, ...newSelfLabels]; onSaved?.(updated); onClose(); }; return ( e.stopPropagation()} > {type === "annotation" ? t("editItem.editAnnotation") : type === "highlight" ? t("editItem.editHighlight") : t("editItem.editBookmark")} {type === "annotation" && ( {t("editItem.textLabel")} setText(e.target.value)} rows={4} maxLength={3000} className="w-full px-3 py-2 rounded-xl border border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 resize-none" placeholder={t("editItem.textPlaceholder")} /> {text.length}/3000 )} {type === "highlight" && ( {t("editItem.colorLabel")} {HIGHLIGHT_COLORS.map((c) => ( setColor(c.value)} className={`w-8 h-8 rounded-full ${c.bg} transition-all ${ color === c.value ? `ring-2 ${c.ring} ring-offset-2 dark:ring-offset-surface-900 scale-110` : "opacity-60 hover:opacity-100" }`} title={c.value} /> ))} {item.target?.selector?.exact && ( {item.target.selector.exact} )} )} {type === "bookmark" && ( <> {t("editItem.titleLabel")} setTitle(e.target.value)} className="w-full px-3 py-2 rounded-xl border border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder={t("editItem.titlePlaceholder")} /> {t("editItem.descriptionLabel")} setDescription(e.target.value)} rows={3} className="w-full px-3 py-2 rounded-xl border border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 resize-none" placeholder={t("editItem.descriptionPlaceholder")} /> > )} {t("editItem.tagsLabel")} setShowLabelPicker(!showLabelPicker)} className={`flex items-center gap-2 text-sm font-medium transition-colors ${ showLabelPicker || selfLabels.length > 0 ? "text-amber-600 dark:text-amber-400" : "text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200" }`} > {t("editItem.contentWarning")} {selfLabels.length > 0 && ( {selfLabels.length} )} {showLabelPicker && ( {SELF_LABEL_VALUES.map((val) => ( toggleLabel(val)} className={`px-3 py-1 rounded-full text-xs font-medium border transition-all ${ selfLabels.includes(val) ? "bg-amber-100 dark:bg-amber-900/40 border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-200" : "bg-surface-50 dark:bg-surface-800 border-surface-200 dark:border-surface-700 text-surface-600 dark:text-surface-400 hover:border-amber-300 dark:hover:border-amber-700" }`} > {t(`composer.labels.${val}`)} ))} )} {error && {error}} {t("editItem.cancel")} {saving ? t("editItem.saving") : t("editItem.save")} ); }
{text.length}/3000
{item.target.selector.exact}
{error}