Browse Source

readings

imwald
Silberengel 5 months ago
parent
commit
c784ff5cf8
  1. 100
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  2. 132
      src/pages/primary/DiscussionsPage/index.tsx

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

@ -7,7 +7,7 @@ import { Textarea } from '@/components/ui/textarea' @@ -7,7 +7,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { Slider } from '@/components/ui/slider'
import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap, Settings } from 'lucide-react'
import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap, Settings, Book } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
@ -80,8 +80,14 @@ export default function CreateThreadDialog({ @@ -80,8 +80,14 @@ export default function CreateThreadDialog({
const [minPow, setMinPow] = useState(0)
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false)
// Readings options state
const [isReadingGroup, setIsReadingGroup] = useState(false)
const [author, setAuthor] = useState('')
const [subject, setSubject] = useState('')
const [showReadingsPanel, setShowReadingsPanel] = useState(false)
const validateForm = () => {
const newErrors: { title?: string; content?: string; relay?: string } = {}
const newErrors: { title?: string; content?: string; relay?: string; author?: string; subject?: string } = {}
if (!title.trim()) {
newErrors.title = t('Title is required')
@ -99,6 +105,16 @@ export default function CreateThreadDialog({ @@ -99,6 +105,16 @@ export default function CreateThreadDialog({
newErrors.relay = t('Please select a relay')
}
// Validate readings fields if reading group is enabled
if (isReadingGroup) {
if (!author.trim()) {
newErrors.author = t('Author is required for reading groups')
}
if (!subject.trim()) {
newErrors.subject = t('Subject (book title) is required for reading groups')
}
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
@ -128,6 +144,13 @@ export default function CreateThreadDialog({ @@ -128,6 +144,13 @@ export default function CreateThreadDialog({
['-'] // Required tag for relay privacy
]
// Add readings tags if this is a reading group
if (isReadingGroup) {
tags.push(['t', 'readings'])
tags.push(['author', author.trim()])
tags.push(['subject', subject.trim()])
}
// Add image metadata tags if images are found
if (images && images.length > 0) {
tags.push(...generateImetaTags(images))
@ -272,6 +295,79 @@ export default function CreateThreadDialog({ @@ -272,6 +295,79 @@ export default function CreateThreadDialog({
</p>
</div>
{/* Readings Options - Only show for literature topic */}
{selectedTopic === 'literature' && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Book className="w-4 h-4" />
<Label className="text-sm font-medium">{t('Readings Options')}</Label>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowReadingsPanel(!showReadingsPanel)}
className="ml-auto"
>
{showReadingsPanel ? t('Hide') : t('Configure')}
</Button>
</div>
{showReadingsPanel && (
<div className="border rounded-lg p-4 space-y-4 bg-muted/30">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-primary" />
<Label htmlFor="reading-group" className="text-sm">
{t('Reading group entry')}
</Label>
</div>
<Switch
id="reading-group"
checked={isReadingGroup}
onCheckedChange={setIsReadingGroup}
/>
</div>
{isReadingGroup && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="author">{t('Author')}</Label>
<Input
id="author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
placeholder={t('Enter the author name')}
className={errors.author ? 'border-destructive' : ''}
/>
{errors.author && (
<p className="text-sm text-destructive">{errors.author}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="subject">{t('Subject (Book Title)')}</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder={t('Enter the book title')}
className={errors.subject ? 'border-destructive' : ''}
/>
{errors.subject && (
<p className="text-sm text-destructive">{errors.subject}</p>
)}
</div>
<p className="text-xs text-muted-foreground">
{t('This will add additional tags for author and subject to help organize reading group discussions.')}
</p>
</div>
)}
</div>
)}
</div>
)}
{/* Relay Selection */}
<div className="space-y-2">
<Label htmlFor="relay">{t('Publish to Relay')}</Label>

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

@ -6,7 +6,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -6,7 +6,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { MessageSquarePlus } from 'lucide-react'
import { MessageSquarePlus, Book, BookOpen } from 'lucide-react'
import ThreadCard from '@/pages/primary/DiscussionsPage/ThreadCard'
import TopicFilter from '@/pages/primary/DiscussionsPage/TopicFilter'
import ThreadSort, { SortOption } from '@/pages/primary/DiscussionsPage/ThreadSort'
@ -25,6 +25,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -25,6 +25,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
const { pubkey } = useNostr()
const { push } = useSecondaryPage()
const [selectedTopic, setSelectedTopic] = useState('general')
const [selectedSubtopic, setSelectedSubtopic] = useState<string | null>(null)
const [selectedRelay, setSelectedRelay] = useState<string | null>(null)
const [selectedSort, setSelectedSort] = useState<SortOption>('newest')
const [allThreads, setAllThreads] = useState<NostrEvent[]>([])
@ -36,6 +37,10 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -36,6 +37,10 @@ const DiscussionsPage = forwardRef((_, ref) => {
const [viewMode, setViewMode] = useState<'flat' | 'grouped'>('flat')
const [groupedThreads, setGroupedThreads] = useState<Record<string, NostrEvent[]>>({})
// Search and filter state for readings
const [searchQuery, setSearchQuery] = useState('')
const [filterBy, setFilterBy] = useState<'author' | 'subject' | 'all'>('all')
// Use DEFAULT_FAVORITE_RELAYS for logged-out users, or user's favorite relays for logged-in users
const availableRelays = pubkey && favoriteRelays.length > 0 ? favoriteRelays : DEFAULT_FAVORITE_RELAYS
@ -141,7 +146,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -141,7 +146,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
}
console.log('Running filterThreadsByTopic with selectedSort:', selectedSort, 'statsLoaded:', statsLoaded, 'viewMode:', viewMode, 'selectedTopic:', selectedTopic)
filterThreadsByTopic()
}, [allThreads, selectedTopic, selectedSort, statsLoaded, viewMode])
}, [allThreads, selectedTopic, selectedSubtopic, selectedSort, statsLoaded, viewMode, searchQuery, filterBy])
// Fetch stats when sort changes to top/controversial
useEffect(() => {
@ -269,6 +274,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -269,6 +274,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
// Find the first matching topic from our available topics
let matchedTopic = 'general' // Default to general
let isReadingGroup = false
for (const topicTag of topicTags) {
if (availableTopicIds.includes(topicTag[1])) {
@ -277,9 +283,16 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -277,9 +283,16 @@ const DiscussionsPage = forwardRef((_, ref) => {
}
}
// Check if this is a reading group thread
if (matchedTopic === 'literature') {
const readingsTag = thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')
isReadingGroup = !!readingsTag
}
return {
...thread,
_categorizedTopic: matchedTopic
_categorizedTopic: matchedTopic,
_isReadingGroup: isReadingGroup
}
})
@ -287,17 +300,32 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -287,17 +300,32 @@ const DiscussionsPage = forwardRef((_, ref) => {
let threadsForTopic = selectedTopic === 'all'
? categorizedThreads.map(thread => {
// Remove the temporary categorization property but keep relay source
const { _categorizedTopic, ...cleanThread } = thread
const { _categorizedTopic, _isReadingGroup, ...cleanThread } = thread
return cleanThread
})
: categorizedThreads
.filter(thread => thread._categorizedTopic === selectedTopic)
.filter(thread => {
if (thread._categorizedTopic !== selectedTopic) return false
// Handle subtopic filtering for literature
if (selectedTopic === 'literature' && selectedSubtopic) {
if (selectedSubtopic === 'readings') {
return thread._isReadingGroup
} else if (selectedSubtopic === 'general') {
return !thread._isReadingGroup
}
}
return true
})
.map(thread => {
// Remove the temporary categorization property but keep relay source
const { _categorizedTopic, ...cleanThread } = thread
const { _categorizedTopic, _isReadingGroup, ...cleanThread } = thread
return cleanThread
})
// Apply search and filter for readings (handled in display logic)
// Apply sorting based on selectedSort
console.log('Sorting by:', selectedSort, 'with', threadsForTopic.length, 'threads')
@ -483,7 +511,10 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -483,7 +511,10 @@ const DiscussionsPage = forwardRef((_, ref) => {
<TopicFilter
topics={DISCUSSION_TOPICS}
selectedTopic={selectedTopic}
onTopicChange={setSelectedTopic}
onTopicChange={(topic) => {
setSelectedTopic(topic)
setSelectedSubtopic(null) // Reset subtopic when changing topic
}}
threads={threads}
replies={[]}
/>
@ -539,6 +570,93 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -539,6 +570,93 @@ const DiscussionsPage = forwardRef((_, ref) => {
<div className="flex justify-center py-8">
<div className="text-muted-foreground">{t('Loading threads...')}</div>
</div>
) : selectedTopic === 'literature' ? (
<div className="space-y-6">
{/* General Literature and Arts Section */}
<div className="space-y-3">
<div className="flex items-center gap-2 pb-2 border-b">
<BookOpen className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold">{t('General Topics')}</h2>
<span className="text-sm text-muted-foreground">
({threads.filter(thread => !thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length} {threads.filter(thread => !thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length === 1 ? t('thread') : t('threads')})
</span>
</div>
<div className="space-y-3">
{threads.filter(thread => !thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).map(thread => (
<ThreadCard
key={thread.id}
thread={thread}
onThreadClick={() => {
push(toNote(thread))
}}
/>
))}
</div>
</div>
{/* Readings Section */}
<div className="space-y-3">
<div className="flex items-center gap-2 pb-2 border-b">
<Book className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold">{t('Readings')}</h2>
<span className="text-sm text-muted-foreground">
({threads.filter(thread => thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length} {threads.filter(thread => thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length === 1 ? t('thread') : t('threads')})
</span>
</div>
{/* Readings-specific search and filter */}
<div className="flex gap-2 items-center p-3 bg-muted/30 rounded-lg">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('Search by author or book...')}
className="px-3 h-10 rounded border bg-background text-sm w-48"
/>
<select
value={filterBy}
onChange={(e) => setFilterBy(e.target.value as 'author' | 'subject' | 'all')}
className="px-3 h-10 rounded border bg-background text-sm"
>
<option value="all">{t('All')}</option>
<option value="author">{t('Author')}</option>
<option value="subject">{t('Subject')}</option>
</select>
</div>
<div className="space-y-3">
{threads
.filter(thread => thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings'))
.filter(thread => {
if (!searchQuery.trim()) return true
const authorTag = thread.tags.find(tag => tag[0] === 'author')
const subjectTag = thread.tags.find(tag => tag[0] === 'subject')
if (filterBy === 'author' && authorTag) {
return authorTag[1].toLowerCase().includes(searchQuery.toLowerCase())
} else if (filterBy === 'subject' && subjectTag) {
return subjectTag[1].toLowerCase().includes(searchQuery.toLowerCase())
} else if (filterBy === 'all') {
const authorMatch = authorTag && authorTag[1].toLowerCase().includes(searchQuery.toLowerCase())
const subjectMatch = subjectTag && subjectTag[1].toLowerCase().includes(searchQuery.toLowerCase())
return authorMatch || subjectMatch
}
return false
})
.map(thread => (
<ThreadCard
key={thread.id}
thread={thread}
onThreadClick={() => {
push(toNote(thread))
}}
/>
))}
</div>
</div>
</div>
) : (viewMode === 'grouped' && selectedTopic === 'all' ?
Object.keys(groupedThreads).length === 0 :
threads.length === 0) ? (

Loading…
Cancel
Save