npm package: @pamfilico/feedback
Description: Feedback components for Material UI and other frameworks
Latest version: 2.1.9

# @pamfilico/feedback

A pure React feedback component library for Material UI. Works with any React framework (Next.js, Vite, Create React App, Remix, etc.).

## Installation

### All React Projects (18 or 19)

```bash
npm install @pamfilico/feedback
npm install react-canvas-draw
```

**Note:** Starting with v2.1.0, `react-canvas-draw` is no longer bundled and must be installed separately in your project. This gives you more control over versioning and allows you to apply any necessary patches for React 19 compatibility.

For React 19 projects, you may need to install `react-canvas-draw` with the `--legacy-peer-deps` flag:

```bash
npm install react-canvas-draw --legacy-peer-deps
```

### Migrating from v2.0.x to v2.1.0

Version 2.1.0 removes Next.js as a dependency, making this a pure React library:

**What changed:**
- Removed Next.js dependency (no longer required)
- Removed `react-canvas-draw` from package dependencies (install separately)
- Replaced Next.js-specific code with standard React patterns

**Action required:**
1. Install `react-canvas-draw` in your project: `npm install react-canvas-draw`
2. If using React 19, add `--legacy-peer-deps` flag when installing react-canvas-draw
3. No code changes needed in your application - the API remains exactly the same

## Quick Start

### Material UI Feedback Button

```tsx
import { MaterialFeedbackButton } from "@pamfilico/feedback/material";

function App() {
  return (
    <MaterialFeedbackButton
      meta={{ user_email: "user@example.com", visitor_id: "abc123" }}
      apiBasePath="/api/v1/feedback"
      additionalHeaders={{ "Authorization": "Bearer token" }}
    />
  );
}
```

### Examples

#### Basic Usage (Drawer Variant)
```tsx
import { MaterialFeedbackButton } from "@pamfilico/feedback/material";

export default function MyApp() {
  return (
    <>
      {/* Your app content */}
      <MaterialFeedbackButton
        meta={{ user_email: "user@example.com" }}
        apiBasePath="/api/v1/feedback"
      />
    </>
  );
}
```

#### Dialog Variant (Centered Form)
If you prefer a centered dialog form instead of a right-side drawer:

```tsx
<MaterialFeedbackButton
  meta={{ user_email: "user@example.com" }}
  apiBasePath="/api/v1/feedback"
  formAsDialog={true}  // Shows form as centered dialog
/>
```

#### With Custom Metadata
Pass any custom data you want to include with feedback submissions:

```tsx
<MaterialFeedbackButton
  meta={{
    user_email: session?.user?.email,
    user_id: session?.user?.id,
    visitor_id: analytics.visitorId,
    company_id: user?.companyId,
    subscription_tier: "pro",
    custom_field: "any value"
  }}
  apiBasePath="/api/v1/feedback"
  additionalHeaders={{
    "Authorization": `Bearer ${token}`,
    "X-Custom-Header": "value"
  }}
/>
```

#### Conditional Display
```tsx
<MaterialFeedbackButton
  meta={{ user_email: user?.email }}
  apiBasePath="/api/v1/feedback"
  hideIfNoMeta={true}  // Only show if meta is provided
/>
```

#### With App ID Tracking
The `appId` prop is automatically merged into the meta object:

```tsx
<MaterialFeedbackButton
  meta={{ user_email: "user@example.com", visitor_id: "xyz789" }}
  apiBasePath="/api/v1/feedback"
  appId="my-app-production"  // Automatically added to meta as app_id
/>
```

#### Next.js App Router Example
```tsx
// app/layout.tsx
import { MaterialFeedbackButton } from "@pamfilico/feedback/material";
import { auth } from "@/lib/auth";

export default async function RootLayout({ children }) {
  const session = await auth();

  return (
    <html>
      <body>
        {children}
        <MaterialFeedbackButton
          meta={{
            user_email: session?.user?.email,
            user_id: session?.user?.id,
            user_name: session?.user?.name
          }}
          apiBasePath="/api/v1/feedback"
          formAsDialog={true}
        />
      </body>
    </html>
  );
}
```

#### Button Placement

Position the feedback button anywhere on screen:

```tsx
// Bottom positions
<MaterialFeedbackButton placement="bottom-right" />  // Default
<MaterialFeedbackButton placement="bottom-left" />
<MaterialFeedbackButton placement="bottom-center" />

// Top positions
<MaterialFeedbackButton placement="top-right" />
<MaterialFeedbackButton placement="top-left" />
<MaterialFeedbackButton placement="top-center" />

// Side positions (rotated vertically)
<MaterialFeedbackButton placement="right-middle" />
<MaterialFeedbackButton placement="left-middle" />

// Parent positioning - use as inline button
<div className="my-toolbar">
  <button>Save</button>
  <button>Cancel</button>
  <MaterialFeedbackButton placement="parent" userEmail="user@example.com" />
</div>
```

**Features:**
- The `parent` placement makes the button follow normal document flow instead of being fixed, perfect for toolbars or inline usage
- Side placements (`right-middle`, `left-middle`) are automatically rotated 90° for vertical display
- On small screens (< 600px), only the icon is shown to save space

#### Button Color Customization

Customize the button color to match your app's theme:

```tsx
// Red (default)
<MaterialFeedbackButton color="error" userEmail="user@example.com" />

// Blue
<MaterialFeedbackButton color="primary" userEmail="user@example.com" />

// Green
<MaterialFeedbackButton color="success" userEmail="user@example.com" />

// Orange
<MaterialFeedbackButton color="warning" userEmail="user@example.com" />

// Purple
<MaterialFeedbackButton color="secondary" userEmail="user@example.com" />

// Light Blue
<MaterialFeedbackButton color="info" userEmail="user@example.com" />
```

#### Internationalization (i18n)

The component supports multiple languages out of the box. All UI text, form labels, validation messages, and notifications are translatable:

```tsx
// English (default)
<MaterialFeedbackButton
  userEmail="user@example.com"
  locale="en"
/>

// Greek (Ελληνικά)
<MaterialFeedbackButton
  userEmail="user@example.com"
  locale="el"
/>
```

**Supported Languages:**
- `en` - English (default)
- `el` - Greek (Ελληνικά)

**What gets translated:**
- Button text
- Form labels and placeholders
- Validation error messages
- Feedback type options (Bug, Feature Request, Other)
- Drawing tool buttons (Reset, Undo, Close)
- Success/error notifications
- Screen size labels (Mobile, Tablet, Desktop)
- All help text and instructions

**Adding new languages:**

To add support for additional languages:

1. Create a new translation file in `src/locales/[language-code].json`
2. Copy the structure from `en.json` and translate all values
3. Update the `Locale` type in `src/locales/index.ts` to include your language code
4. Import and add your translations to the `translations` object

Example for Spanish (`es`):
```typescript
// src/locales/index.ts
import enTranslations from './en.json';
import elTranslations from './el.json';
import esTranslations from './es.json'; // Your new translation

export type Locale = 'en' | 'el' | 'es'; // Add 'es'

const translations: Record<Locale, Translations> = {
  en: enTranslations,
  el: elTranslations,
  es: esTranslations, // Add your translations
};
```

## Components

### MaterialFeedbackButton

A floating feedback button that opens a fullscreen dialog for creating feedback. When clicked, it automatically captures a screenshot of the current page and opens a dialog where users can draw annotations on the screenshot and submit their feedback.

![Feedback Submission Example](https://raw.githubusercontent.com/pamfilico/feedback/master/image.png)

**Key Features:**
- 📸 **Automatic screenshot capture** - Captures the entire page when the button is clicked
- ✏️ **Interactive annotation canvas** - Draw directly on the screenshot with customizable brush
- 🎨 **Drawing tools** - Clear canvas, undo last stroke, brush color/size controls
- 📝 **Feedback form** - Type selection (bug/feature/other), description field
- 📱 **Device detection** - Automatically detects and stores device type (mobile/tablet/desktop)
- 🔄 **Manual upload fallback** - If auto-capture fails, users can upload their own screenshot
- 🌐 **URL tracking** - Automatically captures the current URL where feedback was submitted
- 🎨 **Customizable button color** - Choose from error, primary, secondary, success, info, or warning colors
- 🌍 **Internationalization (i18n)** - Built-in support for multiple languages (English and Greek included)
- 📱 **Mobile-optimized UI** - Separate mobile and desktop components with optimized layouts

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `meta` | `Record<string, any> \| null` | `null` | Custom metadata object to include with feedback (e.g., user info, analytics IDs, etc.) |
| `apiBasePath` | `string` | `"/api/feedback"` | API endpoint for feedback submission |
| `additionalHeaders` | `Record<string, string>` | `{}` | Additional headers for API requests |
| `hideIfNoMeta` | `boolean` | `false` | Hide button if no meta provided |
| `appId` | `string` | `undefined` | Application identifier - automatically merged into meta as `app_id` |
| `formAsDialog` | `boolean` | `false` | Show form as centered dialog instead of drawer (desktop only) |
| `placement` | `'bottom-right' \| 'bottom-left' \| 'bottom-center' \| 'top-right' \| 'top-left' \| 'top-center' \| 'right-middle' \| 'left-middle' \| 'parent'` | `'bottom-right'` | Button position on screen. Side positions are rotated vertically. Use 'parent' for inline positioning |
| `color` | `'error' \| 'primary' \| 'secondary' \| 'success' \| 'info' \| 'warning'` | `'error'` | Button color theme (red, blue, purple, green, light blue, orange) |
| `locale` | `string` | `'en'` | Language locale for UI text. Supports 'en' (English) and 'el' (Greek). Falls back to 'en' for invalid values |

#### Submission Data Schema

When a user submits feedback, the component sends a POST request to your API endpoint with the following data structure:

```typescript
{
  feedbackType: "bug" | "feature" | "other";  // Type of feedback (camelCase for compatibility)
  description: string;                 // User's feedback description
  image: string;                       // Base64 encoded screenshot image (data:image/png;base64,...)
  drawings: {                         // Canvas drawing data for annotations
    lines: Array<{
      points: Array<{x: number, y: number}>;
      brushColor: string;
      brushRadius: number;
    }>;
    width: number;                    // Canvas width
    height: number;                   // Canvas height
  } | null;
  current_url: string;                 // URL where feedback was submitted
  material_ui_screensize: "mobile" | "tablet" | "desktop";  // Device type
  meta: Record<string, any>;           // Custom metadata (includes app_id if provided)
}
```

**Example Submission:**
```json
{
  "feedbackType": "bug",
  "description": "The submit button is not working on the checkout page",
  "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
  "drawings": {
    "lines": [
      {
        "points": [{"x": 100, "y": 150}, {"x": 102, "y": 152}],
        "brushColor": "#ff0000",
        "brushRadius": 2
      }
    ],
    "width": 1920,
    "height": 1080
  },
  "current_url": "https://example.com/checkout",
  "material_ui_screensize": "desktop",
  "meta": {
    "user_email": "user@example.com",
    "user_id": "123",
    "visitor_id": "abc-def-ghi",
    "app_id": "my-app-production"
  }
}
```

---

### FeedbackPageComponent

A complete feedback list page with inline editing capabilities. Displays user feedback items with pagination and opens a fullscreen dialog for editing.

```tsx
import { FeedbackPageComponent } from "@pamfilico/feedback/material";

function FeedbackPage() {
  return (
    <FeedbackPageComponent
      fetchFeedbacksUrl="https://api.example.com/api/v1/feedback"
      editingUrl="https://api.example.com/api/v1/feedback"  // Optional: for edit operations
      additionalHeaders={{ "EPICWORK-TOKEN": token }}
      onClickEditButtonFeedbackItem={(id) => console.log("Editing:", id)}
    />
  );
}
```

**Key Points:**
- `fetchFeedbacksUrl`: Used for fetching the paginated list. Pagination params (`page`, `limit`) are automatically appended.
- `editingUrl`: Used for GET/PUT operations on individual feedback items. The `feedbackId` is automatically appended (e.g., `${editingUrl}/${feedbackId}`).

#### Props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `fetchFeedbacksUrl` | `string` | Yes | Full URL for fetching paginated feedback list. Pagination params (page, limit) are automatically appended as query parameters |
| `editingUrl` | `string` | No | Base URL for edit operations. feedbackId is automatically appended for GET/PUT requests (e.g., `${editingUrl}/${feedbackId}`). Optional if you don't need inline editing |
| `additionalHeaders` | `Record<string, string>` | No | Authentication headers for API requests |
| `onClickEditButtonFeedbackItem` | `(feedbackId: string) => void` | No | Optional callback when edit button is clicked |

#### Features
- 📋 Paginated feedback list
- ✏️ Inline editing with fullscreen dialog
- 🔄 Auto-refresh after updates
- 📱 Device type indicators (mobile/tablet/desktop)
- 🎨 Type-based color coding (bug/feature/other)
- 🖼️ Screenshot previews with annotation support

---

### FeedbackEditPageComponent

A wrapper component that handles fetching feedback data and rendering the appropriate edit component (Desktop or Mobile) based on the original submission device.

```tsx
import {
  FeedbackEditPageComponent,
  DesktopEditFeedbackComponent,
  MobileEditFeedbackComponent
} from "@pamfilico/feedback/material";

function EditFeedback({ feedbackId }) {
  return (
    <FeedbackEditPageComponent
      editingUrl="https://api.example.com/api/v1/feedback"
      feedbackId={feedbackId}
      additionalHeaders={{ "EPICWORK-TOKEN": token }}
      onUpdate={(id) => router.push("/feedback")}
      onCancel={() => router.push("/feedback")}
      desktopComponent={DesktopEditFeedbackComponent}
      mobileComponent={MobileEditFeedbackComponent}
    />
  );
}
```

**Note:** The `feedbackId` is automatically appended to `editingUrl` for GET/PUT requests.

#### Props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `editingUrl` | `string` | Yes | Base URL for edit operations. feedbackId is automatically appended (e.g., `${editingUrl}/${feedbackId}`) |
| `feedbackId` | `string` | Yes | ID of feedback to edit |
| `additionalHeaders` | `Record<string, string>` | No | Authentication headers |
| `onUpdate` | `(feedbackId: string) => void` | No | Callback when feedback is updated |
| `onCancel` | `() => void` | No | Callback when editing is cancelled |
| `desktopComponent` | `React.ComponentType` | Yes | Desktop edit component |
| `mobileComponent` | `React.ComponentType` | Yes | Mobile edit component |

---

### DesktopEditFeedbackComponent

Edit component optimized for desktop/tablet devices with side-by-side screenshot annotation and form.

#### Props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `feedback` | `any` | Yes | Feedback object to edit |
| `editingUrl` | `string` | Yes | Base URL for edit operations. feedbackId is automatically appended from feedback.id |
| `additionalHeaders` | `Record<string, string>` | No | Authentication headers |
| `onUpdate` | `(feedbackId: string) => void` | No | Callback after successful update |
| `onCancel` | `() => void` | No | Callback when cancelled |

---

### MobileEditFeedbackComponent

Edit component optimized for mobile devices with stacked screenshot and form layout.

#### Props

Same as `DesktopEditFeedbackComponent`.

---

## API Requirements

All feedback components expect your backend API to implement these endpoints:

### POST `/api/v1/feedback`
**Used by:** MaterialFeedbackButton

Creates a new feedback submission.

**Request Body:**
```typescript
{
  user_email: string | null;          // Optional - user's email
  feedbackType: "bug" | "feature" | "other";  // Optional - type of feedback
  description: string;                // Optional - feedback description
  image: string;                      // Optional - base64 data URL or S3 URL
  current_url: string;                // Optional - URL where feedback submitted
  drawings: {                         // Optional - drawing annotation data
    lines: Array<{
      points: Array<{x: number, y: number}>;
      brushColor: string;
      brushRadius: number;
    }>;
    width: number;
    height: number;
  } | null;
  material_ui_screensize: "mobile" | "tablet" | "desktop";  // Optional - device type
  softwarefast_task_id: string;      // Optional - external task tracker ID
}
```

**Expected Response:**
```json
{
  "success": true,
  "message": "Feedback received!",
  "data": {
    "id": "uuid-string",
    "user_id": "uuid-string",
    "type_of": "bug",
    "message": "Description text",
    "created_at": "2025-01-01T12:00:00"
  }
}
```

---

### GET `{fetchFeedbacksUrl}?page=1&limit=20`
**Used by:** FeedbackPageComponent

Returns paginated list of feedback items. The component automatically appends `page` and `limit` query parameters to the `fetchFeedbacksUrl` prop. Additional query parameters (like `user_id`) can be included directly in the `fetchFeedbacksUrl` string.

**Response:**
```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "uuid-1",
        "user_id": "user-uuid",
        "type_of": "bug",
        "message": "The button doesn't work",
        "image": "https://storage.example.com/feedback/screenshot.png",
        "drawings": {
          "lines": [...],
          "width": 1920,
          "height": 1080
        },
        "current_url": "https://example.com/page",
        "material_ui_screensize": "desktop",
        "softwarefast_task_id": "TASK-123",  // Optional: external task tracking ID
        "created_at": "2025-10-07T10:00:00Z",
        "last_updated": "2025-10-07T10:30:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total_count": 50,
      "total_pages": 3,
      "has_next": true,
      "has_prev": false
    }
  }
}
```

**Feedback Item Schema:**
```typescript
{
  id: string;                        // Unique feedback identifier
  user_id: string | null;            // User who submitted (can be null)
  type_of: "bug" | "feature" | "other";
  message: string;                   // Feedback description
  image: string | null;              // URL to stored screenshot (not base64)
  drawings: {                        // Drawing annotation data
    lines: Array<{
      points: Array<{x: number, y: number}>;
      brushColor: string;
      brushRadius: number;
    }>;
    width: number;
    height: number;
  } | null;
  current_url: string | null;       // URL where feedback was submitted
  material_ui_screensize: "mobile" | "tablet" | "desktop" | null;
  softwarefast_task_id: string | null;  // Optional external tracking
  created_at: string;                // ISO 8601 timestamp
  last_updated: string;              // ISO 8601 timestamp
}
```

---

### GET `{editingUrl}/{feedbackId}`
**Used by:** FeedbackEditPageComponent

Returns a single feedback item for editing. The component automatically constructs the URL by appending the feedbackId to the editingUrl prop.

**Example Request:** `GET https://api.example.com/api/v1/feedback/uuid-1`

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "uuid-1",
    "user_id": "user-uuid",
    "type_of": "bug",
    "message": "The button doesn't work",
    "image": "https://storage.example.com/feedback/screenshot.png",
    "drawings": {
      "lines": [...],
      "width": 1920,
      "height": 1080
    },
    "current_url": "https://example.com/page",
    "material_ui_screensize": "desktop",
    "softwarefast_task_id": null,
    "created_at": "2025-10-07T10:00:00Z",
    "last_updated": "2025-10-07T10:00:00Z"
  }
}
```

---

### PUT `{editingUrl}/{feedbackId}`
**Used by:** DesktopEditFeedbackComponent, MobileEditFeedbackComponent

Updates an existing feedback item. The component automatically constructs the URL by appending feedback.id to the editingUrl prop.

**Example Request:** `PUT https://api.example.com/api/v1/feedback/uuid-1`

**Request Body:**
```json
{
  "feedbackType": "bug",
  "description": "Updated description with more details",
  "image": "data:image/png;base64,iVBORw0KGg...",  // Can be base64 data URL or existing URL
  "drawings": {
    "lines": [
      {
        "points": [{"x": 100, "y": 150}],
        "brushColor": "#ff0000",
        "brushRadius": 2
      }
    ],
    "width": 1920,
    "height": 1080
  }
}
```

**Expected Response:**
```json
{
  "success": true,
  "data": {
    "id": "uuid-1",
    "message": "Feedback updated successfully"
  }
}
```

---

### Storage Recommendations

**Screenshot Storage:**
- When receiving `image` field from MaterialFeedbackButton (base64 data URL), decode and store the image in your cloud storage (S3, Cloud Storage, etc.)
- Store the public URL in the `image` field of your database
- Return the URL (not base64) in GET responses to reduce payload size
- When updating feedback, accept either base64 data URLs or existing URLs in the `image` field

**Drawings Storage:**
- Store the `drawings` object as JSON in your database
- This allows the edit components to reload and continue editing annotations
- The drawings data is relatively small (~1-5KB per feedback item)

## Features

- 📸 Automatic screenshot capture
- ✏️ Drawing annotations on screenshots
- 📱 Responsive design with device detection
- 🎨 Material UI themed
- 🔄 Manual upload fallback
- 🌐 Customizable API endpoint
- 📋 Complete feedback management UI
- 🔐 Flexible authentication header support
- 📄 Pagination support
- 🖼️ Screenshot preview and editing

## License

MIT