setActiveThread(t)} />
) : activeChannel && activeChannel.type === 'calendar' && activeChannelId ? (
+ ) : activeChannel && activeChannel.type === 'docs' && activeChannelId ? (
+
) : (
<>
diff --git a/web/src/components/CreateChannelModal.tsx b/web/src/components/CreateChannelModal.tsx
index 23b9da8..e25f11b 100644
--- a/web/src/components/CreateChannelModal.tsx
+++ b/web/src/components/CreateChannelModal.tsx
@@ -11,7 +11,7 @@ interface ChannelApiResponse {
id: string;
server_id: string;
name: string;
- type: 'text' | 'voice' | 'forum' | 'calendar';
+ type: 'text' | 'voice' | 'forum' | 'calendar' | 'docs';
category: string;
position: number;
slowmode_seconds: number;
@@ -29,7 +29,7 @@ const SLOWMODE_OPTIONS = [
export function CreateChannelModal({ serverId, onClose }: CreateChannelModalProps) {
const [name, setName] = useState('');
- const [type, setType] = useState<'text' | 'voice' | 'forum' | 'calendar'>('text');
+ const [type, setType] = useState<'text' | 'voice' | 'forum' | 'calendar' | 'docs'>('text');
const [category, setCategory] = useState('general');
const [slowmodeSeconds, setSlowmodeSeconds] = useState(0);
const [loading, setLoading] = useState(false);
@@ -107,13 +107,14 @@ export function CreateChannelModal({ serverId, onClose }: CreateChannelModalProp
diff --git a/web/src/components/DocsView.tsx b/web/src/components/DocsView.tsx
new file mode 100644
index 0000000..e3a9798
--- /dev/null
+++ b/web/src/components/DocsView.tsx
@@ -0,0 +1,134 @@
+import { useEffect, useState } from 'react';
+import { api } from '../lib/api.ts';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+
+interface DocSummary {
+ id: string;
+ channel_id: string;
+ creator_id: string;
+ title: string;
+ created_at: string;
+ updated_at: string;
+}
+
+interface DocDetail extends DocSummary {
+ content: string;
+}
+
+interface DocsViewProps {
+ channelId: string;
+ channelName: string;
+}
+
+export function DocsView({ channelId, channelName }: DocsViewProps) {
+ const [docs, setDocs] = useState
([]);
+ const [activeDoc, setActiveDoc] = useState(null);
+ const [editing, setEditing] = useState(false);
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [showNew, setShowNew] = useState(false);
+ const [newTitle, setNewTitle] = useState('');
+
+ useEffect(() => {
+ setLoading(true);
+ api.get(`/channels/${channelId}/docs`)
+ .then((data) => setDocs(Array.isArray(data) ? data : []))
+ .finally(() => setLoading(false));
+ }, [channelId]);
+
+ const openDoc = async (id: string) => {
+ const d = await api.get(`/channels/docs/${id}`);
+ setActiveDoc(d);
+ setTitle(d.title);
+ setContent(d.content);
+ setEditing(false);
+ };
+
+ const createDoc = async () => {
+ if (!newTitle.trim()) return;
+ const d = await api.post(`/channels/${channelId}/docs`, { title: newTitle, content: '' });
+ setDocs((prev) => [{ id: d.id, channel_id: d.channel_id, creator_id: d.creator_id, title: d.title, created_at: d.created_at, updated_at: d.updated_at }, ...prev]);
+ setShowNew(false);
+ setNewTitle('');
+ openDoc(d.id);
+ };
+
+ const saveDoc = async () => {
+ if (!activeDoc) return;
+ const d = await api.patch(`/channels/docs/${activeDoc.id}`, { title, content });
+ setActiveDoc(d);
+ setDocs((prev) => prev.map((s) => s.id === d.id ? { ...s, title: d.title, updated_at: d.updated_at } : s));
+ setEditing(false);
+ };
+
+ const deleteDoc = async (id: string) => {
+ if (!confirm('Delete this doc?')) return;
+ await api.delete(`/channels/docs/${id}`);
+ setDocs((prev) => prev.filter((d) => d.id !== id));
+ if (activeDoc?.id === id) setActiveDoc(null);
+ };
+
+ return (
+
+
+ ☰ {channelName}
+
+
+ {showNew && (
+
+ setNewTitle(e.target.value)} placeholder="doc title" className="terminal-input flex-1" />
+
+
+ )}
+
+
+ {loading &&
[loading...]
}
+ {!loading && docs.length === 0 &&
[no docs]
}
+ {docs.map((d) => (
+
openDoc(d.id)}
+ className={`p-2 cursor-pointer truncate ${activeDoc?.id === d.id ? 'bg-gb-bg-t text-gb-fg' : 'text-gb-fg-s hover:bg-gb-bg-s'}`}
+ >
+ {d.title}
+
updated {new Date(d.updated_at).toLocaleDateString()}
+
+ ))}
+
+
+ {!activeDoc && (
+
+ select or create a doc
+
+ )}
+ {activeDoc && !editing && (
+
+
+
{activeDoc.title}
+
+
+
+
+
+
+ {activeDoc.content || '*empty*'}
+
+
+ )}
+ {activeDoc && editing && (
+
+ )}
+
+
+
+ );
+}
diff --git a/web/src/stores/channel.ts b/web/src/stores/channel.ts
index eb75e3b..c8e7655 100644
--- a/web/src/stores/channel.ts
+++ b/web/src/stores/channel.ts
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { api } from '../lib/api.ts';
-export type ChannelType = 'text' | 'voice' | 'forum' | 'calendar';
+export type ChannelType = 'text' | 'voice' | 'forum' | 'calendar' | 'docs';
export interface Channel {
id: string;
diff --git a/web/tsconfig.app.tsbuildinfo b/web/tsconfig.app.tsbuildinfo
index 662e8a5..a249d24 100644
--- a/web/tsconfig.app.tsbuildinfo
+++ b/web/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/App.tsx","./src/main.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandManager.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/EmojiPicker.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/ws.ts"],"version":"5.9.3"}
\ No newline at end of file
+{"root":["./src/App.tsx","./src/main.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandManager.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/DocsView.tsx","./src/components/EmojiPicker.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/ws.ts"],"version":"5.9.3"}
\ No newline at end of file