diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx new file mode 100644 index 0000000..38e1f38 --- /dev/null +++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx @@ -0,0 +1,245 @@ +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' +import { Badge } from '@/components/ui/badge' +import { Hash, X, Users, Code, DollarSign, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt } from 'lucide-react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useNostr } from '@/providers/NostrProvider' +import { TDraftEvent } from '@/types' +import { cn } from '@/lib/utils' +import dayjs from 'dayjs' + +interface CreateThreadDialogProps { + topic: string + availableRelays: string[] + onClose: () => void + onThreadCreated: () => void +} + +export const DISCUSSION_TOPICS = [ + { id: 'general', label: 'General', icon: Hash, color: 'bg-gray-100 text-gray-800' }, + { id: 'meetups', label: 'Meetups', icon: Users, color: 'bg-blue-100 text-blue-800' }, + { id: 'devs', label: 'Developers', icon: Code, color: 'bg-green-100 text-green-800' }, + { id: 'finance', label: 'Bitcoin, Finance & Economics', icon: DollarSign, color: 'bg-yellow-100 text-yellow-800' }, + { id: 'politics', label: 'Politics & Breaking News', icon: Newspaper, color: 'bg-red-100 text-red-800' }, + { id: 'literature', label: 'Literature & Art', icon: BookOpen, color: 'bg-purple-100 text-purple-800' }, + { id: 'philosophy', label: 'Philosophy & Theology', icon: Scroll, color: 'bg-indigo-100 text-indigo-800' }, + { id: 'tech', label: 'Technology & Science', icon: Cpu, color: 'bg-cyan-100 text-cyan-800' }, + { id: 'sports', label: 'Sports and Gaming', icon: Trophy, color: 'bg-orange-100 text-orange-800' }, + { id: 'entertainment', label: 'Entertainment & Pop Culture', icon: Film, color: 'bg-pink-100 text-pink-800' }, + { id: 'health', label: 'Health & Wellness', icon: Heart, color: 'bg-red-100 text-red-800' }, + { id: 'lifestyle', label: 'Lifestyle & Personal Development', icon: TrendingUp, color: 'bg-emerald-100 text-emerald-800' }, + { id: 'food', label: 'Food & Cooking', icon: Utensils, color: 'bg-amber-100 text-amber-800' }, + { id: 'travel', label: 'Travel & Adventure', icon: MapPin, color: 'bg-teal-100 text-teal-800' }, + { id: 'home', label: 'Home & Garden', icon: Home, color: 'bg-lime-100 text-lime-800' }, + { id: 'pets', label: 'Pets & Animals', icon: PawPrint, color: 'bg-rose-100 text-rose-800' }, + { id: 'fashion', label: 'Fashion & Beauty', icon: Shirt, color: 'bg-violet-100 text-violet-800' } +] + +export default function CreateThreadDialog({ + topic: initialTopic, + availableRelays, + onClose, + onThreadCreated +}: CreateThreadDialogProps) { + const { t } = useTranslation() + const { pubkey, publish } = useNostr() + const [title, setTitle] = useState('') + const [content, setContent] = useState('') + const [selectedTopic] = useState(initialTopic) + const [selectedRelay, setSelectedRelay] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [errors, setErrors] = useState<{ title?: string; content?: string; relay?: string }>({}) + + const validateForm = () => { + const newErrors: { title?: string; content?: string; relay?: string } = {} + + if (!title.trim()) { + newErrors.title = t('Title is required') + } else if (title.length > 100) { + newErrors.title = t('Title must be 100 characters or less') + } + + if (!content.trim()) { + newErrors.content = t('Content is required') + } else if (content.length > 5000) { + newErrors.content = t('Content must be 5000 characters or less') + } + + if (!selectedRelay) { + newErrors.relay = t('Please select a relay') + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!pubkey) { + alert(t('You must be logged in to create a thread')) + return + } + + if (!validateForm()) { + return + } + + setIsSubmitting(true) + + try { + // Create the thread event (kind 11) + const threadEvent: TDraftEvent = { + kind: 11, + content: content.trim(), + tags: [ + ['title', title.trim()], + ['t', selectedTopic], + ['-'] // Required tag for relay privacy + ], + created_at: dayjs().unix() + } + + // Publish to the selected relay only + const publishedEvent = await publish(threadEvent, { + specifiedRelayUrls: [selectedRelay] + }) + + if (publishedEvent) { + onThreadCreated() + onClose() + } else { + throw new Error(t('Failed to publish thread')) + } + } catch (error) { + console.error('Error creating thread:', error) + alert(t('Failed to create thread. Please try again.')) + } finally { + setIsSubmitting(false) + } + } + + const selectedTopicInfo = DISCUSSION_TOPICS.find(t => t.id === selectedTopic) || DISCUSSION_TOPICS[0] + + return ( +
+ + + {t('Create New Thread')} + + + + +
+ {/* Topic Selection */} +
+ +
+ + + {selectedTopicInfo.label} + +
+

+ {t('Threads are organized by topics. You can change this after creation.')} +

+
+ + {/* Title Input */} +
+ + setTitle(e.target.value)} + placeholder={t('Enter a descriptive title for your thread')} + maxLength={100} + className={errors.title ? 'border-destructive' : ''} + /> + {errors.title && ( +

{errors.title}

+ )} +

+ {title.length}/100 {t('characters')} +

+
+ + {/* Content Input */} +
+ +