Browse Source

drop-down topic list

imwald
Silberengel 5 months ago
parent
commit
e88865659f
  1. 245
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  2. 22
      src/pages/primary/DiscussionsPage/ThreadCard.tsx
  3. 2
      src/pages/primary/DiscussionsPage/TopicFilter.tsx
  4. 45
      src/pages/primary/DiscussionsPage/index.tsx

245
src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx

@ -0,0 +1,245 @@ @@ -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<string>('')
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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-xl font-semibold">{t('Create New Thread')}</CardTitle>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="h-8 w-8"
>
<X className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Topic Selection */}
<div className="space-y-2">
<Label htmlFor="topic">{t('Topic')}</Label>
<div className="flex items-center gap-2">
<selectedTopicInfo.icon className="w-4 h-4" />
<Badge variant="secondary" className={cn('text-sm', selectedTopicInfo.color)}>
{selectedTopicInfo.label}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{t('Threads are organized by topics. You can change this after creation.')}
</p>
</div>
{/* Title Input */}
<div className="space-y-2">
<Label htmlFor="title">{t('Thread Title')}</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t('Enter a descriptive title for your thread')}
maxLength={100}
className={errors.title ? 'border-destructive' : ''}
/>
{errors.title && (
<p className="text-sm text-destructive">{errors.title}</p>
)}
<p className="text-sm text-muted-foreground">
{title.length}/100 {t('characters')}
</p>
</div>
{/* Content Input */}
<div className="space-y-2">
<Label htmlFor="content">{t('Thread Content')}</Label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder={t('Share your thoughts, ask questions, or start a discussion...')}
rows={8}
maxLength={5000}
className={errors.content ? 'border-destructive' : ''}
/>
{errors.content && (
<p className="text-sm text-destructive">{errors.content}</p>
)}
<p className="text-sm text-muted-foreground">
{content.length}/5000 {t('characters')}
</p>
</div>
{/* Relay Selection */}
<div className="space-y-2">
<Label htmlFor="relay">{t('Publish to Relay')}</Label>
<Select value={selectedRelay} onValueChange={setSelectedRelay}>
<SelectTrigger className={errors.relay ? 'border-destructive' : ''}>
<SelectValue placeholder={t('Select a relay to publish to')} />
</SelectTrigger>
<SelectContent>
{availableRelays.map(relay => (
<SelectItem key={relay} value={relay}>
{relay.replace('wss://', '').replace('ws://', '')}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.relay && (
<p className="text-sm text-destructive">{errors.relay}</p>
)}
<p className="text-sm text-muted-foreground">
{t('Choose the relay where this discussion will be hosted.')}
</p>
</div>
{/* Form Actions */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onClose}
className="flex-1"
>
{t('Cancel')}
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="flex-1"
>
{isSubmitting ? t('Creating...') : t('Create Thread')}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}

22
src/pages/primary/DiscussionsPage/ThreadCard.tsx

@ -7,6 +7,7 @@ import { formatDistanceToNow } from 'date-fns' @@ -7,6 +7,7 @@ import { formatDistanceToNow } from 'date-fns'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { truncateText } from '@/lib/utils'
import { DISCUSSION_TOPICS } from './CreateThreadDialog'
interface ThreadCardProps {
thread: NostrEvent
@ -32,20 +33,15 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC @@ -32,20 +33,15 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
const createdAt = new Date(thread.created_at * 1000)
const timeAgo = formatDistanceToNow(createdAt, { addSuffix: true })
// Get topic display info
// Get topic display info from centralized DISCUSSION_TOPICS
const getTopicInfo = (topicId: string) => {
const topicMap: Record<string, { label: string; color: string }> = {
general: { label: 'General', color: 'bg-gray-100 text-gray-800' },
meetups: { label: 'Meetups', color: 'bg-blue-100 text-blue-800' },
devs: { label: 'Developers', color: 'bg-green-100 text-green-800' },
finance: { label: 'Finance', color: 'bg-yellow-100 text-yellow-800' },
politics: { label: 'Politics', color: 'bg-red-100 text-red-800' },
literature: { label: 'Literature', color: 'bg-purple-100 text-purple-800' },
philosophy: { label: 'Philosophy', color: 'bg-indigo-100 text-indigo-800' },
tech: { label: 'Technology', color: 'bg-cyan-100 text-cyan-800' },
sports: { label: 'Sports', color: 'bg-orange-100 text-orange-800' }
const topic = DISCUSSION_TOPICS.find(t => t.id === topicId)
return topic || {
id: topicId,
label: topicId,
icon: Hash,
color: 'bg-gray-100 text-gray-800'
}
return topicMap[topicId] || { label: topicId, color: 'bg-gray-100 text-gray-800' }
}
const topicInfo = getTopicInfo(topic)
@ -66,7 +62,7 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC @@ -66,7 +62,7 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
</h3>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Badge variant="secondary" className={cn('text-xs', topicInfo.color)}>
<Hash className="w-3 h-3 mr-1" />
<topicInfo.icon className="w-3 h-3 mr-1" />
{topicInfo.label}
</Badge>
<div className="flex items-center gap-1">

2
src/pages/primary/DiscussionsPage/TopicFilter.tsx

@ -37,7 +37,7 @@ export default function TopicFilter({ topics, selectedTopic, onTopicChange }: To @@ -37,7 +37,7 @@ export default function TopicFilter({ topics, selectedTopic, onTopicChange }: To
onClick={() => onTopicChange(topic.id)}
className="flex items-center gap-2"
>
<Hash className="w-4 h-4" />
<topic.icon className="w-4 h-4" />
<span>{topic.label}</span>
{topic.id === selectedTopic && (
<span className="ml-auto text-primary"></span>

45
src/pages/primary/DiscussionsPage/index.tsx

@ -1,29 +1,18 @@ @@ -1,29 +1,18 @@
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Card, CardContent } from '@/components/ui/card'
import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { MessageSquarePlus, Hash } from 'lucide-react'
import { MessageSquarePlus } from 'lucide-react'
import ThreadCard from '@/pages/primary/DiscussionsPage/ThreadCard'
import TopicFilter from '@/pages/primary/DiscussionsPage/TopicFilter'
import CreateThreadDialog, { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/CreateThreadDialog'
import { NostrEvent } from 'nostr-tools'
import client from '@/services/client.service'
const DISCUSSION_TOPICS = [
{ id: 'general', label: 'General', icon: Hash },
{ id: 'meetups', label: 'Meetups', icon: Hash },
{ id: 'devs', label: 'Developers', icon: Hash },
{ id: 'finance', label: 'Finance & Economics', icon: Hash },
{ id: 'politics', label: 'Politics & Breaking News', icon: Hash },
{ id: 'literature', label: 'Literature & Art', icon: Hash },
{ id: 'philosophy', label: 'Philosophy & Theology', icon: Hash },
{ id: 'tech', label: 'Technology & Science', icon: Hash },
{ id: 'sports', label: 'Sports', icon: Hash }
]
const DiscussionsPage = forwardRef((_, ref) => {
const { t } = useTranslation()
const { favoriteRelays } = useFavoriteRelays()
@ -180,31 +169,3 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -180,31 +169,3 @@ const DiscussionsPage = forwardRef((_, ref) => {
DiscussionsPage.displayName = 'DiscussionsPage'
export default DiscussionsPage
// Placeholder components - to be implemented
function CreateThreadDialog({
onClose,
onThreadCreated
}: {
topic: string
availableRelays: string[]
onClose: () => void
onThreadCreated: () => void
}) {
// TODO: Implement thread creation dialog
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-md mx-4">
<CardHeader>
<CardTitle>Create New Thread</CardTitle>
</CardHeader>
<CardContent>
<p>Thread creation will be implemented here...</p>
<div className="flex gap-2 mt-4">
<Button onClick={onClose} variant="outline">Cancel</Button>
<Button onClick={onThreadCreated}>Create</Button>
</div>
</CardContent>
</Card>
</div>
)
}

Loading…
Cancel
Save