@mounaji_npm/forum 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +445 -0
- package/dist/mounajiforum.es.js +1090 -0
- package/dist/mounajiforum.es.js.map +1 -0
- package/dist/mounajiforum.umd.cjs +2 -0
- package/dist/mounajiforum.umd.cjs.map +1 -0
- package/package.json +47 -0
- package/src/CreatePostPage.jsx +237 -0
- package/src/ForumPage.jsx +193 -0
- package/src/PostPage.jsx +122 -0
- package/src/components/CategoryNav.jsx +83 -0
- package/src/components/PostCard.jsx +186 -0
- package/src/components/PostDetail.jsx +89 -0
- package/src/components/ReplyThread.jsx +244 -0
- package/src/components/shared.jsx +171 -0
- package/src/demo.js +125 -0
- package/src/index.js +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# @mounaji_npm/forum
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @mounaji_npm/tokens @mounaji_npm/forum
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or scaffold a full forum project with the CLI (see `@mounaji_npm/cli`):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @mounaji_npm/cli create my-forum
|
|
17
|
+
# Choose template: 2. Forum / Community
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## What's included
|
|
23
|
+
|
|
24
|
+
| Export | Description |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `ForumPage` | Full listing page — category sidebar, search, sort, post list |
|
|
27
|
+
| `PostPage` | Post detail page — full content, vote column, reply thread |
|
|
28
|
+
| `CreatePostPage` | New post form — title, body, category chips, tags |
|
|
29
|
+
| `PostCard` | Single post card (used inside `ForumPage`) |
|
|
30
|
+
| `PostList` | List of `PostCard` with skeleton loading and empty state |
|
|
31
|
+
| `PostDetail` | Full post content block with vote column |
|
|
32
|
+
| `ReplyCard` | Single reply with vote + accepted-answer badge |
|
|
33
|
+
| `ReplyThread` | Sorted reply list + `ReplyComposer` below |
|
|
34
|
+
| `ReplyComposer` | Textarea + submit button for writing a reply |
|
|
35
|
+
| `CategoryNav` | Sidebar category list with counts |
|
|
36
|
+
| `VoteButton` | Up/downvote control with optimistic updates |
|
|
37
|
+
| `AuthorMeta` | Avatar + name + relative timestamp |
|
|
38
|
+
| `TagChip` | `#tag` pill |
|
|
39
|
+
| `CategoryBadge` | Colored category label badge |
|
|
40
|
+
| `DEMO_POSTS` | Demo post data for prototyping |
|
|
41
|
+
| `DEMO_CATEGORIES` | Demo category data |
|
|
42
|
+
| `DEMO_REPLIES` | Demo reply data |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick Start — Next.js App Router
|
|
47
|
+
|
|
48
|
+
### 1. Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install @mounaji_npm/tokens @mounaji_npm/forum
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Forum listing page
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
// app/forum/page.js
|
|
58
|
+
'use client';
|
|
59
|
+
import { useRouter } from 'next/navigation';
|
|
60
|
+
import { ForumPage } from '@mounaji_npm/forum';
|
|
61
|
+
|
|
62
|
+
const CATEGORIES = [
|
|
63
|
+
{ id: 'all', label: 'All Posts', icon: '◉', count: 42 },
|
|
64
|
+
{ id: 'general', label: 'General', icon: '💬', count: 18 },
|
|
65
|
+
{ id: 'questions',label: 'Questions', icon: '❓', count: 14 },
|
|
66
|
+
{ id: 'ideas', label: 'Ideas', icon: '💡', count: 10 },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export default function ForumListPage() {
|
|
70
|
+
const router = useRouter();
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<ForumPage
|
|
74
|
+
categories={CATEGORIES}
|
|
75
|
+
title="Community Forum"
|
|
76
|
+
subtitle="Ask questions, share ideas, and connect with others."
|
|
77
|
+
onPostClick={id => router.push(`/forum/${id}`)}
|
|
78
|
+
onNewPost={() => router.push('/forum/create')}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Post detail page
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
// app/forum/[id]/page.js
|
|
88
|
+
'use client';
|
|
89
|
+
import { useRouter } from 'next/navigation';
|
|
90
|
+
import { PostPage } from '@mounaji_npm/forum';
|
|
91
|
+
|
|
92
|
+
export default function PostDetailPage({ params }) {
|
|
93
|
+
const router = useRouter();
|
|
94
|
+
|
|
95
|
+
// Replace with your data fetching (SWR, React Query, fetch, etc.)
|
|
96
|
+
const post = usePost(params.id);
|
|
97
|
+
const replies = useReplies(params.id);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<PostPage
|
|
101
|
+
post={post}
|
|
102
|
+
replies={replies}
|
|
103
|
+
currentUser={{ id: 'u1', name: 'jane_doe' }}
|
|
104
|
+
onBack={() => router.push('/forum')}
|
|
105
|
+
onVotePost={(postId, vote) => api.votePost(postId, vote)}
|
|
106
|
+
onVoteReply={(replyId, vote) => api.voteReply(replyId, vote)}
|
|
107
|
+
onAccept={replyId => api.acceptReply(replyId)}
|
|
108
|
+
onSubmitReply={body => api.createReply(params.id, body)}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 4. Create post page
|
|
115
|
+
|
|
116
|
+
```jsx
|
|
117
|
+
// app/forum/create/page.js
|
|
118
|
+
'use client';
|
|
119
|
+
import { useRouter } from 'next/navigation';
|
|
120
|
+
import { CreatePostPage } from '@mounaji_npm/forum';
|
|
121
|
+
|
|
122
|
+
const CATEGORIES = [
|
|
123
|
+
{ id: 'general', label: 'General', icon: '💬' },
|
|
124
|
+
{ id: 'questions', label: 'Questions', icon: '❓' },
|
|
125
|
+
{ id: 'ideas', label: 'Ideas', icon: '💡' },
|
|
126
|
+
{ id: 'bugs', label: 'Bug Reports', icon: '🐛' },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
export default function CreatePage() {
|
|
130
|
+
const router = useRouter();
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<CreatePostPage
|
|
134
|
+
categories={CATEGORIES}
|
|
135
|
+
currentUser={{ id: 'u1', name: 'jane_doe' }}
|
|
136
|
+
onSubmit={async (post) => {
|
|
137
|
+
await api.createPost(post);
|
|
138
|
+
router.push('/forum');
|
|
139
|
+
}}
|
|
140
|
+
onCancel={() => router.push('/forum')}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Quick Start — Vite + React Router
|
|
149
|
+
|
|
150
|
+
```jsx
|
|
151
|
+
// src/App.jsx
|
|
152
|
+
import { BrowserRouter, Routes, Route, useNavigate, useParams } from 'react-router-dom';
|
|
153
|
+
import { TokensProvider } from '@mounaji_npm/tokens';
|
|
154
|
+
import { ForumPage, PostPage, CreatePostPage, DEMO_POSTS, DEMO_REPLIES } from '@mounaji_npm/forum';
|
|
155
|
+
|
|
156
|
+
function ForumRoute() {
|
|
157
|
+
const navigate = useNavigate();
|
|
158
|
+
return (
|
|
159
|
+
<ForumPage
|
|
160
|
+
onPostClick={id => navigate(`/forum/${id}`)}
|
|
161
|
+
onNewPost={() => navigate('/forum/create')}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function PostRoute() {
|
|
167
|
+
const { id } = useParams();
|
|
168
|
+
const navigate = useNavigate();
|
|
169
|
+
const post = DEMO_POSTS.find(p => p.id === id) ?? DEMO_POSTS[0];
|
|
170
|
+
return (
|
|
171
|
+
<PostPage
|
|
172
|
+
post={post}
|
|
173
|
+
replies={DEMO_REPLIES}
|
|
174
|
+
onBack={() => navigate('/forum')}
|
|
175
|
+
onSubmitReply={body => console.log('new reply:', body)}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function CreateRoute() {
|
|
181
|
+
const navigate = useNavigate();
|
|
182
|
+
return (
|
|
183
|
+
<CreatePostPage
|
|
184
|
+
onSubmit={post => { console.log('new post:', post); navigate('/forum'); }}
|
|
185
|
+
onCancel={() => navigate('/forum')}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default function App() {
|
|
191
|
+
return (
|
|
192
|
+
<TokensProvider>
|
|
193
|
+
<BrowserRouter>
|
|
194
|
+
<Routes>
|
|
195
|
+
<Route path="/forum" element={<ForumRoute />} />
|
|
196
|
+
<Route path="/forum/:id" element={<PostRoute />} />
|
|
197
|
+
<Route path="/forum/create" element={<CreateRoute />} />
|
|
198
|
+
</Routes>
|
|
199
|
+
</BrowserRouter>
|
|
200
|
+
</TokensProvider>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Using demo data for prototyping
|
|
208
|
+
|
|
209
|
+
All three page components ship with demo data as default props. If you just want to see the UI without wiring any backend:
|
|
210
|
+
|
|
211
|
+
```jsx
|
|
212
|
+
import { ForumPage, PostPage, CreatePostPage } from '@mounaji_npm/forum';
|
|
213
|
+
|
|
214
|
+
// Renders immediately with demo posts, categories, and replies
|
|
215
|
+
<ForumPage />
|
|
216
|
+
<PostPage />
|
|
217
|
+
<CreatePostPage />
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
To inspect or seed a database with the demo data:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import { DEMO_POSTS, DEMO_CATEGORIES, DEMO_REPLIES } from '@mounaji_npm/forum';
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Component Reference
|
|
229
|
+
|
|
230
|
+
### `ForumPage`
|
|
231
|
+
|
|
232
|
+
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.
|
|
233
|
+
|
|
234
|
+
| Prop | Type | Default | Description |
|
|
235
|
+
|---|---|---|---|
|
|
236
|
+
| `categories` | `Category[]` | `DEMO_CATEGORIES` | Sidebar category list |
|
|
237
|
+
| `posts` | `Post[]` | `DEMO_POSTS` | Posts to display |
|
|
238
|
+
| `activeCategory` | string | `'all'` | Initially selected category id |
|
|
239
|
+
| `searchQuery` | string | `''` | Controlled search value |
|
|
240
|
+
| `sortBy` | `'newest'|'top'|'unanswered'` | `'newest'` | Initial sort |
|
|
241
|
+
| `onCategoryChange` | `(id) => void` | — | Called when category is clicked |
|
|
242
|
+
| `onSearch` | `(query) => void` | — | Called on search input change |
|
|
243
|
+
| `onSortChange` | `(sort) => void` | — | Called when sort tab changes |
|
|
244
|
+
| `onPostClick` | `(postId) => void` | — | Called when a post card is clicked |
|
|
245
|
+
| `onNewPost` | `() => void` | — | Called when "+ New Post" is clicked |
|
|
246
|
+
| `onVote` | `(postId, vote) => void` | — | Called when a post is voted |
|
|
247
|
+
| `isLoading` | boolean | `false` | Show skeleton cards |
|
|
248
|
+
| `isDark` | boolean | `true` | Dark / light theme |
|
|
249
|
+
| `title` | string | `'Forum'` | Page heading |
|
|
250
|
+
| `subtitle` | string | — | Subheading below title |
|
|
251
|
+
| `header` | ReactNode | — | Fully replaces the default header |
|
|
252
|
+
| `style` | CSSProperties | — | Wrapper style |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### `PostPage`
|
|
257
|
+
|
|
258
|
+
Post detail + replies. Defaults to `DEMO_POSTS[0]` and `DEMO_REPLIES` when no props are passed.
|
|
259
|
+
|
|
260
|
+
| Prop | Type | Default | Description |
|
|
261
|
+
|---|---|---|---|
|
|
262
|
+
| `post` | Post | `DEMO_POSTS[0]` | Post object |
|
|
263
|
+
| `replies` | `Reply[]` | `DEMO_REPLIES` | Reply list |
|
|
264
|
+
| `currentUser` | `{ id, name, avatar? }` | `null` | Logged-in user (used for accept-answer permission) |
|
|
265
|
+
| `onBack` | `() => void` | — | Called when "← Back to Forum" is clicked |
|
|
266
|
+
| `onVotePost` | `(postId, vote) => void` | — | Called when the post is voted |
|
|
267
|
+
| `onVoteReply` | `(replyId, vote) => void` | — | Called when a reply is voted |
|
|
268
|
+
| `onAccept` | `(replyId) => void` | — | Called when a reply is marked as accepted |
|
|
269
|
+
| `onSubmitReply` | `(body) => void \| Promise` | — | Called when reply form is submitted |
|
|
270
|
+
| `isLoading` | boolean | `false` | Show post skeleton |
|
|
271
|
+
| `isDark` | boolean | `true` | Theme |
|
|
272
|
+
| `style` | CSSProperties | — | Wrapper style |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### `CreatePostPage`
|
|
277
|
+
|
|
278
|
+
New post form with title, body (textarea), category selector, and tags input.
|
|
279
|
+
|
|
280
|
+
| Prop | Type | Default | Description |
|
|
281
|
+
|---|---|---|---|
|
|
282
|
+
| `categories` | `Category[]` | `DEMO_CATEGORIES` (without `all`) | Category options shown as chips |
|
|
283
|
+
| `currentUser` | `{ name, avatar? }` | `null` | Shown as "Posting as …" hint |
|
|
284
|
+
| `onSubmit` | `(post) => void \| Promise` | — | Called with `{ title, body, categoryId, tags }` |
|
|
285
|
+
| `onCancel` | `() => void` | — | Called when Cancel is clicked |
|
|
286
|
+
| `isDark` | boolean | `true` | Theme |
|
|
287
|
+
| `style` | CSSProperties | — | Wrapper style |
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### `VoteButton`
|
|
292
|
+
|
|
293
|
+
Up/downvote control with optimistic local state.
|
|
294
|
+
|
|
295
|
+
```jsx
|
|
296
|
+
import { VoteButton } from '@mounaji_npm/forum';
|
|
297
|
+
|
|
298
|
+
<VoteButton
|
|
299
|
+
count={42}
|
|
300
|
+
userVote={1} // 1 = upvoted, -1 = downvoted, 0 = no vote
|
|
301
|
+
onChange={v => api.vote(v)}
|
|
302
|
+
vertical // stacks up/count/down vertically (default: false)
|
|
303
|
+
size="sm" // 'sm' | 'md'
|
|
304
|
+
isDark
|
|
305
|
+
/>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### `CategoryNav`
|
|
311
|
+
|
|
312
|
+
Standalone sidebar component — use it outside `ForumPage` in a custom layout.
|
|
313
|
+
|
|
314
|
+
```jsx
|
|
315
|
+
import { CategoryNav } from '@mounaji_npm/forum';
|
|
316
|
+
|
|
317
|
+
<CategoryNav
|
|
318
|
+
categories={CATEGORIES}
|
|
319
|
+
active="questions"
|
|
320
|
+
onChange={id => setActive(id)}
|
|
321
|
+
title="Browse"
|
|
322
|
+
isDark
|
|
323
|
+
/>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### `ReplyComposer`
|
|
329
|
+
|
|
330
|
+
Standalone reply textarea — use it in a custom post layout.
|
|
331
|
+
|
|
332
|
+
```jsx
|
|
333
|
+
import { ReplyComposer } from '@mounaji_npm/forum';
|
|
334
|
+
|
|
335
|
+
<ReplyComposer
|
|
336
|
+
currentUser={{ name: 'jane_doe' }}
|
|
337
|
+
onSubmit={async body => { await api.createReply(postId, body); }}
|
|
338
|
+
placeholder="Share your thoughts…"
|
|
339
|
+
isDark
|
|
340
|
+
/>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Data Shapes
|
|
346
|
+
|
|
347
|
+
### Post
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
{
|
|
351
|
+
id: string,
|
|
352
|
+
title: string,
|
|
353
|
+
body: string,
|
|
354
|
+
author: { id: string, name: string, avatar?: string },
|
|
355
|
+
category: { id: string, label: string, icon?: string },
|
|
356
|
+
tags: string[],
|
|
357
|
+
votes: number,
|
|
358
|
+
userVote: 1 | 0 | -1, // current user's vote (default 0)
|
|
359
|
+
replyCount: number,
|
|
360
|
+
viewCount: number,
|
|
361
|
+
createdAt: string | Date,
|
|
362
|
+
updatedAt?: string | Date,
|
|
363
|
+
isPinned?: boolean,
|
|
364
|
+
isSolved?: boolean,
|
|
365
|
+
isLocked?: boolean,
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Reply
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
{
|
|
373
|
+
id: string,
|
|
374
|
+
body: string,
|
|
375
|
+
author: { id: string, name: string, avatar?: string },
|
|
376
|
+
votes: number,
|
|
377
|
+
userVote: 1 | 0 | -1,
|
|
378
|
+
createdAt: string | Date,
|
|
379
|
+
isAccepted?: boolean,
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Category
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
{
|
|
387
|
+
id: string,
|
|
388
|
+
label: string,
|
|
389
|
+
icon?: string, // emoji
|
|
390
|
+
count?: number, // shown in sidebar
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Theming
|
|
397
|
+
|
|
398
|
+
All components read from `@mounaji_npm/tokens` CSS variables. Override at the `TokensProvider` level:
|
|
399
|
+
|
|
400
|
+
```jsx
|
|
401
|
+
import { TokensProvider } from '@mounaji_npm/tokens';
|
|
402
|
+
import { ForumPage } from '@mounaji_npm/forum';
|
|
403
|
+
|
|
404
|
+
<TokensProvider initialTokens={{
|
|
405
|
+
colorPrimary: '#7C3AED',
|
|
406
|
+
colorAccent: '#06B6D4',
|
|
407
|
+
radiusLg: '1rem',
|
|
408
|
+
}}>
|
|
409
|
+
<ForumPage ... />
|
|
410
|
+
</TokensProvider>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Switch to light mode by passing `isDark={false}` to any page:
|
|
414
|
+
|
|
415
|
+
```jsx
|
|
416
|
+
<ForumPage isDark={false} ... />
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Connecting a real backend
|
|
422
|
+
|
|
423
|
+
The forum components are headless — they accept data and fire callbacks. Plug in any data layer:
|
|
424
|
+
|
|
425
|
+
```jsx
|
|
426
|
+
import useSWR from 'swr';
|
|
427
|
+
import { ForumPage } from '@mounaji_npm/forum';
|
|
428
|
+
|
|
429
|
+
export default function ForumContainer() {
|
|
430
|
+
const { data: posts, isLoading } = useSWR('/api/posts', fetcher);
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<ForumPage
|
|
434
|
+
posts={posts ?? []}
|
|
435
|
+
isLoading={isLoading}
|
|
436
|
+
onPostClick={id => router.push(`/forum/${id}`)}
|
|
437
|
+
onNewPost={() => router.push('/forum/create')}
|
|
438
|
+
onVote={(postId, vote) => fetch(`/api/posts/${postId}/vote`, {
|
|
439
|
+
method: 'POST',
|
|
440
|
+
body: JSON.stringify({ vote }),
|
|
441
|
+
})}
|
|
442
|
+
/>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
```
|