npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mounaji_npm/forum

v0.1.0

Published

Modular forum and posts system — PostCard, PostList, PostDetail, ReplyThread, CategoryNav, and full page scaffolds

Downloads

27

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/forum

Or scaffold a full forum project with the CLI (see @mounaji_npm/cli):

npx @mounaji_npm/cli create my-forum
# Choose template: 2. Forum / Community

What'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/forum

2. 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 }),
      })}
    />
  );
}