@mounaji_npm/forum
v0.1.0
Published
Modular forum and posts system — PostCard, PostList, PostDetail, ReplyThread, CategoryNav, and full page scaffolds
Downloads
27
Maintainers
Readme
@mounaji_npm/forum
Modular forum and community posts system for React applications. Drop-in pages for listing posts, reading threads, and creating new discussions — all styled via @mounaji_npm/tokens CSS variables with no external icon or UI libraries required.
Install
npm install @mounaji_npm/tokens @mounaji_npm/forumOr scaffold a full forum project with the CLI (see @mounaji_npm/cli):
npx @mounaji_npm/cli create my-forum
# Choose template: 2. Forum / CommunityWhat's included
| Export | Description |
|---|---|
| ForumPage | Full listing page — category sidebar, search, sort, post list |
| PostPage | Post detail page — full content, vote column, reply thread |
| CreatePostPage | New post form — title, body, category chips, tags |
| PostCard | Single post card (used inside ForumPage) |
| PostList | List of PostCard with skeleton loading and empty state |
| PostDetail | Full post content block with vote column |
| ReplyCard | Single reply with vote + accepted-answer badge |
| ReplyThread | Sorted reply list + ReplyComposer below |
| ReplyComposer | Textarea + submit button for writing a reply |
| CategoryNav | Sidebar category list with counts |
| VoteButton | Up/downvote control with optimistic updates |
| AuthorMeta | Avatar + name + relative timestamp |
| TagChip | #tag pill |
| CategoryBadge | Colored category label badge |
| DEMO_POSTS | Demo post data for prototyping |
| DEMO_CATEGORIES | Demo category data |
| DEMO_REPLIES | Demo reply data |
Quick Start — Next.js App Router
1. Install
npm install @mounaji_npm/tokens @mounaji_npm/forum2. Forum listing page
// app/forum/page.js
'use client';
import { useRouter } from 'next/navigation';
import { ForumPage } from '@mounaji_npm/forum';
const CATEGORIES = [
{ id: 'all', label: 'All Posts', icon: '◉', count: 42 },
{ id: 'general', label: 'General', icon: '💬', count: 18 },
{ id: 'questions',label: 'Questions', icon: '❓', count: 14 },
{ id: 'ideas', label: 'Ideas', icon: '💡', count: 10 },
];
export default function ForumListPage() {
const router = useRouter();
return (
<ForumPage
categories={CATEGORIES}
title="Community Forum"
subtitle="Ask questions, share ideas, and connect with others."
onPostClick={id => router.push(`/forum/${id}`)}
onNewPost={() => router.push('/forum/create')}
/>
);
}3. Post detail page
// app/forum/[id]/page.js
'use client';
import { useRouter } from 'next/navigation';
import { PostPage } from '@mounaji_npm/forum';
export default function PostDetailPage({ params }) {
const router = useRouter();
// Replace with your data fetching (SWR, React Query, fetch, etc.)
const post = usePost(params.id);
const replies = useReplies(params.id);
return (
<PostPage
post={post}
replies={replies}
currentUser={{ id: 'u1', name: 'jane_doe' }}
onBack={() => router.push('/forum')}
onVotePost={(postId, vote) => api.votePost(postId, vote)}
onVoteReply={(replyId, vote) => api.voteReply(replyId, vote)}
onAccept={replyId => api.acceptReply(replyId)}
onSubmitReply={body => api.createReply(params.id, body)}
/>
);
}4. Create post page
// app/forum/create/page.js
'use client';
import { useRouter } from 'next/navigation';
import { CreatePostPage } from '@mounaji_npm/forum';
const CATEGORIES = [
{ id: 'general', label: 'General', icon: '💬' },
{ id: 'questions', label: 'Questions', icon: '❓' },
{ id: 'ideas', label: 'Ideas', icon: '💡' },
{ id: 'bugs', label: 'Bug Reports', icon: '🐛' },
];
export default function CreatePage() {
const router = useRouter();
return (
<CreatePostPage
categories={CATEGORIES}
currentUser={{ id: 'u1', name: 'jane_doe' }}
onSubmit={async (post) => {
await api.createPost(post);
router.push('/forum');
}}
onCancel={() => router.push('/forum')}
/>
);
}Quick Start — Vite + React Router
// src/App.jsx
import { BrowserRouter, Routes, Route, useNavigate, useParams } from 'react-router-dom';
import { TokensProvider } from '@mounaji_npm/tokens';
import { ForumPage, PostPage, CreatePostPage, DEMO_POSTS, DEMO_REPLIES } from '@mounaji_npm/forum';
function ForumRoute() {
const navigate = useNavigate();
return (
<ForumPage
onPostClick={id => navigate(`/forum/${id}`)}
onNewPost={() => navigate('/forum/create')}
/>
);
}
function PostRoute() {
const { id } = useParams();
const navigate = useNavigate();
const post = DEMO_POSTS.find(p => p.id === id) ?? DEMO_POSTS[0];
return (
<PostPage
post={post}
replies={DEMO_REPLIES}
onBack={() => navigate('/forum')}
onSubmitReply={body => console.log('new reply:', body)}
/>
);
}
function CreateRoute() {
const navigate = useNavigate();
return (
<CreatePostPage
onSubmit={post => { console.log('new post:', post); navigate('/forum'); }}
onCancel={() => navigate('/forum')}
/>
);
}
export default function App() {
return (
<TokensProvider>
<BrowserRouter>
<Routes>
<Route path="/forum" element={<ForumRoute />} />
<Route path="/forum/:id" element={<PostRoute />} />
<Route path="/forum/create" element={<CreateRoute />} />
</Routes>
</BrowserRouter>
</TokensProvider>
);
}Using demo data for prototyping
All three page components ship with demo data as default props. If you just want to see the UI without wiring any backend:
import { ForumPage, PostPage, CreatePostPage } from '@mounaji_npm/forum';
// Renders immediately with demo posts, categories, and replies
<ForumPage />
<PostPage />
<CreatePostPage />To inspect or seed a database with the demo data:
import { DEMO_POSTS, DEMO_CATEGORIES, DEMO_REPLIES } from '@mounaji_npm/forum';Component Reference
ForumPage
Full forum listing page. Manages client-side filtering and sorting when posts is a static array; pass new posts data from your backend to override.
| Prop | Type | Default | Description |
|---|---|---|---|
| categories | Category[] | DEMO_CATEGORIES | Sidebar category list |
| posts | Post[] | DEMO_POSTS | Posts to display |
| activeCategory | string | 'all' | Initially selected category id |
| searchQuery | string | '' | Controlled search value |
| sortBy | 'newest'|'top'|'unanswered' | 'newest' | Initial sort |
| onCategoryChange | (id) => void | — | Called when category is clicked |
| onSearch | (query) => void | — | Called on search input change |
| onSortChange | (sort) => void | — | Called when sort tab changes |
| onPostClick | (postId) => void | — | Called when a post card is clicked |
| onNewPost | () => void | — | Called when "+ New Post" is clicked |
| onVote | (postId, vote) => void | — | Called when a post is voted |
| isLoading | boolean | false | Show skeleton cards |
| isDark | boolean | true | Dark / light theme |
| title | string | 'Forum' | Page heading |
| subtitle | string | — | Subheading below title |
| header | ReactNode | — | Fully replaces the default header |
| style | CSSProperties | — | Wrapper style |
PostPage
Post detail + replies. Defaults to DEMO_POSTS[0] and DEMO_REPLIES when no props are passed.
| Prop | Type | Default | Description |
|---|---|---|---|
| post | Post | DEMO_POSTS[0] | Post object |
| replies | Reply[] | DEMO_REPLIES | Reply list |
| currentUser | { id, name, avatar? } | null | Logged-in user (used for accept-answer permission) |
| onBack | () => void | — | Called when "← Back to Forum" is clicked |
| onVotePost | (postId, vote) => void | — | Called when the post is voted |
| onVoteReply | (replyId, vote) => void | — | Called when a reply is voted |
| onAccept | (replyId) => void | — | Called when a reply is marked as accepted |
| onSubmitReply | (body) => void \| Promise | — | Called when reply form is submitted |
| isLoading | boolean | false | Show post skeleton |
| isDark | boolean | true | Theme |
| style | CSSProperties | — | Wrapper style |
CreatePostPage
New post form with title, body (textarea), category selector, and tags input.
| Prop | Type | Default | Description |
|---|---|---|---|
| categories | Category[] | DEMO_CATEGORIES (without all) | Category options shown as chips |
| currentUser | { name, avatar? } | null | Shown as "Posting as …" hint |
| onSubmit | (post) => void \| Promise | — | Called with { title, body, categoryId, tags } |
| onCancel | () => void | — | Called when Cancel is clicked |
| isDark | boolean | true | Theme |
| style | CSSProperties | — | Wrapper style |
VoteButton
Up/downvote control with optimistic local state.
import { VoteButton } from '@mounaji_npm/forum';
<VoteButton
count={42}
userVote={1} // 1 = upvoted, -1 = downvoted, 0 = no vote
onChange={v => api.vote(v)}
vertical // stacks up/count/down vertically (default: false)
size="sm" // 'sm' | 'md'
isDark
/>CategoryNav
Standalone sidebar component — use it outside ForumPage in a custom layout.
import { CategoryNav } from '@mounaji_npm/forum';
<CategoryNav
categories={CATEGORIES}
active="questions"
onChange={id => setActive(id)}
title="Browse"
isDark
/>ReplyComposer
Standalone reply textarea — use it in a custom post layout.
import { ReplyComposer } from '@mounaji_npm/forum';
<ReplyComposer
currentUser={{ name: 'jane_doe' }}
onSubmit={async body => { await api.createReply(postId, body); }}
placeholder="Share your thoughts…"
isDark
/>Data Shapes
Post
{
id: string,
title: string,
body: string,
author: { id: string, name: string, avatar?: string },
category: { id: string, label: string, icon?: string },
tags: string[],
votes: number,
userVote: 1 | 0 | -1, // current user's vote (default 0)
replyCount: number,
viewCount: number,
createdAt: string | Date,
updatedAt?: string | Date,
isPinned?: boolean,
isSolved?: boolean,
isLocked?: boolean,
}Reply
{
id: string,
body: string,
author: { id: string, name: string, avatar?: string },
votes: number,
userVote: 1 | 0 | -1,
createdAt: string | Date,
isAccepted?: boolean,
}Category
{
id: string,
label: string,
icon?: string, // emoji
count?: number, // shown in sidebar
}Theming
All components read from @mounaji_npm/tokens CSS variables. Override at the TokensProvider level:
import { TokensProvider } from '@mounaji_npm/tokens';
import { ForumPage } from '@mounaji_npm/forum';
<TokensProvider initialTokens={{
colorPrimary: '#7C3AED',
colorAccent: '#06B6D4',
radiusLg: '1rem',
}}>
<ForumPage ... />
</TokensProvider>Switch to light mode by passing isDark={false} to any page:
<ForumPage isDark={false} ... />Connecting a real backend
The forum components are headless — they accept data and fire callbacks. Plug in any data layer:
import useSWR from 'swr';
import { ForumPage } from '@mounaji_npm/forum';
export default function ForumContainer() {
const { data: posts, isLoading } = useSWR('/api/posts', fetcher);
return (
<ForumPage
posts={posts ?? []}
isLoading={isLoading}
onPostClick={id => router.push(`/forum/${id}`)}
onNewPost={() => router.push('/forum/create')}
onVote={(postId, vote) => fetch(`/api/posts/${postId}/vote`, {
method: 'POST',
body: JSON.stringify({ vote }),
})}
/>
);
}