98 lines
4.3 KiB
TypeScript
98 lines
4.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useThreadStore, type Thread, type ForumTag } from '../stores/thread.ts';
|
|
|
|
interface ForumViewProps {
|
|
channelId: string;
|
|
channelName: string;
|
|
onOpenThread: (thread: { id: string; name: string }) => void;
|
|
}
|
|
|
|
export function ForumView({ channelId, channelName, onOpenThread }: ForumViewProps) {
|
|
const threads = useThreadStore((s) => s.threadsByParent[channelId] || []);
|
|
const tags = useThreadStore((s) => s.tagsByForum[channelId] || []);
|
|
const fetchThreads = useThreadStore((s) => s.fetchThreads);
|
|
const fetchForumTags = useThreadStore((s) => s.fetchForumTags);
|
|
const createThread = useThreadStore((s) => s.createThread);
|
|
const createForumTag = useThreadStore((s) => s.createForumTag);
|
|
const [title, setTitle] = useState('');
|
|
const [body, setBody] = useState('');
|
|
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
|
const [tagName, setTagName] = useState('');
|
|
const [showForm, setShowForm] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchThreads(channelId);
|
|
fetchForumTags(channelId);
|
|
}, [channelId, fetchThreads, fetchForumTags]);
|
|
|
|
const handlePost = async () => {
|
|
if (!title.trim()) return;
|
|
const t = await createThread(channelId, { name: title.trim(), tag_ids: selectedTagIds });
|
|
if (body.trim()) {
|
|
// ponytail: send first message via thread store after creation
|
|
const { sendThreadMessage } = useThreadStore.getState();
|
|
await sendThreadMessage(t.id, body.trim());
|
|
}
|
|
setTitle('');
|
|
setBody('');
|
|
setSelectedTagIds([]);
|
|
setShowForm(false);
|
|
};
|
|
|
|
const toggleTag = (id: string) => {
|
|
setSelectedTagIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
|
|
};
|
|
|
|
const handleCreateTag = async () => {
|
|
if (!tagName.trim()) return;
|
|
await createForumTag(channelId, { name: tagName.trim() });
|
|
setTagName('');
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-gb-bg">
|
|
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg-s flex items-center justify-between">
|
|
<span>■ {channelName}</span>
|
|
<button onClick={() => setShowForm((p) => !p)} className="text-xs text-gb-fg-f hover:text-gb-orange font-mono">[NEW POST]</button>
|
|
</div>
|
|
{showForm && (
|
|
<div className="p-3 border-b border-gb-bg-t space-y-2 font-mono text-xs">
|
|
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="post title" className="terminal-input w-full" />
|
|
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="body (optional)" className="terminal-input w-full h-20" />
|
|
<div className="flex flex-wrap gap-2">
|
|
{tags.map((tag: ForumTag) => (
|
|
<button
|
|
key={tag.id}
|
|
onClick={() => toggleTag(tag.tag_id || tag.id)}
|
|
className={`px-2 py-0.5 border ${selectedTagIds.includes(tag.tag_id || tag.id) ? 'bg-gb-orange text-gb-bg border-gb-orange' : 'border-gb-bg-t text-gb-fg'}`}
|
|
>
|
|
{tag.emoji && <span>{tag.emoji}</span>} {tag.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<input value={tagName} onChange={(e) => setTagName(e.target.value)} placeholder="new tag" className="terminal-input flex-1" />
|
|
<button onClick={handleCreateTag} className="px-2 bg-gb-bg-s border border-gb-bg-t hover:border-gb-fg-t">[+TAG]</button>
|
|
</div>
|
|
<button onClick={handlePost} disabled={!title.trim()} className="px-3 py-1 bg-gb-orange text-gb-bg disabled:opacity-50">[POST]</button>
|
|
</div>
|
|
)}
|
|
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
|
{threads.length === 0 && <p className="text-gb-fg-f text-xs font-mono">[no posts]</p>}
|
|
{threads.map((t: Thread) => (
|
|
<button
|
|
key={t.id}
|
|
onClick={() => onOpenThread({ id: t.id, name: t.name })}
|
|
className="w-full text-left border border-gb-bg-t p-2 hover:border-gb-fg-t bg-gb-bg-s"
|
|
>
|
|
<div className="text-gb-fg text-sm font-bold truncate">{t.name}</div>
|
|
<div className="text-gb-fg-f text-xs">
|
|
replies:{t.message_count} latest:{t.last_message_at ? new Date(t.last_message_at).toLocaleDateString() : '—'}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|