npm package: @igamingcareer/igaming-components
Description: Reusable React component library for the iGamingCareer platform
Author: iGamingCareer
Latest version: 1.2.16

# iGaming Components

A reusable React component library for iGamingCareer that can be consumed from React apps or embedded in plain HTML via data attributes. The project is built with Vite and ships Tailwind-powered styles.

## Local development

1. Install dependencies (Node 16+ is recommended):

   ```bash
   npm install
   ```

2. Start the Vite dev server:

   ```bash
   npm run dev
   ```

3. Build the distributable assets:

   ```bash
   npm run build
   # or generate the library bundle and type declarations
   npm run build:lib
   ```


## Component preview hub in `index.html`

To avoid commenting/uncommenting mount nodes, `index.html` now includes a preview selector (`#component-selector`) that shows one preview component at a time.

### How it works

- The selector writes the selected component into the URL query param: `?component=<mount-id>`.
- `index.html` keeps all preview mount nodes in the DOM and toggles visibility so only the selected one is visible.
- `src/main.tsx` reads the same query param and only mounts the selected preview component with `shouldRenderPreviewComponent(...)`.

### Add a new preview component

When you create a new preview (example: `dashboard-component`), you only need to add the mount node.
`main.tsx` now auto-registers previews from `data-preview-item`.

1. **Add the mount node in `index.html`**

   ```html
   <div id="dashboard-component" data-preview-item="true" data-preview-label="Dashboard component"></div>
   ```

2. **(Optional) Provide a custom label**

   - If `data-preview-label` is present, that text is used in the selector and generated nav link.
   - If omitted, the label is generated from the id (`dashboard-component` → `Dashboard Component`).

3. **Done**

   - The selector options are generated automatically.
   - Preview links are auto-generated into `.navbar-component[data-preview-links="true"]` with:
     - `class="navbar-link"`
     - `data-text="<label>"`
     - `data-url="/<component-id>"`

No manual option editing is required anymore.

### Useful URLs

- `http://localhost:5173/?component=dashboard-component`
- `http://localhost:5173/?component=onboarding-demo`

This lets you deep-link directly to a specific preview while keeping all mount nodes available in one page.

### In-page preview logs (no DevTools required)

- If the viewport shrinks below desktop, the logger is hidden automatically.
The preview page includes a **Show logs** button (bottom-right).

- Click **Show logs** to open an in-page console panel.
- The panel mirrors `console.log`, `console.info`, `console.warn`, and `console.error` events.
- Click **Hide logs** to collapse it, or **Clear** to reset entries.

This is useful for QA/demo sessions where you want to monitor runtime events without opening browser DevTools.

## Storybook

Storybook is configured with the Vite builder to explore components in isolation.

- Start Storybook locally:

  ```bash
  npm run storybook
  ```

- Generate the static Storybook site:

  ```bash
  npm run build-storybook
  ```

## Using the library in React

Install the package from npm (or link the local build), then import the components and shared styles:

```bash
npm install @igamingcareer/igaming-components
```

```tsx
import {
  Button,
  JobListings,
  ConsentBanner,
  HomePage,
} from "@igamingcareer/igaming-components";
import "@igamingcareer/igaming-components/styles/globals.css";

export function Example() {
  return (
    <div>
      <Button dataName="Apply now" />
      <JobListings />
      <ConsentBanner />
      <HomePage />
    </div>
  );
}
```

## JobsList filter configuration

`JobsList` now supports multi-select company/country/city filters, optional counts in filter labels, and top-level sorting for those filter options.

```tsx
<JobsList
  apiUrl={process.env.NEXT_PUBLIC_JOB_API_URL!}
  filterKeys={["company", "country", "city", "department"]}
  searchableFilters={["company", "country", "city", "department"]}
  filterDisplayConfig={{
    multiSelectKeys: ["company", "country", "city"], // default
    showOptionCounts: true,                           // default, renders "Malta (3)"
    optionSort: "count_desc",                        // "alphabetical" | "count_desc" | "count_asc"
  }}
/>
```

### `filterDisplayConfig`

- `multiSelectKeys`
  - Controls which dynamic filters render as multi-select checkbox groups.
  - Defaults to `["company", "country", "city"]`.
  - If you remove one of those keys, that filter falls back to the legacy single-select dropdown.

- `showOptionCounts`
  - Defaults to `true`.
  - When `true`, dynamic filters show the number of matching jobs next to each option.
  - Example: `Malta (3)`.
  - Set it to `false` only if you want to hide those counts.

- `optionSort`
  - Controls how dynamic filter options are ordered.
  - Supported values:
    - `"alphabetical"` (default)
    - `"count_desc"` — highest count first
    - `"count_asc"` — lowest count first

## Login (remember me identifier)

The `Login` and `LoginForm` components support a real “Remember me” experience by persisting the identifier (email/username)
separately from the checkbox state. When the user opts in, the identifier is stored in `localStorage`, and on the next visit the
identifier field is prefilled and focus moves to the password input. Passwords are never persisted.

```tsx
import { Login } from "@igamingcareer/igaming-components";
import "@igamingcareer/igaming-components/styles/globals.css";

export function LoginScreen() {
  return (
    <Login
      onSubmit={(identifier, password, rememberMe) => {
        // send credentials to your API
      }}
      rememberIdentifierPersistKey="igaming-auth-identifier"
      defaultPrefilledIdentifier="player@example.com"
    />
  );
}
```

**Behavior details**

- The checkbox state is persisted via `rememberMeStorageKey` (default: `"igaming-auth-remember-me"`).
- When `rememberMe` is true on submit, the identifier is stored at `rememberIdentifierPersistKey`. If `rememberMe` is false, the
  identifier is removed.
- On mount, if `rememberMe` is true and a stored identifier exists, the identifier is prefilled and the password input is focused.

## Notifications (global notification system)

The notification system provides a provider, hook, and global helpers to trigger messages from anywhere in your app.

### Setup

Wrap your application with the provider once (typically in your app/layout/root component):

```tsx
import {
  NotificationProvider,
} from "@igamingcareer/igaming-components";
import "@igamingcareer/igaming-components/styles/globals.css";

export function AppRoot({ children }: { children: React.ReactNode }) {
  return (
    <NotificationProvider placement="top-right">
      {children}
    </NotificationProvider>
  );
}
```

### Trigger notifications with the hook

```tsx
import { useNotification } from "@igamingcareer/igaming-components";

export function ProfileSaveButton() {
  const { notify } = useNotification();

  return (
    <button
      onClick={() =>
        notify({
          type: "success",
          title: "Profile updated",
          message: "Your changes were saved successfully.",
        })
      }
    >
      Save
    </button>
  );
}
```

### Trigger notifications globally

```tsx
import { notify } from "@igamingcareer/igaming-components";

export function handleLoginError() {
  notify({
    type: "error",
    title: "Login failed",
    message: "We couldn't sign you in. Please try again.",
    actions: [
      {
        label: "Retry",
        variant: "primary",
        onClick: () => console.log("retry login"),
      },
    ],
  });
}
```

### Use the NotificationTester in a consumer app

The `NotificationTester` is a ready-made panel for manually firing notifications. Mount it anywhere in your app (or inside a dedicated demo page) and wrap it with `NotificationProvider`.

```tsx
import React from "react";
import ReactDOM from "react-dom/client";
import {
  NotificationProvider,
  NotificationTester,
} from "@igamingcareer/igaming-components";
import "@igamingcareer/igaming-components/styles/globals.css";

const notificationTesterElement = document.getElementById("notification-tester");

if (notificationTesterElement) {
  ReactDOM.createRoot(notificationTesterElement).render(
    <NotificationProvider placement="top-right">
      <NotificationTester />
    </NotificationProvider>
  );
}
```

If you are still using the legacy React 17 API, the equivalent is:

```tsx
import React from "react";
import ReactDOM from "react-dom";
import {
  NotificationProvider,
  NotificationTester,
} from "@igamingcareer/igaming-components";
import "@igamingcareer/igaming-components/styles/globals.css";

const notificationTesterElement = document.getElementById("notification-tester");

if (notificationTesterElement) {
  ReactDOM.render(
    <NotificationProvider placement="top-right">
      <NotificationTester />
    </NotificationProvider>,
    notificationTesterElement
  );
}
```

### Configuration options

| Option | Type | Description |
| --- | --- | --- |
| `placement` | `NotificationPlacement` | Controls where the stack renders (`top-right`, `bottom-left`, `center`, etc.). |
| `duration` | `number` | Auto-dismiss time in ms (`0` or `Infinity` disables). |
| `persistent` | `boolean` | Keeps the message open until dismissed. |
| `actions` | `NotificationAction[]` | Buttons/links with callbacks (e.g., Retry). |
| `content` | `ReactNode` | Custom body content for advanced layouts. |
| `containerClassName` | `string` | Customize the stack container styling. |

### Recommended follow-up tasks

Create sub-issues for the notification epic so teams can track separate scopes:

1. Design exploration (visual styles and variants).
2. Implementation checklists (API ergonomics, accessibility, animation polish).
3. Documentation expansion (Storybook examples + README).
4. Next.js usage examples (app router and pages router snippets).

## Follow company callbacks (CompanyCard + CompanyDetail)

The company list/detail components are stateless and emit follow actions so your app can handle API calls. Use the follow props to control state and provide callbacks for follow/unfollow actions.

```tsx
import { useState } from "react";
import {
  CompanyCard,
  CompanyDetail,
  type CompanyFollowActionPayload,
} from "@igamingcareer/igaming-components";

export function FollowCompaniesExample({ company }: { company: Company }) {
  const [isFollowing, setIsFollowing] = useState(false);
  const [followLoading, setFollowLoading] = useState(false);

  const handleToggleFollow = async (companyId: string, nextIsFollowing: boolean) => {
    setFollowLoading(true);
    try {
      // call your API here
      // await api.followCompany({ companyId, follow: nextIsFollowing });
      setIsFollowing(nextIsFollowing);
    } finally {
      setFollowLoading(false);
    }
  };

  const handleFollowAction = (payload: CompanyFollowActionPayload) => {
    // optional: analytics or logging
    console.log("follow action", payload);
  };

  return (
    <>
      <CompanyCard
        company={{ ...company, isFollowing }}
        isFollowing={isFollowing}
        followLoading={followLoading}
        onToggleFollow={handleToggleFollow}
        onFollowAction={handleFollowAction}
      />

      <CompanyDetail
        company={{ ...company, isFollowing }}
        isFollowing={isFollowing}
        followLoading={followLoading}
        onToggleFollow={handleToggleFollow}
        onFollowAction={handleFollowAction}
      />
    </>
  );
}
```

## Company admin shell layout

The CompanyAdminShell component now renders the admin tabs internally. It handles tab routing, renders the built-in admin views, and surfaces update events so the parent can persist changes.

### Permission system

Permissions decide whether a tab is visible and whether fields render as editable inputs or read-only text. Use a boolean per section:

```ts
export type CompanyAdminPermissions = {
  canViewOverview?: boolean;
  canManageProducts?: boolean;
  canManagePeople?: boolean;
  canManageJobs?: boolean;
  canManageEvents?: boolean;
  canManageNews?: boolean;
  canManageSettings?: boolean;
};
```

### Update events

Each admin section emits update events so the parent can save to the backend and update local state.

```ts
import type { CompanyAdminUpdateEvent } from "@igamingcareer/igaming-components";

const handleUpdate = async (event: CompanyAdminUpdateEvent) => {
  console.log("update event", event);
  // call backend API
  // update local state based on event.type
};
```

### Example usage

```tsx
import { useState } from "react";
import {
  CompanyAdminShell,
  type AdminTab,
  type CompanyAdminPermissions,
  type CompanyAdminUpdateEvent,
} from "@igamingcareer/igaming-components";

export function CompanyAdminExample({ company }: { company: Company }) {
  const [activeTab, setActiveTab] = useState<AdminTab>("overview");
  const [isSaving, setIsSaving] = useState(false);

  const permissions: CompanyAdminPermissions = {
    canViewOverview: true,
    canManageProducts: true,
    canManagePeople: false,
    canManageJobs: true,
    canManageEvents: true,
    canManageNews: false,
    canManageSettings: true,
  };

  const handleUpdate = async (event: CompanyAdminUpdateEvent) => {
    setIsSaving(true);
    try {
      // call backend API
      console.log("update", event);
    } finally {
      setIsSaving(false);
    }
  };

  return (
    <CompanyAdminShell
      company={company}
      role="admin"
      permissions={permissions}
      activeTab={activeTab}
      onTabChange={setActiveTab}
      onUpdate={handleUpdate}
      isSaving={isSaving}
    />
  );
}
```

### CompanyAdminShell props

| Prop | Type | Description |
| --- | --- | --- |
| `company` | `Company` | Full company object used for header and admin sections. |
| `companyName` | `string` | Optional override for the display name shown in the header. |
| `companySlug` | `string` | Optional override for the slug used in the header. |
| `activeTab` | `AdminTab` | Current active tab key (controlled). |
| `visibleTabs` | `AdminTab[]` | Tabs that should appear in the admin UI (optional override). |
| `onTabChange` | `(tab: AdminTab) => void` | Called when a user switches tabs. |
| `role` | `CompanyAdminRole` | Role label shown in the header (defaults to `admin`). |
| `permissions` | `CompanyAdminPermissions` | Permission map for tabs and editability. |
| `companyMeta` | `CompanyAdminCompanyMeta` | Optional metadata for the header summary. |
| `summaryStats` | `CompanyAdminSummaryStat[]` | Optional summary stats for the header (derived from `company` when omitted). |
| `onUpdate` | `(event: CompanyAdminUpdateEvent) => void \| Promise<void>` | Optional update callback for child sections. |
| `isSaving` | `boolean` | Optional saving state for buttons. |

### Backend integration guide

1. Listen for `CompanyAdminUpdateEvent` from the active admin tab.
2. Call your backend endpoint for that section.
3. Merge the returned data into your local `company` state.
4. Provide the updated `company` back to the admin components.

### Component patterns

- Each admin section owns local `formState` and emits a full payload via `onUpdate`.
- When `canEdit` is `false`, fields render as read-only text and edit controls are hidden.
- Use `isSaving` to lock save buttons and show spinners.

### Best practices

- Use `onUpdate` to connect each tab to your backend and keep `company` state in sync.
- Use separate state slices for admin-only data (e.g., settings) if it is not part of the core company API.
- Guard any optional permissions with defaults so read-only views remain safe.

### Troubleshooting

- **Nothing renders:** ensure the active tab is included in `visibleTabs` and has permission enabled.
- **Save button disabled:** verify `isSaving` is `false` and `canEdit` is `true`.
- **Updates not persisting:** confirm your parent handler merges the payload back into company state.

## Dashboard tab routing events

The Dashboard component can emit a routing-friendly event whenever a tab is selected. Use it to keep your app router in sync (for example, pushing routes in Next.js). You can also pass an `activeTab` prop to control the active tab from outside, along with a `tabPaths` map so your app remains the source of truth for URL formats.

```tsx
import { useState } from "react";
import {
  Dashboard,
  type DashboardTab,
  type DashboardTabChangeEvent,
} from "@igamingcareer/igaming-components";

export function DashboardWithRouter({ data }: { data: DashboardData }) {
  const [activeTab, setActiveTab] = useState<DashboardTab>("overview");

  const handleTabChange = (event: DashboardTabChangeEvent) => {
    setActiveTab(event.tab);
    // Example: Next.js router
    // router.push(event.path ?? "/dashboard");
  };

  return (
    <Dashboard
      user={data.user}
      profileData={data.profile}
      alerts={data.alerts}
      snapshot={data.snapshot}
      feedItems={data.feedItems}
      onUpdateProfile={data.onUpdateProfile}
      activeTab={activeTab}
      tabPaths={{
        overview: "/dashboard",
        profile: "/dashboard/profile",
        jobs: "/dashboard/jobs",
        courses: "/dashboard/courses",
        news: "/dashboard/news",
        settings: "/dashboard/settings",
      }}
      onTabChange={handleTabChange}
    />
  );
}
```

## Company detail tab routing events

The CompanyDetail component can emit a routing-friendly event whenever a tab is selected, so your Next.js app remains the source of truth for URLs. Provide `activeTab`, `tabPaths`, and `onTabChange` to keep routing and UI in sync.

```tsx
import { useState } from "react";
import {
  CompanyDetail,
  type CompanyDetailTab,
  type CompanyDetailTabChangeEvent,
} from "@igamingcareer/igaming-components";

export function CompanyDetailWithRouter({ data }: { data: CompanyDetailData }) {
  const [activeTab, setActiveTab] = useState<CompanyDetailTab>("overview");

  const handleTabChange = (event: CompanyDetailTabChangeEvent) => {
    setActiveTab(event.tab);
    // Example: Next.js router
    // router.push(event.path ?? `/companies/${data.company.slug}`);
  };

  return (
    <CompanyDetail
      company={data.company}
      jobs={data.jobs}
      news={data.news}
      events={data.events}
      activeTab={activeTab}
      tabPaths={{
        overview: `/companies/${data.company.slug}`,
        products: `/companies/${data.company.slug}/products`,
        trust: `/companies/${data.company.slug}/trust`,
        people: `/companies/${data.company.slug}/people`,
        jobs: `/companies/${data.company.slug}/jobs`,
        news: `/companies/${data.company.slug}/news`,
        events: `/companies/${data.company.slug}/events`,
        insights: `/companies/${data.company.slug}/insights`,
      }}
      onTabChange={handleTabChange}
    />
  );
}
```

## Onboarding flow (Dashboard + OnboardingPopup)

The new `Onboarding` folder includes components and helpers for managing the full onboarding lifecycle:

- **Dashboard onboarding preview** (inside `DashboardOverview`)
  - Uses `DashboardOnboardingSection` to show a compact, actionable checklist.
  - Data is driven by `OnboardingStep[]`, `emailVerified`, `profileComplete`, and `onboardingProgress`.
- **Full onboarding modal**
  - `OnboardingPopup` renders `OnboardingModal` + `OnboardingDashboard` and manages state via `OnboardingProvider`.
  - Use it for a blocking or reminder-style onboarding flow.

### Key backend flags

Fetch the flags from your backend (for example `/api/auth/me`) and map them into steps using `mapBackendFlagsToSteps`:

```ts
export interface OnboardingBackendFlags {
  email_verified: boolean;
  email_verified_at?: string | null;
  profile_complete: boolean;
  onboarding_steps: OnboardingStepId[];
}
```

These flags define the onboarding state:

1. **Registration started** → `email_verified = false`, `profile_complete = false`.
2. **Email verified** → `email_verified = true`, `profile_complete = false` (profile step becomes `pending`).
3. **Onboarding completed** → `email_verified = true`, `profile_complete = true` (all steps `complete`).

### Dashboard integration (Next.js)

Pass onboarding data into `Dashboard` so the overview can render the compact onboarding section. Use the helper
functions to keep the UI in sync with your backend.

```tsx
import {
  Dashboard,
  type OnboardingBackendFlags,
  mapBackendFlagsToSteps,
  calculateOnboardingProgress,
} from "@igamingcareer/igaming-components";

export function DashboardPage({ data }: { data: DashboardData }) {
  const flags: OnboardingBackendFlags = data.onboardingFlags;
  const steps = mapBackendFlagsToSteps(flags);
  const progress = calculateOnboardingProgress(steps);

  const handleOnboardingAction = (actionId: string, stepId: string) => {
    // actionId values: "resend_verification" | "edit_profile" | custom
    // Your routing or API calls here (Next.js router, toast, etc.)
    console.log("onboarding action", actionId, stepId);
  };

  return (
    <Dashboard
      user={data.user}
      profileData={data.profile}
      alerts={data.alerts}
      snapshot={data.snapshot}
      feedItems={data.feedItems}
      onUpdateProfile={data.onUpdateProfile}
      onboardingSteps={steps}
      emailVerified={flags.email_verified}
      profileComplete={flags.profile_complete}
      onboardingProgress={progress}
      onOnboardingAction={handleOnboardingAction}
      onOpenOnboarding={() => console.log("open full onboarding modal")}
    />
  );
}
```

### Onboarding popup (full flow)

Wrap your app (or the page that needs onboarding) with `OnboardingProvider`. Trigger the modal via the context
API (`openModal`) when registration starts or when the user returns and still has pending steps.

```tsx
import { useEffect } from "react";
import {
  OnboardingProvider,
  OnboardingPopup,
  useOnboardingContext,
  type OnboardingBackendFlags,
} from "@igamingcareer/igaming-components";

function OnboardingGate({ flags }: { flags: OnboardingBackendFlags }) {
  const {
    openModal,
    updateFromBackend,
    isComplete,
    isDismissed,
  } = useOnboardingContext();

  useEffect(() => {
    updateFromBackend(flags);

    // Registration started or incomplete onboarding: prompt the user.
    if (!isComplete && !isDismissed) {
      openModal();
    }
  }, [flags, isComplete, isDismissed, openModal, updateFromBackend]);

  return (
    <OnboardingPopup
      onComplete={() => console.log("onboarding finished")}
      onClose={() => console.log("onboarding dismissed/closed")}
    />
  );
}

export function AppRoot({ flags }: { flags: OnboardingBackendFlags }) {
  return (
    <OnboardingProvider>
      <OnboardingGate flags={flags} />
      {/* rest of your app */}
    </OnboardingProvider>
  );
}
```

### Action handling checklist

Use these hooks to connect the onboarding UI to your Next.js app:

- **Registration started**
  - Load backend flags and call `updateFromBackend(flags)`.
  - Call `openModal()` to show `OnboardingPopup`.
- **Email verified**
  - Update backend flags (or re-fetch `/api/auth/me`) and call `updateFromBackend(flags)`.
  - The profile step changes from `blocked` to `pending`.
- **Onboarding completed**
  - When the profile is complete, update flags so all steps are `complete`.
  - `OnboardingPopup` will show the completion state and invoke `onComplete` when closed.

### Component options

- `DashboardOnboardingSection`
  - `steps`, `progress`, `emailVerified`, `profileComplete`, `onActionClick`, `onOpenFullOnboarding`
- `OnboardingDashboard`
  - `steps`, `progress`, `variant` (`"compact"` or `"full"`), `onActionClick`
- `OnboardingChecklist`
  - `steps`, `variant` (`"compact"` or `"full"`), `onActionClick`
- `OnboardingModal`
  - `open`, `onOpenChange`, `steps`, `emailVerified`, `profileComplete`, `onActionClick`, `onResendVerification`,
    `onChangeEmail`, `onDismiss`, `dismissLabel`, `title`, `description`
- `OnboardingPopup`
  - `onComplete`, `onClose` (state is sourced from `OnboardingProvider`)


## Dashboard tab routing events

The Dashboard component can emit a routing-friendly event whenever a tab is selected. Use it to keep your app router in sync (for example, pushing routes in Next.js). You can also pass an `activeTab` prop to control the active tab from outside, along with a `tabPaths` map so your app remains the source of truth for URL formats.

```tsx
import { useState } from "react";
import {
  Dashboard,
  type DashboardTab,
  type DashboardTabChangeEvent,
} from "@igamingcareer/igaming-components";

export function DashboardWithRouter({ data }: { data: DashboardData }) {
  const [activeTab, setActiveTab] = useState<DashboardTab>("overview");

  const handleTabChange = (event: DashboardTabChangeEvent) => {
    setActiveTab(event.tab);
    // Example: Next.js router
    // router.push(event.path ?? "/dashboard");
  };

  return (
    <Dashboard
      user={data.user}
      profileData={data.profile}
      alerts={data.alerts}
      snapshot={data.snapshot}
      feedItems={data.feedItems}
      onUpdateProfile={data.onUpdateProfile}
      activeTab={activeTab}
      tabPaths={{
        overview: "/dashboard",
        profile: "/dashboard/profile",
        jobs: "/dashboard/jobs",
        courses: "/dashboard/courses",
        news: "/dashboard/news",
        settings: "/dashboard/settings",
      }}
      onTabChange={handleTabChange}
    />
  );
}
```

## Company detail tab routing events

The CompanyDetail component can emit a routing-friendly event whenever a tab is selected, so your Next.js app remains the source of truth for URLs. Provide `activeTab`, `tabPaths`, and `onTabChange` to keep routing and UI in sync.

```tsx
import { useState } from "react";
import {
  CompanyDetail,
  type CompanyDetailTab,
  type CompanyDetailTabChangeEvent,
} from "@igamingcareer/igaming-components";

export function CompanyDetailWithRouter({ data }: { data: CompanyDetailData }) {
  const [activeTab, setActiveTab] = useState<CompanyDetailTab>("overview");

  const handleTabChange = (event: CompanyDetailTabChangeEvent) => {
    setActiveTab(event.tab);
    // Example: Next.js router
    // router.push(event.path ?? `/companies/${data.company.slug}`);
  };

  return (
    <CompanyDetail
      company={data.company}
      jobs={data.jobs}
      news={data.news}
      events={data.events}
      activeTab={activeTab}
      tabPaths={{
        overview: `/companies/${data.company.slug}`,
        products: `/companies/${data.company.slug}/products`,
        trust: `/companies/${data.company.slug}/trust`,
        people: `/companies/${data.company.slug}/people`,
        jobs: `/companies/${data.company.slug}/jobs`,
        news: `/companies/${data.company.slug}/news`,
        events: `/companies/${data.company.slug}/events`,
        insights: `/companies/${data.company.slug}/insights`,
      }}
      onTabChange={handleTabChange}
    />
  );
}
```

## Dashboard tab routing events

The Dashboard component can emit a routing-friendly event whenever a tab is selected. Use it to keep your app router in sync (for example, pushing routes in Next.js). You can also pass an `activeTab` prop to control the active tab from outside, along with a `tabPaths` map so your app remains the source of truth for URL formats.

```tsx
import { useState } from "react";
import {
  Dashboard,
  type DashboardTab,
  type DashboardTabChangeEvent,
} from "@igamingcareer/igaming-components";

export function DashboardWithRouter({ data }: { data: DashboardData }) {
  const [activeTab, setActiveTab] = useState<DashboardTab>("overview");

  const handleTabChange = (event: DashboardTabChangeEvent) => {
    setActiveTab(event.tab);
    // Example: Next.js router
    // router.push(event.path ?? "/dashboard");
  };

  return (
    <Dashboard
      user={data.user}
      profileData={data.profile}
      alerts={data.alerts}
      snapshot={data.snapshot}
      feedItems={data.feedItems}
      onUpdateProfile={data.onUpdateProfile}
      activeTab={activeTab}
      tabPaths={{
        overview: "/dashboard",
        profile: "/dashboard/profile",
        jobs: "/dashboard/jobs",
        courses: "/dashboard/courses",
        news: "/dashboard/news",
        settings: "/dashboard/settings",
      }}
      onTabChange={handleTabChange}
    />
  );
}
```

## Save company callbacks (CompanyCard + CompanyDetail)

The company components also emit save events so host apps can manage bookmarking without API logic inside the library. Use the save props to track state and respond to user interactions.

```tsx
import { useState } from "react";
import {
  CompanyCard,
  CompanyDetail,
  type SaveCompanyPayload,
} from "@igamingcareer/igaming-components";

export function SaveCompaniesExample({ company }: { company: Company }) {
  const [isSaved, setIsSaved] = useState(false);
  const [saveLoading, setSaveLoading] = useState(false);

  const handleToggleSave = async (companyId: string, nextIsSaved: boolean) => {
    setSaveLoading(true);
    try {
      // call your API here
      // await api.saveCompany({ companyId, save: nextIsSaved });
      setIsSaved(nextIsSaved);
    } finally {
      setSaveLoading(false);
    }
  };

  const handleSaveAction = (payload: SaveCompanyPayload) => {
    // optional: analytics or logging
    console.log("save action", payload);
  };

  return (
    <>
      <CompanyCard
        company={{ ...company, isSaved }}
        isSaved={isSaved}
        saveLoading={saveLoading}
        onToggleSave={handleToggleSave}
        onSaveAction={handleSaveAction}
      />

      <CompanyDetail
        company={{ ...company, isSaved }}
        isSaved={isSaved}
        saveLoading={saveLoading}
        onToggleSave={handleToggleSave}
        onSaveAction={handleSaveAction}
      />
    </>
  );
}
```

Use `saveDisabled` to disable the action for unauthenticated users, and keep `isSaved` in sync with your data source.

## Claim company CTA (CompanyClaim)

Use the `CompanyClaim` component to render a minimal “Claim this company” CTA. It is fully controlled by props and emits a click event upward without any auth or API logic.

```tsx
import { CompanyClaim } from "@igamingcareer/igaming-components";

export function CompanyClaimExample({ company, canClaim }: { company: Company; canClaim: boolean }) {
  const handleClaimClick = () => {
    // optional: analytics or open your claim flow
    console.log("claim company", { companyId: company.id });
  };

  return (
    <CompanyClaim
      isClaimed={company.claimedStatus === "claimed"}
      canClaim={canClaim}
      onClaimClick={handleClaimClick}
    />
  );
}
```

## Claim profile dialog submissions (CompanyDetail)

When the claim dialog is submitted from `CompanyDetail`, the component emits a structured payload so your app can handle the request outside the library.

```tsx
import { CompanyDetail } from "@igamingcareer/igaming-components";
import type { CompanyClaimRequestPayload } from "@igamingcareer/igaming-components";

export function CompanyDetailWithClaim({ company }: { company: Company }) {
  const handleClaimSubmit = (payload: CompanyClaimRequestPayload) => {
    // send to your API or analytics
    console.log("claim submit", payload);
  };

  return <CompanyDetail company={company} onClaimSubmit={handleClaimSubmit} />;
}
```

## Claim company CTA (CompanyClaim)

Use the `CompanyClaim` component to render a minimal “Claim this company” CTA. It is fully controlled by props and emits a click event upward without any auth or API logic.

```tsx
import { CompanyClaim } from "@igamingcareer/igaming-components";

export function CompanyClaimExample({ company, canClaim }: { company: Company; canClaim: boolean }) {
  const handleClaimClick = () => {
    // optional: analytics or open your claim flow
    console.log("claim company", { companyId: company.id });
  };

  return (
    <CompanyClaim
      isClaimed={company.claimedStatus === "claimed"}
      canClaim={canClaim}
      onClaimClick={handleClaimClick}
    />
  );
}
```

## CV upload from the dashboard

The dashboard emits CV upload events so the host application can upload, store,
and extract text. The component library does **not** parse PDFs directly — pass
the extracted preview text back into the dashboard to display it.

```tsx
import { useCallback, useState } from "react";
import { Dashboard } from "@igamingcareer/igaming-components";

export function DashboardWithCvUpload() {
  const [cvPreviewText, setCvPreviewText] = useState("");
  const [cvPreviewStatus, setCvPreviewStatus] = useState<
    "idle" | "parsing" | "error"
  >("idle");
  const [cvPreviewErrorMessage, setCvPreviewErrorMessage] = useState<
    string | undefined
  >(undefined);

  const handleUploadCV = useCallback(async (file: File | null) => {
    if (!file) {
      setCvPreviewText("");
      setCvPreviewStatus("idle");
      setCvPreviewErrorMessage(undefined);
      return;
    }

    try {
      setCvPreviewStatus("parsing");
      setCvPreviewErrorMessage(undefined);

      // Upload the file to your backend and parse the PDF there.
      // const response = await uploadResume(file);
      // setCvPreviewText(response.text);
      // Example placeholder:
      setCvPreviewText("Preview text returned by your backend.");
      setCvPreviewStatus("idle");
    } catch (error) {
      setCvPreviewStatus("error");
      setCvPreviewErrorMessage(
        "We couldn't extract text from this CV. The file is still saved."
      );
    }
  }, []);

  return (
    <Dashboard
      user={{ name: "Jane Doe", email: "jane@example.com" }}
      profileData={/* your profile data */}
      alerts={[]}
      snapshot={[]}
      feedItems={[]}
      onUpdateProfile={async () => {}}
      onUploadCV={handleUploadCV}
      cvPreviewText={cvPreviewText}
      cvPreviewStatus={cvPreviewStatus}
      cvPreviewErrorMessage={cvPreviewErrorMessage}
    />
  );
}
```

Use the `file` argument to upload the PDF to your backend. When the upload is
deleted from the dialog, the handler receives `null`.

## Tag badge and tag picker

Use the `TagBadge` component to display tags (for job cards, filters, etc.) and the `TagPicker` component to select or create tags.

### Example 1: Basic tag badge

```tsx
import { TagBadge } from "@igamingcareer/igaming-components";

export function TagBadgeExample() {
  return <TagBadge name="Remote" color="#10B981" size="sm" />;
}
```

### Example 2: Removable and clickable tag badge

```tsx
import { TagBadge } from "@igamingcareer/igaming-components";

export function InteractiveTagBadge() {
  return (
    <TagBadge
      name="Interview"
      color="#F59E0B"
      size="md"
      removable
      onRemove={() => console.log("remove tag")}
      onClick={() => console.log("badge clicked")}
    />
  );
}
```

### Example 3: Tag picker with inline creation

```tsx
import { useState } from "react";
import { TagPicker } from "@igamingcareer/igaming-components";

const initialTags = [
  { id: "1", name: "Remote", color: "#10B981" },
  { id: "2", name: "Urgent", color: "#EF4444" },
  { id: "3", name: "Interview", color: "#F59E0B", category: "Stage" },
];

export function TagPickerExample() {
  const [tags, setTags] = useState(initialTags);
  const [selectedTagIds, setSelectedTagIds] = useState<string[]>(["1"]);

  const handleCreateTag = async ({ name, color }: { name: string; color: string }) => {
    const nextTag = { id: String(Date.now()), name, color };
    setTags((prev) => [...prev, nextTag]);
  };

  return (
    <TagPicker
      availableTags={tags}
      selectedTagIds={selectedTagIds}
      onTagSelect={(tagId) => setSelectedTagIds((prev) => [...prev, tagId])}
      onTagDeselect={(tagId) =>
        setSelectedTagIds((prev) => prev.filter((id) => id !== tagId))
      }
      onCreateTag={handleCreateTag}
      maxTags={8}
      placeholder="Add tags to this job..."
    />
  );
}
```

### Sign-in prompt modal for gated actions

Use the `SignInPromptModal` to nudge unauthenticated users to log in or create an account before performing actions such as
saving a job or opening a full job description. The parent app controls when the modal opens and owns the navigation/flows via
the emitted callbacks.

```tsx
import { useState } from "react";
import { SignInPromptModal } from "@igamingcareer/igaming-components";

export function GatedSaveButton() {
  const [open, setOpen] = useState(false);

  const handleLogin = () => {
    setOpen(false);
    // trigger your app's login flow
  };

  const handleRegister = () => {
    setOpen(false);
    // trigger your app's register flow
  };

  return (
    <>
      <button onClick={() => setOpen(true)}>Save job</button>

      <SignInPromptModal
        open={open}
        onClose={() => setOpen(false)}
        onLogin={handleLogin}
        onRegister={handleRegister}
        appName="iGamingCareer.com"
        attemptedAction="save this job"
        helperContent={<p className="text-sm text-muted-foreground">Create an account to sync saves across devices.</p>}
      />
    </>
  );
}
```

## Embedding components in plain HTML

The library can hydrate elements that carry specific classes or data attributes. Build the project, host the `dist/` output (or publish it to a CDN), and reference the emitted CSS/JS assets from your page.

1. Run `npm run build` and copy the `dist/` assets to your static host.
2. Add the generated stylesheet and script tags from `dist/index.html` (the file names include hashes). Example:

   ```html
   <link rel="stylesheet" href="/assets/index-XXXX.css" />
   <script type="module" src="/assets/index-XXXX.js"></script>
   ```

3. Mark up the page with the hooks expected by the components (see `src/main.tsx` for the full list). For example:

   ```html
   <div id="app"></div>

   <div class="button-component" data-name="Join now"></div>

   <div class="modal-component" id="offer-modal">
     <div class="title">Limited offer</div>
     <div class="description">Save 20% when you enroll today.</div>
     <div class="footer">Terms and conditions apply.</div>
   </div>
   ```

   When the bundle runs it will automatically hydrate these elements:

   - `#app` renders the root `<App />` component.
   - `.button-component` renders the `<Button />` component using the `data-name` attribute as the label.
   - `.modal-component` renders a `<CustomModal />` instance, reading nested `.title`, `.description`, and `.footer` content.

4. Use additional class hooks such as `.group-prices-component`, `.hero-component`, `.courses-component`, `.chatbot-component`, `.faq-component`, `.videos-component`, `.testimonials-component`, and `.sliding-summary-component` to hydrate the corresponding widgets. Pass data through `data-*` attributes as illustrated in `src/main.tsx`.

This approach allows teams that are not using React to embed the library’s UI on any HTML page while still benefiting from the React components under the hood.

## Interaction events (Listing, Courses, News)

The Listing, IGamingCoursePage, and NewsPage components can now emit structured interaction events for analytics or logging. Supply an `onEmitEvent` callback and optional `eventContext` when you render the component (React or DOM embedding):

```tsx
import { Listing } from "@igamingcareer/igaming-components"
import type { EmittedEvent } from "@igamingcareer/igaming-components"

const handleEvent = (event: EmittedEvent) => {
  console.log("component event", event)
  // Forward to your analytics/datastore
}

<Listing
  apiUrl="/api/jobs"
  filterKeys={["company", "city"]}
  cardType="job"
  onEmitEvent={handleEvent}
  eventContext={{ route: "/jobs", source: "listing" }}
/>
```

All emitted events share the base shape `{ domain, name, payload, timestamp, context }` (see `src/types/events.ts`). Domain-specific unions live in `src/types/events.jobs.ts`, `src/types/events.courses.ts`, and `src/types/events.news.ts`.

Key events include:

- Jobs Listing: search submissions/removals, filter select/clear/all-clear, date changes, pagination, view mode, items per page, job open/save/apply, alerts/subscription steps.
- Courses Listing: search submissions, filter changes/clear, category/subcategory selection, view mode, pagination, items per page, favorites, enroll/open clicks.
- News Listing: search submissions/removals, filter select/clear/all-clear, date changes, category selection, pagination, view mode, items per page, article open/bookmark/share.

When embedding via `renderMultiple` (see `src/main.tsx`), a global `window.IGC_EMIT_EVENT` handler will be passed automatically if present, along with a default context using the current route and optional `data-source` attribute.