From 325210886ba3ef7b1b1bc693d6dcc6b091f486fa Mon Sep 17 00:00:00 2001 From: mroxso <24775431+mroxso@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:51:00 +0100 Subject: [PATCH] NIP-51: Interest Set (#39) * feat: implement Interest Sets management and display in settings and homepage * Update src/pages/HomePage.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/hooks/useInterestSets.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix whitespace preservation in Interest Set identifier publishing (#40) * Initial plan * fix: trim identifier before publishing interest set Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> * Update src/components/InterestSetsManager.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: highperfocused Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- src/components/InterestSetsManager.tsx | 358 +++++++++++++++++++++++++ src/components/LatestInHashtag.tsx | 5 +- src/hooks/useDeleteInterestSet.ts | 51 ++++ src/hooks/useInterestSets.ts | 74 +++++ src/hooks/usePublishInterestSet.ts | 75 ++++++ src/pages/HomePage.tsx | 72 ++--- src/pages/SettingsPage.tsx | 19 +- 7 files changed, 621 insertions(+), 33 deletions(-) create mode 100644 src/components/InterestSetsManager.tsx create mode 100644 src/hooks/useDeleteInterestSet.ts create mode 100644 src/hooks/useInterestSets.ts create mode 100644 src/hooks/usePublishInterestSet.ts diff --git a/src/components/InterestSetsManager.tsx b/src/components/InterestSetsManager.tsx new file mode 100644 index 0000000..f98b297 --- /dev/null +++ b/src/components/InterestSetsManager.tsx @@ -0,0 +1,358 @@ +import { useState } from 'react'; +import { Plus, Trash2, Edit2, Hash, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useInterestSets, type InterestSet } from '@/hooks/useInterestSets'; +import { usePublishInterestSet } from '@/hooks/usePublishInterestSet'; +import { useDeleteInterestSet } from '@/hooks/useDeleteInterestSet'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; + +interface InterestSetFormData { + identifier: string; + title: string; + image: string; + description: string; + hashtags: string[]; +} + +const defaultFormData: InterestSetFormData = { + identifier: '', + title: '', + image: '', + description: '', + hashtags: [], +}; + +function InterestSetDialog({ + interestSet, + trigger, + onSuccess +}: { + interestSet?: InterestSet; + trigger: React.ReactNode; + onSuccess?: () => void; +}) { + const [open, setOpen] = useState(false); + const [formData, setFormData] = useState( + interestSet + ? { + identifier: interestSet.identifier, + title: interestSet.title || '', + image: interestSet.image || '', + description: interestSet.description || '', + hashtags: interestSet.hashtags, + } + : defaultFormData + ); + const [hashtagInput, setHashtagInput] = useState(''); + + const { mutate: publishInterestSet, isPending } = usePublishInterestSet(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.identifier.trim()) { + return; + } + + publishInterestSet( + { + identifier: formData.identifier.trim(), + title: formData.title.trim() || undefined, + image: formData.image.trim() || undefined, + description: formData.description.trim() || undefined, + hashtags: formData.hashtags, + }, + { + onSuccess: () => { + setOpen(false); + setFormData(defaultFormData); + setHashtagInput(''); + onSuccess?.(); + }, + } + ); + }; + + const handleAddHashtag = () => { + const tag = hashtagInput.trim().toLowerCase().replace(/^#/, ''); + if (tag && !formData.hashtags.includes(tag)) { + setFormData({ ...formData, hashtags: [...formData.hashtags, tag] }); + setHashtagInput(''); + } + }; + + const handleRemoveHashtag = (hashtag: string) => { + setFormData({ + ...formData, + hashtags: formData.hashtags.filter((h) => h !== hashtag), + }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddHashtag(); + } + }; + + return ( + + {trigger} + + + + {interestSet ? 'Edit Interest Set' : 'Create Interest Set'} + + + Group hashtags together to organize your interests. These will appear as sections on your homepage. + + +
+
+ + + setFormData({ ...formData, identifier: e.target.value }) + } + placeholder="e.g., tech, entertainment" + required + disabled={!!interestSet} + /> +

+ A unique identifier for this set. Cannot be changed after creation. +

+
+ +
+ + setFormData({ ...formData, title: e.target.value })} + placeholder="e.g., Technology & Innovation" + /> +
+ +
+ + setFormData({ ...formData, image: e.target.value })} + placeholder="https://example.com/image.jpg" + type="url" + /> +
+ +
+ +