From 4697d269bc8a88e76d7cdfdc46861ec57514ec50 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 Apr 2026 19:02:39 -0500 Subject: [PATCH] Put Title and ID side-by-side, auto-slug ID from Title, remove NIP jargon --- src/components/EmojiPackDialog.tsx | 95 +++++++++++++++++++----------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/src/components/EmojiPackDialog.tsx b/src/components/EmojiPackDialog.tsx index 619a73c1..d9e25d9d 100644 --- a/src/components/EmojiPackDialog.tsx +++ b/src/components/EmojiPackDialog.tsx @@ -49,6 +49,16 @@ function isValidShortcode(s: string): boolean { return /^[a-zA-Z0-9_-]+$/.test(s); } +/** Convert a title into a URL-safe slug for the identifier. */ +function slugify(text: string): string { + return text + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') + .replace(/[\s_]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + let entryCounter = 0; function nextEntryId(): string { return `entry-${++entryCounter}-${Date.now()}`; @@ -88,6 +98,7 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial // Form state const [identifier, setIdentifier] = useState(initialData?.identifier ?? ''); const [name, setName] = useState(initialData?.name ?? ''); + const [idTouched, setIdTouched] = useState(isEditMode); const [emojis, setEmojis] = useState(initialData?.emojis ?? []); const [isDragOver, setIsDragOver] = useState(false); const [uploadingCount, setUploadingCount] = useState(0); @@ -99,15 +110,30 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial const isUploading = uploadingCount > 0; + const effectiveIdentifier = idTouched ? identifier : slugify(name); + const resetForm = useCallback(() => { setIdentifier(initialData?.identifier ?? ''); setName(initialData?.name ?? ''); + setIdTouched(isEditMode); setEmojis(initialData?.emojis ?? []); setIsDragOver(false); setUploadingCount(0); setDragIndex(null); setDragOverIndex(null); - }, [initialData]); + }, [initialData, isEditMode]); + + const handleNameChange = useCallback((value: string) => { + setName(value); + if (!idTouched) { + setIdentifier(slugify(value)); + } + }, [idTouched]); + + const handleIdChange = useCallback((value: string) => { + setIdTouched(true); + setIdentifier(value); + }, []); const handleOpenChange = useCallback((nextOpen: boolean) => { if (!nextOpen) resetForm(); @@ -302,7 +328,8 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial /** Publish the emoji pack. */ const handlePublish = useCallback(async () => { - if (!user || !identifier.trim() || emojis.length === 0) return; + const resolvedId = effectiveIdentifier.trim(); + if (!user || !resolvedId || emojis.length === 0) return; // Validate all shortcodes const invalid = emojis.find((e) => !isValidShortcode(e.shortcode)); @@ -336,7 +363,7 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial const fresh = await fetchFreshEvent(nostr, { kinds: [30030], authors: [user.pubkey], - '#d': [identifier.trim()], + '#d': [resolvedId], }); if (fresh) { // Keep tags that aren't d, name, or emoji @@ -347,7 +374,7 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial } const tags: string[][] = [ - ['d', identifier.trim()], + ['d', resolvedId], ...(name.trim() ? [['name', name.trim()]] : []), ...preservedTags, ...emojis.map((e) => ['emoji', e.shortcode, e.url]), @@ -365,7 +392,7 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial toast({ title: isEditMode ? 'Emoji pack updated!' : 'Emoji pack created!', - description: `"${name.trim() || identifier.trim()}" with ${emojis.length} emoji${emojis.length !== 1 ? 's' : ''}.`, + description: `"${name.trim() || resolvedId}" with ${emojis.length} emoji${emojis.length !== 1 ? 's' : ''}.`, }); handleOpenChange(false); @@ -376,11 +403,11 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial variant: 'destructive', }); } - }, [user, identifier, name, emojis, isEditMode, nostr, publishEvent, queryClient, toast, handleOpenChange]); + }, [user, effectiveIdentifier, name, emojis, isEditMode, nostr, publishEvent, queryClient, toast, handleOpenChange]); // Validation const hasValidEmojis = emojis.length > 0 && emojis.every((e) => !e.uploading && e.url && e.shortcode); - const canPublish = identifier.trim() && hasValidEmojis && !isPublishing && !isUploading; + const canPublish = effectiveIdentifier.trim() && hasValidEmojis && !isPublishing && !isUploading; if (!user) return null; @@ -395,39 +422,37 @@ export function EmojiPackDialog({ open, onOpenChange, editEvent }: EmojiPackDial {isEditMode ? 'Update your custom emoji set. Drag and drop images to add more emojis.' - : 'Create a NIP-30 custom emoji set. Drag and drop images or a folder to populate it.'} + : 'Create a custom emoji set. Drag and drop images or a folder to populate it.'}
- {/* Identifier (d-tag) */} -
- - setIdentifier(e.target.value)} - disabled={isEditMode} - className={isEditMode ? 'font-mono text-sm text-muted-foreground' : 'font-mono text-sm'} - /> -

- {isEditMode - ? 'The identifier cannot be changed after creation.' - : 'A unique identifier for this emoji pack. Cannot be changed later.'} -

-
- - {/* Pack name */} -
- - setName(e.target.value)} - /> + {/* Title & ID side-by-side */} +
+
+ + handleNameChange(e.target.value)} + /> +
+
+ + handleIdChange(e.target.value)} + disabled={isEditMode} + className={`font-mono text-sm ${isEditMode ? 'text-muted-foreground' : ''}`} + /> + {isEditMode && ( +

Cannot be changed.

+ )} +
{/* Drop zone for adding emojis */}