|
|
|
@ -243,7 +243,7 @@ function analyzeDynamicTopics(entries: EventMapEntry[]): { |
|
|
|
|
|
|
|
|
|
|
|
// Enhanced topic categorization with dynamic topics
|
|
|
|
// Enhanced topic categorization with dynamic topics
|
|
|
|
function getEnhancedTopicFromTags(allTopics: string[], predefinedTopicIds: string[], dynamicTopics: DynamicTopic[]): string { |
|
|
|
function getEnhancedTopicFromTags(allTopics: string[], predefinedTopicIds: string[], dynamicTopics: DynamicTopic[]): string { |
|
|
|
// First check predefined topics
|
|
|
|
// First check predefined topics (these are main topics)
|
|
|
|
for (const topic of allTopics) { |
|
|
|
for (const topic of allTopics) { |
|
|
|
if (predefinedTopicIds.includes(topic)) { |
|
|
|
if (predefinedTopicIds.includes(topic)) { |
|
|
|
return topic |
|
|
|
return topic |
|
|
|
@ -258,14 +258,8 @@ function getEnhancedTopicFromTags(allTopics: string[], predefinedTopicIds: strin |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Finally check dynamic subtopics
|
|
|
|
// If no main topic found, return 'general' as the main topic
|
|
|
|
for (const topic of allTopics) { |
|
|
|
// The grouping logic will handle subtopics under their main topics
|
|
|
|
const dynamicTopic = dynamicTopics.find(dt => dt.id === topic && dt.isSubtopic) |
|
|
|
|
|
|
|
if (dynamicTopic) { |
|
|
|
|
|
|
|
return topic |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return 'general' |
|
|
|
return 'general' |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -315,7 +309,7 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
|
|
|
|
|
|
|
|
logger.debug('[DiscussionsPage] Using', finalRelays.length, 'comprehensive relays') |
|
|
|
logger.debug('[DiscussionsPage] Using', finalRelays.length, 'comprehensive relays') |
|
|
|
return Array.from(new Set(finalRelays)) |
|
|
|
return Array.from(new Set(finalRelays)) |
|
|
|
}, []) // Remove dependencies to prevent infinite loop
|
|
|
|
}, []) // No dependencies - will be called fresh each time from fetchAllEvents
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch all events
|
|
|
|
// Fetch all events
|
|
|
|
const fetchAllEvents = useCallback(async () => { |
|
|
|
const fetchAllEvents = useCallback(async () => { |
|
|
|
@ -329,6 +323,8 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
// Get comprehensive relay list
|
|
|
|
// Get comprehensive relay list
|
|
|
|
const allRelays = await buildComprehensiveRelayList() |
|
|
|
const allRelays = await buildComprehensiveRelayList() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug('[DiscussionsPage] Using relays:', allRelays.slice(0, 10), '... (total:', allRelays.length, ')') |
|
|
|
|
|
|
|
|
|
|
|
// Step 1: Fetch all discussion threads (kind 11)
|
|
|
|
// Step 1: Fetch all discussion threads (kind 11)
|
|
|
|
const discussionThreads = await client.fetchEvents(allRelays, [ |
|
|
|
const discussionThreads = await client.fetchEvents(allRelays, [ |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -338,6 +334,13 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
]) |
|
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
logger.debug('[DiscussionsPage] Fetched', discussionThreads.length, 'discussion threads') |
|
|
|
logger.debug('[DiscussionsPage] Fetched', discussionThreads.length, 'discussion threads') |
|
|
|
|
|
|
|
if (discussionThreads.length > 0) { |
|
|
|
|
|
|
|
logger.debug('[DiscussionsPage] Sample threads:', discussionThreads.slice(0, 3).map(t => ({ |
|
|
|
|
|
|
|
id: t.id.substring(0, 8), |
|
|
|
|
|
|
|
pubkey: t.pubkey.substring(0, 8), |
|
|
|
|
|
|
|
created_at: new Date(t.created_at * 1000).toISOString() |
|
|
|
|
|
|
|
}))) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Step 2: Get thread IDs and fetch related comments and reactions
|
|
|
|
// Step 2: Get thread IDs and fetch related comments and reactions
|
|
|
|
const threadIds = discussionThreads.map((thread: NostrEvent) => thread.id) |
|
|
|
const threadIds = discussionThreads.map((thread: NostrEvent) => thread.id) |
|
|
|
@ -453,7 +456,7 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
setLoading(false) |
|
|
|
setLoading(false) |
|
|
|
setIsRefreshing(false) |
|
|
|
setIsRefreshing(false) |
|
|
|
} |
|
|
|
} |
|
|
|
}, []) // Remove dependencies to prevent infinite loop
|
|
|
|
}, []) // Only run when explicitly called (mount or refresh button)
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate time span counts
|
|
|
|
// Calculate time span counts
|
|
|
|
const calculateTimeSpanCounts = useCallback(() => { |
|
|
|
const calculateTimeSpanCounts = useCallback(() => { |
|
|
|
@ -603,37 +606,77 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
return () => clearTimeout(timeoutId) |
|
|
|
return () => clearTimeout(timeoutId) |
|
|
|
}, [eventMap, searchQuery]) |
|
|
|
}, [eventMap, searchQuery]) |
|
|
|
|
|
|
|
|
|
|
|
// Group events by topic
|
|
|
|
// Group events by topic with hierarchy (main topics and subtopics)
|
|
|
|
const groupedEvents = useMemo(() => { |
|
|
|
const groupedEvents = useMemo(() => { |
|
|
|
const groups = new Map<string, EventMapEntry[]>() |
|
|
|
const mainTopicGroups = new Map<string, { |
|
|
|
|
|
|
|
entries: EventMapEntry[] |
|
|
|
|
|
|
|
subtopics: Map<string, EventMapEntry[]> |
|
|
|
|
|
|
|
}>() |
|
|
|
|
|
|
|
|
|
|
|
searchedEntries.forEach((entry) => { |
|
|
|
searchedEntries.forEach((entry) => { |
|
|
|
const topic = entry.categorizedTopic |
|
|
|
const mainTopic = entry.categorizedTopic |
|
|
|
if (!groups.has(topic)) { |
|
|
|
|
|
|
|
groups.set(topic, []) |
|
|
|
// Initialize main topic group if it doesn't exist
|
|
|
|
|
|
|
|
if (!mainTopicGroups.has(mainTopic)) { |
|
|
|
|
|
|
|
mainTopicGroups.set(mainTopic, { |
|
|
|
|
|
|
|
entries: [], |
|
|
|
|
|
|
|
subtopics: new Map() |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const group = mainTopicGroups.get(mainTopic)! |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this entry has any dynamic subtopics
|
|
|
|
|
|
|
|
const entrySubtopics = entry.allTopics.filter(topic => { |
|
|
|
|
|
|
|
const dynamicTopic = dynamicTopics.allTopics.find(dt => dt.id === topic && dt.isSubtopic) |
|
|
|
|
|
|
|
return !!dynamicTopic |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (entrySubtopics.length > 0) { |
|
|
|
|
|
|
|
// Group under the first subtopic found
|
|
|
|
|
|
|
|
const subtopic = entrySubtopics[0] |
|
|
|
|
|
|
|
if (!group.subtopics.has(subtopic)) { |
|
|
|
|
|
|
|
group.subtopics.set(subtopic, []) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
group.subtopics.get(subtopic)!.push(entry) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// No subtopic, add to main topic
|
|
|
|
|
|
|
|
group.entries.push(entry) |
|
|
|
} |
|
|
|
} |
|
|
|
groups.get(topic)!.push(entry) |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// Sort threads within each group by newest-first (most recent activity)
|
|
|
|
// Sort threads within each group and subtopic by newest-first
|
|
|
|
groups.forEach((entries) => { |
|
|
|
mainTopicGroups.forEach((group) => { |
|
|
|
entries.sort((a, b) => { |
|
|
|
const sortEntries = (entries: EventMapEntry[]) => { |
|
|
|
const aActivity = Math.max( |
|
|
|
entries.sort((a, b) => { |
|
|
|
a.event.created_at * 1000, |
|
|
|
const aActivity = Math.max( |
|
|
|
a.lastCommentTime > 0 ? a.lastCommentTime * 1000 : 0, |
|
|
|
a.event.created_at * 1000, |
|
|
|
a.lastVoteTime > 0 ? a.lastVoteTime * 1000 : 0 |
|
|
|
a.lastCommentTime > 0 ? a.lastCommentTime * 1000 : 0, |
|
|
|
) |
|
|
|
a.lastVoteTime > 0 ? a.lastVoteTime * 1000 : 0 |
|
|
|
const bActivity = Math.max( |
|
|
|
) |
|
|
|
b.event.created_at * 1000, |
|
|
|
const bActivity = Math.max( |
|
|
|
b.lastCommentTime > 0 ? b.lastCommentTime * 1000 : 0, |
|
|
|
b.event.created_at * 1000, |
|
|
|
b.lastVoteTime > 0 ? b.lastVoteTime * 1000 : 0 |
|
|
|
b.lastCommentTime > 0 ? b.lastCommentTime * 1000 : 0, |
|
|
|
) |
|
|
|
b.lastVoteTime > 0 ? b.lastVoteTime * 1000 : 0 |
|
|
|
return bActivity - aActivity // Newest first
|
|
|
|
) |
|
|
|
}) |
|
|
|
return bActivity - aActivity // Newest first
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sortEntries(group.entries) |
|
|
|
|
|
|
|
group.subtopics.forEach((entries) => sortEntries(entries)) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Convert to array format for rendering with proper hierarchy
|
|
|
|
|
|
|
|
const result: Array<[string, EventMapEntry[], Map<string, EventMapEntry[]>]> = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mainTopicGroups.forEach((group, mainTopic) => { |
|
|
|
|
|
|
|
// Add main topic with its subtopics
|
|
|
|
|
|
|
|
result.push([mainTopic, group.entries, group.subtopics]) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// Sort groups by most recent activity (newest first)
|
|
|
|
// Sort groups by most recent activity (newest first)
|
|
|
|
const sortedGroups = Array.from(groups.entries()).sort(([, aEntries], [, bEntries]) => { |
|
|
|
result.sort(([, aEntries], [, bEntries]) => { |
|
|
|
if (aEntries.length === 0 && bEntries.length === 0) return 0 |
|
|
|
if (aEntries.length === 0 && bEntries.length === 0) return 0 |
|
|
|
if (aEntries.length === 0) return 1 |
|
|
|
if (aEntries.length === 0) return 1 |
|
|
|
if (bEntries.length === 0) return -1 |
|
|
|
if (bEntries.length === 0) return -1 |
|
|
|
@ -652,8 +695,8 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
return bMostRecent - aMostRecent // Newest first
|
|
|
|
return bMostRecent - aMostRecent // Newest first
|
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
return sortedGroups |
|
|
|
return result |
|
|
|
}, [searchedEntries]) |
|
|
|
}, [searchedEntries, dynamicTopics]) |
|
|
|
|
|
|
|
|
|
|
|
// Handle refresh
|
|
|
|
// Handle refresh
|
|
|
|
const handleRefresh = () => { |
|
|
|
const handleRefresh = () => { |
|
|
|
@ -711,21 +754,39 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
<div className="flex flex-col h-full"> |
|
|
|
<div className="flex flex-col h-full"> |
|
|
|
{/* Header */} |
|
|
|
{/* Header */} |
|
|
|
<div className="flex flex-col gap-4 p-4 border-b"> |
|
|
|
<div className="flex flex-col gap-4 p-4 border-b"> |
|
|
|
<div className="flex items-center justify-between"> |
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> |
|
|
|
<h1 className="text-2xl font-bold">{t('Discussions')}</h1> |
|
|
|
<h1 className="text-2xl font-bold">{t('Discussions')}</h1> |
|
|
|
<div className="flex items-center gap-3"> |
|
|
|
|
|
|
|
<button |
|
|
|
<button |
|
|
|
onClick={() => setShowCreateDialog(true)} |
|
|
|
onClick={() => setShowCreateDialog(true)} |
|
|
|
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700" |
|
|
|
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 w-full sm:w-auto" |
|
|
|
> |
|
|
|
> |
|
|
|
{t('Create Thread')} |
|
|
|
{t('Create Thread')} |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Search Bar */} |
|
|
|
|
|
|
|
<div className="relative"> |
|
|
|
|
|
|
|
{isSearching ? ( |
|
|
|
|
|
|
|
<RefreshCw className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 animate-spin" /> |
|
|
|
|
|
|
|
) : ( |
|
|
|
|
|
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
<input |
|
|
|
|
|
|
|
type="text" |
|
|
|
|
|
|
|
placeholder={t('Search threads by title, content, tags, npub, author...')} |
|
|
|
|
|
|
|
value={searchQuery} |
|
|
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)} |
|
|
|
|
|
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-black dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Filters - Stack on mobile, row on desktop */} |
|
|
|
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4"> |
|
|
|
{/* Topic Selection Dropdown */} |
|
|
|
{/* Topic Selection Dropdown */} |
|
|
|
<select
|
|
|
|
<select
|
|
|
|
value={selectedTopic}
|
|
|
|
value={selectedTopic}
|
|
|
|
onChange={(e) => setSelectedTopic(e.target.value)} |
|
|
|
onChange={(e) => setSelectedTopic(e.target.value)} |
|
|
|
className="px-3 py-2 bg-white dark:bg-gray-800 text-black dark:text-white border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
|
|
className="w-full sm:w-auto px-3 py-2 bg-white dark:bg-gray-800 text-black dark:text-white border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
|
|
> |
|
|
|
> |
|
|
|
<option value="all">All Topics ({allEventMap.size})</option> |
|
|
|
<option value="all">All Topics ({allEventMap.size})</option> |
|
|
|
{availableTopics.map(({ topic, count, isDynamic, isMainTopic, isSubtopic }) => ( |
|
|
|
{availableTopics.map(({ topic, count, isDynamic, isMainTopic, isSubtopic }) => ( |
|
|
|
@ -743,76 +804,104 @@ const DiscussionsPage = forwardRef(() => { |
|
|
|
<select
|
|
|
|
<select
|
|
|
|
value={timeSpan}
|
|
|
|
value={timeSpan}
|
|
|
|
onChange={(e) => setTimeSpan(e.target.value as '30days' | '90days' | 'all')} |
|
|
|
onChange={(e) => setTimeSpan(e.target.value as '30days' | '90days' | 'all')} |
|
|
|
className="px-3 py-2 bg-white dark:bg-gray-800 text-black dark:text-white border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
|
|
className="w-full sm:w-auto px-3 py-2 bg-white dark:bg-gray-800 text-black dark:text-white border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
|
|
> |
|
|
|
> |
|
|
|
<option value="30days">30 days ({timeSpanCounts['30days']})</option> |
|
|
|
<option value="30days">30 days ({timeSpanCounts['30days']})</option> |
|
|
|
<option value="90days">90 days ({timeSpanCounts['90days']})</option> |
|
|
|
<option value="90days">90 days ({timeSpanCounts['90days']})</option> |
|
|
|
<option value="all">All found ({timeSpanCounts.all})</option> |
|
|
|
<option value="all">All found ({timeSpanCounts.all})</option> |
|
|
|
</select> |
|
|
|
</select> |
|
|
|
|
|
|
|
|
|
|
|
<button |
|
|
|
{/* Refresh Button */} |
|
|
|
|
|
|
|
<button |
|
|
|
onClick={handleRefresh} |
|
|
|
onClick={handleRefresh} |
|
|
|
disabled={loading} |
|
|
|
disabled={loading} |
|
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" |
|
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded w-full sm:w-auto flex items-center justify-center sm:justify-start" |
|
|
|
> |
|
|
|
> |
|
|
|
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} /> |
|
|
|
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} /> |
|
|
|
</button> |
|
|
|
<span className="ml-2 sm:hidden">{t('Refresh')}</span> |
|
|
|
</div> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Search Bar */} |
|
|
|
|
|
|
|
<div className="relative"> |
|
|
|
|
|
|
|
{isSearching ? ( |
|
|
|
|
|
|
|
<RefreshCw className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 animate-spin" /> |
|
|
|
|
|
|
|
) : ( |
|
|
|
|
|
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
<input |
|
|
|
|
|
|
|
type="text" |
|
|
|
|
|
|
|
placeholder={t('Search threads by title, content, tags, npub, author...')} |
|
|
|
|
|
|
|
value={searchQuery} |
|
|
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)} |
|
|
|
|
|
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-black dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
{/* Content */} |
|
|
|
{/* Content */} |
|
|
|
<div className="flex-1 overflow-y-auto p-4"> |
|
|
|
<div className="flex-1 overflow-y-auto p-2 sm:p-4"> |
|
|
|
{loading ? ( |
|
|
|
{loading ? ( |
|
|
|
<div className="text-center py-8">{t('Loading...')}</div> |
|
|
|
<div className="text-center py-8">{t('Loading...')}</div> |
|
|
|
) : isSearching ? ( |
|
|
|
) : isSearching ? ( |
|
|
|
<div className="text-center py-8">{t('Searching...')}</div> |
|
|
|
<div className="text-center py-8">{t('Searching...')}</div> |
|
|
|
) : ( |
|
|
|
) : ( |
|
|
|
<div className="space-y-6"> |
|
|
|
<div className="space-y-6"> |
|
|
|
{groupedEvents.map(([topic, events]) => { |
|
|
|
{groupedEvents.map(([topic, events, subtopics]) => { |
|
|
|
const topicInfo = availableTopics.find(t => t.topic === topic) |
|
|
|
const topicInfo = availableTopics.find(t => t.topic === topic) |
|
|
|
const isDynamicMain = topicInfo?.isDynamic && topicInfo?.isMainTopic |
|
|
|
const isDynamicMain = topicInfo?.isDynamic && topicInfo?.isMainTopic |
|
|
|
const isDynamicSubtopic = topicInfo?.isDynamic && topicInfo?.isSubtopic |
|
|
|
const isDynamicSubtopic = topicInfo?.isDynamic && topicInfo?.isSubtopic |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<div key={topic}> |
|
|
|
<div key={topic} className="space-y-4"> |
|
|
|
<h2 className="text-lg font-semibold mb-3 capitalize flex items-center gap-2"> |
|
|
|
{/* Main Topic Header */} |
|
|
|
{isDynamicMain && <span className="text-orange-500">🔥</span>} |
|
|
|
<h2 className="text-lg font-semibold mb-3 capitalize flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2"> |
|
|
|
{isDynamicSubtopic && <span className="text-blue-500">📌</span>} |
|
|
|
<span className="flex items-center gap-2"> |
|
|
|
{topic} ({events.length} {events.length === 1 ? t('thread') : t('threads')}) |
|
|
|
{isDynamicMain && <span className="text-orange-500">🔥</span>} |
|
|
|
{isDynamicMain && <span className="text-xs bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200 px-2 py-1 rounded">Main Topic</span>} |
|
|
|
{isDynamicSubtopic && <span className="text-blue-500">📌</span>} |
|
|
|
{isDynamicSubtopic && <span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">Subtopic</span>} |
|
|
|
{topic} ({events.length} {events.length === 1 ? t('thread') : t('threads')}) |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
{isDynamicMain && <span className="text-xs bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200 px-2 py-1 rounded w-fit">Main Topic</span>} |
|
|
|
|
|
|
|
{isDynamicSubtopic && <span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded w-fit">Subtopic</span>} |
|
|
|
</h2> |
|
|
|
</h2> |
|
|
|
<div className="space-y-3"> |
|
|
|
|
|
|
|
{events.map((entry) => ( |
|
|
|
{/* Main Topic Threads */} |
|
|
|
<ThreadCard |
|
|
|
{events.length > 0 && ( |
|
|
|
key={entry.event.id} |
|
|
|
<div className="space-y-3"> |
|
|
|
thread={entry.event} |
|
|
|
{events.map((entry) => ( |
|
|
|
commentCount={entry.commentCount} |
|
|
|
<ThreadCard |
|
|
|
lastCommentTime={entry.lastCommentTime} |
|
|
|
key={entry.event.id} |
|
|
|
lastVoteTime={entry.lastVoteTime} |
|
|
|
thread={entry.event} |
|
|
|
upVotes={entry.upVotes} |
|
|
|
commentCount={entry.commentCount} |
|
|
|
downVotes={entry.downVotes} |
|
|
|
lastCommentTime={entry.lastCommentTime} |
|
|
|
onThreadClick={() => handleThreadClick(entry.event.id)} |
|
|
|
lastVoteTime={entry.lastVoteTime} |
|
|
|
/> |
|
|
|
upVotes={entry.upVotes} |
|
|
|
))} |
|
|
|
downVotes={entry.downVotes} |
|
|
|
</div> |
|
|
|
onThreadClick={() => handleThreadClick(entry.event.id)} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
))} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Subtopic Groups */} |
|
|
|
|
|
|
|
{subtopics.size > 0 && ( |
|
|
|
|
|
|
|
<div className="ml-2 sm:ml-4 space-y-4"> |
|
|
|
|
|
|
|
{Array.from(subtopics.entries()).map(([subtopic, subtopicEvents]) => { |
|
|
|
|
|
|
|
const subtopicInfo = availableTopics.find(t => t.topic === subtopic) |
|
|
|
|
|
|
|
const isSubtopicDynamic = subtopicInfo?.isDynamic && subtopicInfo?.isSubtopic |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<div key={subtopic} className="space-y-2"> |
|
|
|
|
|
|
|
<h3 className="text-sm sm:text-md font-medium capitalize flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-muted-foreground"> |
|
|
|
|
|
|
|
<span className="flex items-center gap-2"> |
|
|
|
|
|
|
|
{isSubtopicDynamic && <span className="text-blue-500">📌</span>} |
|
|
|
|
|
|
|
{subtopic} ({subtopicEvents.length} {subtopicEvents.length === 1 ? t('thread') : t('threads')}) |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
{isSubtopicDynamic && <span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded w-fit">Subtopic</span>} |
|
|
|
|
|
|
|
</h3> |
|
|
|
|
|
|
|
<div className="space-y-3"> |
|
|
|
|
|
|
|
{subtopicEvents.map((entry) => ( |
|
|
|
|
|
|
|
<ThreadCard |
|
|
|
|
|
|
|
key={entry.event.id} |
|
|
|
|
|
|
|
thread={entry.event} |
|
|
|
|
|
|
|
commentCount={entry.commentCount} |
|
|
|
|
|
|
|
lastCommentTime={entry.lastCommentTime} |
|
|
|
|
|
|
|
lastVoteTime={entry.lastVoteTime} |
|
|
|
|
|
|
|
upVotes={entry.upVotes} |
|
|
|
|
|
|
|
downVotes={entry.downVotes} |
|
|
|
|
|
|
|
onThreadClick={() => handleThreadClick(entry.event.id)} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
))} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
})} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
)} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
) |
|
|
|
) |
|
|
|
})} |
|
|
|
})} |
|
|
|
|