diff --git a/README.md b/README.md index d742852a..d3d6c09e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Enable the features you need and keep building your product. | **OpenAPI** | Auto-generated API documentation with interactive Scalar UI | | **Route Docs** | Auto-generated client route documentation with interactive navigation | | **Better Auth UI** | Beautiful shadcn/ui authentication components for better-auth | +| **Comments** | Commenting system with moderation, likes, and nested replies | Each plugin ships **frontend + backend together**: routes, APIs, database models, React components, SSR, and SEO — already wired. diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index fafb0aff..244fb555 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -124,3 +124,9 @@ Because the CLI executes your config file to extract the `dbSchema`, there are a ```bash SOME_VAR=value npx @btst/cli generate --config=lib/stack.ts --orm=prisma --output=schema.prisma ``` + +or using dotenv-cli: + +```bash +npx dotenv-cli -e .env.local -- npx @btst/cli generate --orm drizzle --config lib/stack.ts --output db/btst-schema.ts +``` \ No newline at end of file diff --git a/docs/content/docs/installation.mdx b/docs/content/docs/installation.mdx index 35db7142..8449717f 100644 --- a/docs/content/docs/installation.mdx +++ b/docs/content/docs/installation.mdx @@ -354,6 +354,7 @@ In order to use BTST, your application must meet the following requirements: export const GET = handler export const POST = handler export const PUT = handler + export const PATCH = handler export const DELETE = handler ``` @@ -390,6 +391,9 @@ In order to use BTST, your application must meet the following requirements: PUT: async ({ request }) => { return handler(request) }, + PATCH: async ({ request }) => { + return handler(request) + }, DELETE: async ({ request }) => { return handler(request) }, diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json index dddd0005..02f60719 100644 --- a/docs/content/docs/meta.json +++ b/docs/content/docs/meta.json @@ -17,6 +17,7 @@ "plugins/form-builder", "plugins/ui-builder", "plugins/kanban", + "plugins/comments", "plugins/open-api", "plugins/route-docs", "plugins/better-auth-ui", diff --git a/docs/content/docs/plugins/blog.mdx b/docs/content/docs/plugins/blog.mdx index aa8894b0..12dd6cdf 100644 --- a/docs/content/docs/plugins/blog.mdx +++ b/docs/content/docs/plugins/blog.mdx @@ -509,6 +509,32 @@ overrides={{ }} ``` +**Slot overrides:** + +| Override | Type | Description | +|----------|------|-------------| +| `postBottomSlot` | `(post: SerializedPost) => ReactNode` | Render additional content below each blog post — use to embed a `CommentThread` | + +```tsx +import { CommentThread } from "@btst/stack/plugins/comments/client/components" + +overrides={{ + blog: { + // ... + postBottomSlot: (post) => ( + + ), + } +}} +``` + ## React Data Hooks and Types You can import the hooks from `"@btst/stack/plugins/blog/client/hooks"` to use in your components. diff --git a/docs/content/docs/plugins/comments.mdx b/docs/content/docs/plugins/comments.mdx new file mode 100644 index 00000000..6b73f7d2 --- /dev/null +++ b/docs/content/docs/plugins/comments.mdx @@ -0,0 +1,536 @@ +--- +title: Comments Plugin +description: Threaded comments with moderation, likes, replies, and embeddable CommentThread component +--- + +import { Tabs, Tab } from "fumadocs-ui/components/tabs"; +import { Callout } from "fumadocs-ui/components/callout"; + +The Comments plugin adds threaded commenting to any resource in your application — blog posts, Kanban tasks, CMS content, or your own custom pages. Comments are displayed with the embeddable `CommentThread` component and managed via a built-in moderation dashboard. + +**Key Features:** +- **Threaded replies** — Top-level comments and nested replies +- **Like system** — One like per user, optimistic UI updates, denormalized counter +- **Edit support** — Authors can edit their own comments; an "edited" timestamp is shown +- **Moderation dashboard** — Tabbed view (Pending / Approved / Spam) with bulk actions +- **Server-side user resolution** — `resolveUser` hook to embed author name and avatar in API responses +- **Optimistic updates** — New comments appear instantly with a "Pending approval" badge when `autoApprove: false` +- **Scroll-into-view lazy loading** — `CommentThread` is mounted only when it scrolls into the viewport + +## Installation + + +Ensure you followed the general [framework installation guide](/installation) first. + + +### 1. Add Plugin to Backend API + +Register the comments backend plugin in your `stack.ts` file: + +```ts title="lib/stack.ts" +import { stack } from "@btst/stack" +import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api" + +const { handler, dbSchema } = stack({ + basePath: "/api/data", + plugins: { + comments: commentsBackendPlugin({ + // Automatically approve comments (default: false — requires moderation) + autoApprove: false, + + // Resolve author display name and avatar from your auth system + resolveUser: async (authorId) => { + const user = await db.users.findById(authorId) + return user + ? { name: user.displayName, avatarUrl: user.avatarUrl } + : null + }, + + // Lifecycle hooks — see Security section below for required configuration + onBeforeList: async (query, ctx) => { + // Restrict non-approved status filters (pending/spam) to admin sessions only + if (query.status && query.status !== "approved") { + const session = await getSession(ctx.headers) + if (!session?.user?.isAdmin) throw new Error("Admin access required") + } + }, + onBeforePost: async (comment, ctx) => { + // Required: resolve the authorId from the authenticated session + // Never use any ID supplied by the client + const session = await getSession(ctx.headers) + if (!session?.user) throw new Error("Authentication required") + return { authorId: session.user.id } + }, + onAfterPost: async (comment, ctx) => { + console.log("New comment posted:", comment.id) + }, + onBeforeEdit: async (commentId, update, ctx) => { + // Required: verify the caller owns the comment they are editing. + // Without this hook all edit requests return 403 by default. + const session = await getSession(ctx.headers) + if (!session?.user) throw new Error("Authentication required") + const comment = await db.comments.findById(commentId) + if (comment?.authorId !== session.user.id && !session.user.isAdmin) + throw new Error("Forbidden") + }, + onBeforeLike: async (commentId, authorId, ctx) => { + // Verify authorId matches the authenticated session + const session = await getSession(ctx.headers) + if (!session?.user) throw new Error("Authentication required") + if (authorId !== session.user.id) throw new Error("Forbidden") + }, + onBeforeStatusChange: async (commentId, status, ctx) => { + // Require admin/moderator role for the moderation endpoint + const session = await getSession(ctx.headers) + if (!session?.user?.isAdmin) throw new Error("Admin access required") + }, + onAfterApprove: async (comment, ctx) => { + // Send notification to comment author + await sendApprovalEmail(comment.authorId) + }, + onBeforeDelete: async (commentId, ctx) => { + // Require admin/moderator role — the Delete button is client-side only + const session = await getSession(ctx.headers) + if (!session?.user?.isAdmin) throw new Error("Admin access required") + }, + + // Required to show authors their own pending comments after posting. + // Without this hook the feature is disabled — client-supplied + // currentUserId is ignored server-side to prevent impersonation. + resolveCurrentUserId: async (ctx) => { + const session = await getSession(ctx.headers) + return session?.user?.id ?? null + }, + }) + }, + adapter: (db) => createMemoryAdapter(db)({}) +}) + +export { handler, dbSchema } +``` + +### 2. Add Plugin to Client + +Register the comments client plugin in your `stack-client.tsx` file: + +```tsx title="lib/stack-client.tsx" +import { createStackClient } from "@btst/stack/client" +import { commentsClientPlugin } from "@btst/stack/plugins/comments/client" +import { QueryClient } from "@tanstack/react-query" + +const getBaseURL = () => + process.env.BASE_URL || "http://localhost:3000" + +export const getStackClient = (queryClient: QueryClient) => { + const baseURL = getBaseURL() + return createStackClient({ + plugins: { + comments: commentsClientPlugin({ + queryClient, + siteBaseURL: baseURL, + siteBasePath: "/pages", + }), + }, + queryClient, + }) +} +``` + +### 3. Add CSS Import + + + +```css title="app/globals.css" +@import "@btst/stack/plugins/comments/css"; +``` + + +```css title="app/app.css" +@import "@btst/stack/plugins/comments/css"; +``` + + +```css title="src/styles/globals.css" +@import "@btst/stack/plugins/comments/css"; +``` + + + +### 4. Configure Overrides + +Add comments overrides to your layout file. You must also register the `CommentsPluginOverrides` type: + + + +```tsx title="app/pages/layout.tsx" +import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" + +type PluginOverrides = { + // ... existing plugins + comments: CommentsPluginOverrides +} + +// Inside your StackProvider overrides: +overrides={{ + comments: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + + // Access control for admin routes + onBeforeModerationPageRendered: async (context) => { + const session = await getSession() + if (!session?.user?.isAdmin) throw new Error("Admin access required") + }, + } +}} +``` + + +```tsx title="app/routes/pages/_layout.tsx" +import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" + +type PluginOverrides = { + // ... existing plugins + comments: CommentsPluginOverrides +} + +// Inside your StackProvider overrides: +overrides={{ + comments: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + onBeforeModerationPageRendered: async (context) => { + const session = await getSession() + if (!session?.user?.isAdmin) throw new Error("Admin access required") + }, + } +}} +``` + + +```tsx title="src/routes/pages/route.tsx" +import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" + +type PluginOverrides = { + // ... existing plugins + comments: CommentsPluginOverrides +} + +// Inside your StackProvider overrides: +overrides={{ + comments: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + onBeforeModerationPageRendered: async (context) => { + const session = await getSession() + if (!session?.user?.isAdmin) throw new Error("Admin access required") + }, + } +}} +``` + + + +## Embedding Comments + +The `CommentThread` component can be embedded anywhere — below a blog post, inside a Kanban task dialog, or on a custom page. + +```tsx +import { CommentThread } from "@btst/stack/plugins/comments/client/components" + + +``` + +### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `resourceId` | `string` | ✓ | Identifier for the resource (e.g. post slug, task ID) | +| `resourceType` | `string` | ✓ | Type of resource (`"blog-post"`, `"kanban-task"`, etc.) | +| `apiBaseURL` | `string` | ✓ | Base URL for API requests | +| `apiBasePath` | `string` | ✓ | Path prefix where the API is mounted | +| `currentUserId` | `string` | — | Authenticated user ID — enables edit/delete/pending badge | +| `loginHref` | `string` | — | Login page URL shown to unauthenticated users | +| `pageSize` | `number` | — | Comments per page. Falls back to `defaultCommentPageSize` from overrides, then 100. A "Load more" button appears when there are additional pages. | +| `components.Input` | `ComponentType` | — | Custom input component (default: `