p1-feedback
v0.2.0
Published
Vercel-style visual feedback widget for Next.js applications
Maintainers
Readme
p1-feedback
A Vercel-style visual feedback widget for Next.js applications. Allows visitors to click anywhere on a page to leave comments, which are stored in MongoDB and linked to deployment versions.
Features
- Click anywhere to leave comments with precise page positioning
- Comments stay at exact location on the page (scroll with content)
- Resolve/reopen comments with status filtering (All/Open/Resolved)
- Threaded replies on comments
- Draggable comment modal
- Comments linked to deployment versions (outdated comments are visually marked)
- Optional name/email for commenters
- Shadow DOM isolation (no style conflicts)
- Unique 3-character ID displayed on each pin
- Accent color: #F9A325 (customizable)
- Admin API for managing comments
Installation
npm install p1-feedbackQuick Start
1. Set up environment variables
Create a .env.local file in your Next.js project:
# MongoDB connection string
MONGODB_URI=mongodb+srv://username:[email protected]/feedback
# Feedback widget configuration
NEXT_PUBLIC_FEEDBACK_PROJECT_ID=my-project
NEXT_PUBLIC_FEEDBACK_ENABLED=true
# Admin API key (optional, for admin endpoints)
FEEDBACK_ADMIN_API_KEY=your-secret-admin-key
# These are automatically available on Vercel:
# VERCEL_DEPLOYMENT_ID
# VERCEL_GIT_COMMIT_SHA
# VERCEL_GIT_COMMIT_REF
# VERCEL_URL2. Copy API routes to your project
Copy the API routes from the package to your Next.js project:
your-project/
└── src/
└── app/
└── api/
├── comments/
│ ├── route.ts
│ └── [id]/
│ └── route.ts
└── admin/
└── comments/
└── route.tsYou can find these files in the p1-feedback repository.
3. Wrap your app with FeedbackProvider
In your root layout (app/layout.tsx):
import { FeedbackProvider } from 'p1-feedback'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<FeedbackProvider
projectId={process.env.NEXT_PUBLIC_FEEDBACK_PROJECT_ID!}
enabled={process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true'}
deployment={{
id: process.env.VERCEL_DEPLOYMENT_ID,
commitSha: process.env.VERCEL_GIT_COMMIT_SHA,
commitRef: process.env.VERCEL_GIT_COMMIT_REF,
url: process.env.VERCEL_URL,
}}
>
{children}
</FeedbackProvider>
</body>
</html>
)
}Configuration Options
FeedbackProvider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| projectId | string | required | Unique identifier for your project |
| enabled | boolean | true | Enable/disable the widget |
| apiUrl | string | '/api' | Base URL for the API endpoints |
| deployment | object | {} | Deployment info for version tracking |
| position | string | 'bottom-right' | Button position: 'bottom-right', 'bottom-left', 'top-right', 'top-left' |
Deployment Object
{
id?: string; // VERCEL_DEPLOYMENT_ID
commitSha?: string; // VERCEL_GIT_COMMIT_SHA
commitRef?: string; // VERCEL_GIT_COMMIT_REF
url?: string; // VERCEL_URL
}Usage
- Click the floating button in the corner to enter comment mode
- Click anywhere on the page to place a comment (crosshair cursor appears)
- A pin appears at the clicked location with a draggable comment form
- Fill in your comment (name and email are optional)
- Click "Post Comment"
Comment Management
- Filter comments: Use the filter bar (All/Open/Resolved) above the floating button
- Reply to comments: Click a pin, then click "Reply" to add threaded replies
- Resolve comments: Click "Resolve" on any open comment to mark it as resolved
- Drag modal: Grab the comment modal header to move it around the screen
Comments from previous deployments will show an "Old" badge and gray pin.
API Endpoints
Public Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/comments?projectId=X&pageUrl=Y | Get comments for a page |
| POST | /api/comments | Create a new comment |
| PATCH | /api/comments/[id] | Update a comment |
| DELETE | /api/comments/[id] | Delete a comment |
Admin Endpoints
Requires X-Admin-Key header matching FEEDBACK_ADMIN_API_KEY.
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/admin/comments?projectId=X | List all comments (paginated) |
| PATCH | /api/admin/comments | Bulk resolve/delete |
| DELETE | /api/admin/comments?id=X | Delete a comment |
Hooks
useFeedback
Access the feedback context from any component:
import { useFeedback } from 'p1-feedback'
function MyComponent() {
const {
comments,
isLoading,
isCommentMode,
setCommentMode,
refreshComments,
updateComment,
statusFilter,
setStatusFilter,
} = useFeedback()
// Filter by status
const openComments = comments.filter(c => c.status === 'open')
// Resolve a comment
const handleResolve = (id: string) => {
updateComment(id, { status: 'resolved' })
}
return (
<div>
<p>{openComments.length} open comments</p>
<button onClick={() => setCommentMode(true)}>
Add Comment
</button>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as 'all' | 'open' | 'resolved')}
>
<option value="all">All</option>
<option value="open">Open</option>
<option value="resolved">Resolved</option>
</select>
</div>
)
}useFeedback Context Values
| Property | Type | Description |
|----------|------|-------------|
| comments | CommentWithReplies[] | All comments for current page |
| isLoading | boolean | Loading state |
| isCommentMode | boolean | Whether comment mode is active |
| setCommentMode | (mode: boolean) => void | Toggle comment mode |
| refreshComments | () => Promise<void> | Reload comments from server |
| addComment | (input) => Promise<Comment> | Create a new comment |
| updateComment | (id, input) => Promise<boolean> | Update comment (e.g., resolve) |
| statusFilter | 'all' \| 'open' \| 'resolved' | Current filter |
| setStatusFilter | (filter) => void | Change status filter |
MongoDB Schema
Comments are stored with the following structure:
{
_id: ObjectId,
projectId: string,
pageUrl: string,
pageTitle?: string,
position: {
xPercent: number,
yPercent: number,
selector?: string,
selectorOffset?: { x: number, y: number },
viewportWidth: number,
viewportHeight: number,
},
content: string,
authorName?: string,
authorEmail?: string,
parentId?: ObjectId,
threadId: ObjectId,
replyCount: number,
deployment: {
id?: string,
commitSha?: string,
commitRef?: string,
url?: string,
},
status: 'open' | 'resolved',
createdAt: Date,
updatedAt: Date,
}Indexes
For optimal performance, create these indexes:
db.comments.createIndex({ projectId: 1, pageUrl: 1, status: 1 })
db.comments.createIndex({ projectId: 1, "deployment.id": 1 })
db.comments.createIndex({ threadId: 1 })
db.comments.createIndex({ createdAt: -1 })Cross-Origin Usage
If your API is hosted on a different domain, set the apiUrl prop:
<FeedbackProvider
projectId="my-project"
apiUrl="https://feedback-api.example.com/api"
>
{children}
</FeedbackProvider>The API includes CORS headers for cross-origin requests.
License
MIT
