Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 57 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,26 @@ import {
} from '@kendew-agency/react-feedback-layer';

function App() {
const handleConfirm = async (comments) => {
const handleConfirm = async (comments, projectId) => {
// Save comments to your database
await saveToDatabase(comments);
await saveToDatabase(comments, projectId);
};

const handleResolve = async (comments) => {
// Mark comments as resolved in your database
await resolveInDatabase(comments);
};

const handleError = (error) => {
console.error('Comment error:', error.code, error.message);
};

return (
<CommentContextProvider
currentUser={{ name: 'John Doe', id: 'user-123' }}
onConfirm={handleConfirm}
onResolve={handleResolve}
onError={handleError}
initialComments={[]}
>
<CommentOverlay>
Expand Down Expand Up @@ -132,13 +137,13 @@ interface CommentOverlayProps {
};

// Callback when user confirms draft comments
onConfirm: (comments: ConfirmedComment[]) => Promise<void>;
onConfirm: (comments: ConfirmedComment[], projectId?: string) => Promise<void>;

// Callback when user resolves comments
onResolve: (comments: ConfirmedComment[]) => Promise<void>;

// Callback to handle errors (optional)
onError?: (error: KnownError) => void;
// Callback to handle errors
onError: (error: KnownError) => void;

// Optional configuration for the comment layer
config?: {
Expand All @@ -152,6 +157,9 @@ interface CommentOverlayProps {
hideResolving?: boolean;
};
};

// Optional project ID to attach to all comments in this overlay
projectId?: string;
}
```

Expand All @@ -161,8 +169,9 @@ interface CommentOverlayProps {
<CommentContextProvider
currentUser={{ name: 'Jane Smith', id: '456', avatar: '/avatar.jpg' }}
initialComments={existingComments}
onConfirm={async (comments) => {
await api.createComments(comments);
projectId="proj_123456789"
onConfirm={async (comments, projectId) => {
await api.createComments(comments, projectId);
}}
onResolve={async (comments) => {
await api.resolveComments(comments);
Expand Down Expand Up @@ -472,6 +481,7 @@ interface CommentContext {
confirmComments: () => Promise<{ error: Error | DOMException | null }>;
resolveComments: () => Promise<{ error: Error | DOMException | null }>;
updateCommentVisibility: (visibility: Partial<CommentVisibility>) => void;
triggerError: (error: KnownError) => void;
}
```

Expand Down Expand Up @@ -570,6 +580,7 @@ type CommentType = {
resolvedAt?: Date;
status: "draft" | "published" | "resolving" | "resolved";
indicator?: Indicator | null;
projectId?: string;
};
```

Expand Down Expand Up @@ -675,10 +686,10 @@ function App() {
return (
<CommentContextProvider
currentUser={{ name: 'John Doe', id: '123' }}
onConfirm={async (comments) => {
onConfirm={async (comments, projectId) => {
const response = await fetch('/api/comments', {
method: 'POST',
body: JSON.stringify(comments),
body: JSON.stringify({ comments, projectId }),
});
if (!response.ok) {
throw new Error('Failed to save comments');
Expand Down Expand Up @@ -749,6 +760,7 @@ function App() {
subscription={subscription}
onConfirm={saveComments}
onResolve={resolveComments}
onError={(error) => console.error(error)}
>
{/* ... */}
</CommentContextProvider>
Expand Down Expand Up @@ -919,6 +931,27 @@ function VisibilityControls() {

## Advanced Patterns

### Project ID

Attach a project ID to all comments created within an overlay. This is useful for multi-project setups where you need to associate comments with a specific project in your database.

```tsx
<CommentContextProvider
currentUser={currentUser}
projectId="proj_123456789"
onConfirm={async (comments, projectId) => {
// projectId is passed through to your callback
await api.createComments(comments, projectId);
}}
onResolve={resolveComments}
onError={handleError}
>
{children}
</CommentContextProvider>
```

Each comment created within this provider will have the `projectId` field set, which you can use for filtering and organizing comments per project.

### Programmatic Comment Creation

```tsx
Expand Down Expand Up @@ -948,19 +981,18 @@ function CustomTool() {

### Error Handling

You can handle errors in two ways:
The `onError` callback is required and handles all error types, including overlay interaction errors like attempting to add a comment while editing or dismissing an empty comment.

**1. Using the `onError` callback (recommended):**
**1. Using the `onError` callback:**

```tsx
<CommentContextProvider
currentUser={currentUser}
onConfirm={saveComments}
onResolve={resolveComments}
onError={(error) => {
console.error('Comment operation failed:', error);
// Show user-friendly error message
toast.error('Failed to save comments. Please try again.');
console.error('Comment operation failed:', error.code, error.message);
toast.error(error.message);
}}
>
{children}
Expand Down Expand Up @@ -1006,7 +1038,10 @@ switch(e.code){
break
case "CONFIRM_ERROR":
alert('Failed to submit your comments')
break
break
case "INSERT_ERROR":
alert('Failed to insert your comment')
break
default:
alert('An unknown error occured')
}
Expand Down Expand Up @@ -1176,6 +1211,13 @@ MIT © [Kendew Agency](https://github.com/Kendew-Agency)

A list if breaking changes that could impact the way you configured the package

### 0.3.0
- Added `projectId` prop to `CommentContextProvider`. When set, the project ID is attached to every new comment and passed as a second argument to the `onConfirm` callback. Update your `onConfirm` signature from `(comments) => ...` to `(comments, projectId?) => ...`.
- `onError` is now a required prop on `CommentContextProvider`. If you previously omitted it, you must provide an error handler.
- Added `InsertError` error class and `INSERT_ERROR` error code. The overlay now triggers `onError` instead of using `alert()` when a user tries to add a comment while editing or dismisses an empty comment.
- Added `triggerError` method to the comment context (available via `useComments()`), allowing programmatic error triggering through the `onError` callback.
- `CommentType` now includes an optional `projectId` field.

### 0.2.0
- Reworked the subscription system. The system remains in beta and may change in the future. Configurations made with version 0.1.2 or older will need adjustment after updating.
- `mode` was defined twice in props. It has been removed as a root prop and is now only part of the config.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kendew-agency/react-feedback-layer",
"version": "0.2.2",
"version": "0.3.0",
"description": "Drop-in React feedback layer for collecting contextual user feedback",
"license": "MIT",
"author": "Kendew Agency",
Expand Down
1 change: 1 addition & 0 deletions playground/features/comments/comment-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const CommentWrapper = ({ children }: { children: ReactNode }) => {
currentUser={{
name: "John Doe",
}}
projectId="proj_1234"
onResolve={resolveCommentsInDataBase}
onError={(e) => {
console.error(e);
Expand Down
6 changes: 5 additions & 1 deletion playground/features/comments/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { ConfirmedComment } from "../../../src/types";

// Mocks server code
export async function sendCommentsToDataBase(comments: ConfirmedComment[]) {
export async function sendCommentsToDataBase(
comments: ConfirmedComment[],
projectId?: string,
) {
console.info("Project ID:", projectId);
console.info("Adding comments:", comments);
}

Expand Down
18 changes: 15 additions & 3 deletions src/components/comment-overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
type CSSProperties,
HTMLProps,
type HTMLProps,
type ReactNode,
useRef,
useState,
} from "react";
import { useComments } from "../contexts/comment-context";
import type { Position } from "../types";
import { getRelativePos, normalizeRect } from "../utils/position";
import { InsertError } from "../errors";

const DRAG_THRESHOLD = 4;

Expand All @@ -22,6 +23,7 @@ export const CommentOverlay = ({ children, ...rest }: CommentOverlayProps) => {
focusOnComment,
getActiveComment,
changeOverlayState,
triggerError,
} = useComments();
const [preview, setPreview] = useState<null | {
x: number;
Expand All @@ -36,8 +38,10 @@ export const CommentOverlay = ({ children, ...rest }: CommentOverlayProps) => {

const onPointerDown = (e: React.PointerEvent) => {
if (overlayState === "editing") {
alert(
"Can not add a new comment. You are currently editing a comment. Please finish editing before adding a new one.",
triggerError(
new InsertError(
"You are editing a comment, please finish editing the current comment first",
),
);
return;
}
Expand Down Expand Up @@ -136,6 +140,14 @@ export const CommentOverlay = ({ children, ...rest }: CommentOverlayProps) => {
<div
data-comment-guard
onClick={() => {
if (getActiveComment()?.content === "") {
triggerError(
new InsertError(
"Comment is empty, either delete the comment or add some content",
),
);
return;
}
focusOnComment(null);
changeOverlayState("idle");
}}
Expand Down
13 changes: 13 additions & 0 deletions src/contexts/comment-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
CommentVisibility,
Indicator,
Position,
KnownError,
} from "../types";
import { tx } from "../lib/tx";
import { hasStatus } from "../utils/hasStatus";
Expand All @@ -34,6 +35,7 @@ const commentReducer = (
...state.comments,
{
id: newId,
projectId: state.projectId,
user: state.currentUser,
position: {
x: action.position.x,
Expand Down Expand Up @@ -160,6 +162,7 @@ export const CommentContextProvider = ({
hideResolving: false,
},
},
projectId,
onError,
}: CommentOverlayProps) => {
const [state, dispatch] = useReducer(commentReducer, {
Expand All @@ -168,6 +171,7 @@ export const CommentContextProvider = ({
currentUser,
focussedComment: null,
config,
projectId,
} satisfies CommentState);

// Replace the initial comments with the comments from the listen query
Expand Down Expand Up @@ -277,6 +281,7 @@ export const CommentContextProvider = ({
...c,
status: "published",
})),
projectId,
),
);

Expand Down Expand Up @@ -324,6 +329,12 @@ export const CommentContextProvider = ({
dispatch({ type: "CHANGE_OVERLAYSTATE", to: state });
};

const triggerError = (error: KnownError) => {
if (onError) {
onError(error);
}
};

return (
<CommentContext.Provider
value={{
Expand All @@ -347,6 +358,8 @@ export const CommentContextProvider = ({
currentUser,
resolveComments,
updateCommentVisibility,

triggerError,
}}
>
{children}
Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export class UnknownError extends BaseError {
}
}

export class InsertError extends BaseError {
constructor(message = "An error occured while inserting your comments") {
super(message, "INSERT_ERROR", 500);
}
}

export class ResolveError extends BaseError {
constructor(message = "An error occured while resolving your comments") {
super(message, "RESOLVE_ERROR", 500);
Expand Down
7 changes: 7 additions & 0 deletions src/types/comment.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { KnownError } from "./error.types";
import type {
CommentOverlayState,
CommentVisibility,
Expand Down Expand Up @@ -40,6 +41,11 @@ export type CommentType = {
* @description shows a box around the content a comment is tied to
*/
indicator?: Indicator | null;
/**
* The id of the project a comment is connected to
* @description useful for filtering comments by project
*/
projectId?: string | undefined;
};

export type Status = "draft" | "published" | "resolving" | "resolved";
Expand Down Expand Up @@ -113,6 +119,7 @@ export type CommentContext = {
error: Error | DOMException | null;
}>;
updateCommentVisibility: (visibility: Partial<CommentVisibility>) => void;
triggerError: (error: KnownError) => void;
};

export type CommentWithStatus<T extends Status> = Omit<
Expand Down
3 changes: 2 additions & 1 deletion src/types/error.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ConfirmError, ResolveError } from "../errors";
export type OverlayErrorCode =
| "RESOLVE_ERROR"
| "CONFIRM_ERROR"
| "UNKNOWN_ERROR";
| "UNKNOWN_ERROR"
| "INSERT_ERROR";

export type KnownError = ConfirmError | ResolveError;
Loading
Loading