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

@neonwatty/feedback-board

v0.1.0

Published

Embeddable feedback board for Next.js + Supabase apps

Readme

@neonwatty/feedback-board

Embeddable feedback board for Next.js + Supabase apps. Collect feature requests, let users upvote, and track status on a public roadmap.

npm version license

Installation

npm install @neonwatty/feedback-board

Peer dependencies

| Package | Version | | ----------------------- | ------- | | react | >= 18 | | react-dom | >= 18 | | next | >= 14 | | @supabase/supabase-js | ^2.0.0 | | tailwindcss | >= 4 |

Quick Start

Step 1: Database

Run the shipped migration in your Supabase SQL Editor:

-- Copy the contents of:
-- node_modules/@neonwatty/feedback-board/migrations/001_initial_schema.sql

Or import it directly:

import migration from "@neonwatty/feedback-board/migrations/001_initial_schema.sql";

This creates:

  • Tables: boards, posts, votes, comments
  • RLS policies: public read, authenticated write, owner-only update/delete
  • Triggers: auto-increment vote_count, auto-update updated_at
  • Indexes: on board_id, status, vote_count, created_at, post_id, user_id

You also need to enable at least one OAuth provider in Supabase Auth (e.g. GitHub, Google) so users can sign in to create posts and vote.

Step 2: Tailwind CSS

Two config lines are needed:

In your CSS file (e.g. app/globals.css) add a @source directive so Tailwind scans the package's compiled classes:

@import "tailwindcss";
@source "../node_modules/@neonwatty/feedback-board/dist";

The @source path is relative to your CSS file. Adjust if your project structure differs.

In next.config.ts add the package to transpilePackages:

const nextConfig: NextConfig = {
  transpilePackages: ["@neonwatty/feedback-board"],
};

Step 3: Server Actions

Next.js requires "use server" directives to be in files within your app — they cannot be imported from node_modules. The solution is to create thin server-action wrappers that call the package's pure functions.

Create lib/feedback-actions.ts:

"use server";

import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import {
  createBoardAction,
  createPostAction,
  toggleVoteAction,
  updatePostStatusAction,
  createCommentAction,
} from "@neonwatty/feedback-board/actions";
import type { PostStatus } from "@neonwatty/feedback-board";

export async function createBoard(formData: FormData) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  const result = await createBoardAction(supabase, user.id, formData);
  if (!result.error) revalidatePath("/");
  return result;
}

export async function createPost(formData: FormData) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  const result = await createPostAction(supabase, user.id, formData);
  if (result.slug) revalidatePath(`/board/${result.slug}`);
  return result;
}

export async function toggleVote(postId: string, boardSlug: string) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) return { error: "Must be logged in to vote." };

  const result = await toggleVoteAction(supabase, user.id, postId, boardSlug);
  revalidatePath(`/board/${boardSlug}`);
  return result;
}

export async function updatePostStatus(postId: string, status: PostStatus, boardSlug: string) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) return { error: "Must be logged in." };

  const result = await updatePostStatusAction(supabase, user.id, postId, status, boardSlug);
  revalidatePath(`/board/${boardSlug}`);
  return result;
}

export async function createComment(formData: FormData) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  const boardSlug = formData.get("board_slug") as string;
  const postId = formData.get("post_id") as string;

  const result = await createCommentAction(supabase, user.id, formData);
  revalidatePath(`/board/${boardSlug}/post/${postId}`);
  return result;
}

Key: import pure functions from @neonwatty/feedback-board/actions (not the main entry). The main entry has a "use client" banner — importing from it in a server file will fail.

Step 4: Provider

Create a client-side provider wrapper that passes the server actions to the feedback board:

// components/feedback-provider-wrapper.tsx
"use client";

import { FeedbackBoardProvider } from "@neonwatty/feedback-board";
import type { FeedbackUser } from "@neonwatty/feedback-board";
import { createBoard, createPost, toggleVote, updatePostStatus, createComment } from "@/lib/feedback-actions";

const actions = {
  createBoard,
  createPost,
  toggleVote,
  updatePostStatus,
  createComment,
};

export function FeedbackProviderWrapper({ user, children }: { user: FeedbackUser | null; children: React.ReactNode }) {
  return (
    <FeedbackBoardProvider actions={actions} user={user} basePath="/feedback" loginPath="/login">
      {children}
    </FeedbackBoardProvider>
  );
}

Then wrap your layout:

// app/layout.tsx (or a nested layout)
import { FeedbackProviderWrapper } from "@/components/feedback-provider-wrapper";

export default async function Layout({ children }) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  const feedbackUser = user ? { id: user.id, email: user.email } : null;

  return <FeedbackProviderWrapper user={feedbackUser}>{children}</FeedbackProviderWrapper>;
}

Components

| Component | Key Props | Description | | ------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------ | | BoardCard | board: BoardWithPostCount, className? | Board summary card with name, description, and post count | | CreateBoardDialog | className? | Dialog for creating a new board with name and description fields | | PostCard | post: PostWithVoteStatus, boardSlug, isLoggedIn, className? | Post card with vote button, title, status badge, and comment count | | PostList | posts: PostWithVoteStatus[], boardSlug, isLoggedIn, className? | Sortable list of posts (by votes, newest, oldest) | | PostForm | boardId, className? | Form for creating a new post | | VoteButton | postId, boardSlug, voteCount, hasVoted, isLoggedIn, className? | Upvote button with optimistic updates | | PostStatusSelect | postId, currentStatus: PostStatus, boardSlug, className? | Dropdown to update post status (admin) | | CommentForm | postId, boardSlug, className? | Form for submitting comments | | CommentList | comments: CommentWithAuthor[], className? | List of comments with author and timestamp | | RoadmapBoard | posts: Post[], boardSlug, className? | Kanban board showing posts grouped by status | | StatusBadge | status: PostStatus, className?, statusClassNames? | Colored badge displaying post status | | FeedbackLink | href, + all Next.js Link props | Link that auto-prepends the configured basePath |

All components also export from the main entry (@neonwatty/feedback-board).

Theming

Every component accepts a className prop for custom styling.

StatusBadge additionally accepts statusClassNames — a partial record mapping each PostStatus to a CSS class:

<StatusBadge status="planned" statusClassNames={{ planned: "bg-purple-100 text-purple-800" }} />

The defaultStatusConfig export provides the built-in label and class for each status, useful for building custom status UIs:

import { defaultStatusConfig } from "@neonwatty/feedback-board";

const config = defaultStatusConfig["in_progress"];
// { label: "In Progress", className: "bg-blue-100 text-blue-800 ..." }

Exports Reference

The package has two entry points and a migrations subpath:

| Import Path | Contains | "use client"? | | ---------------------------------------- | ------------------------------------------------- | --------------- | | @neonwatty/feedback-board | Components, Provider, types, cn utility | Yes | | @neonwatty/feedback-board/actions | Pure action functions (createBoardAction, etc.) | No | | @neonwatty/feedback-board/migrations/* | SQL migration files | N/A |

Why the split? The main entry has a "use client" banner so React components work in Next.js. If server-side code imported from this entry, all exports would be tainted as client-only. The ./actions subpath has no banner, keeping the pure functions callable from server actions.

FormData Fields

Actions that accept FormData expect these fields:

| Action | Fields | | --------------------- | ---------------------------------- | | createBoardAction | name, description | | createPostAction | board_id, title, description | | createCommentAction | post_id, body |

toggleVoteAction and updatePostStatusAction take positional arguments instead of FormData.

Testing

See examples/integration-test-example.tsx for a complete integration test template covering:

  • renderWithProvider helper with mock actions and user
  • createMockSupabase for testing pure action functions
  • Component rendering tests
  • Action function tests with mocked Supabase

Prerequisites: vitest, @testing-library/react, @testing-library/user-event

License

MIT