@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 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
+ ```