npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@igamingcareer/igaming-components

v1.0.125

Published

Reusable React component library for the iGamingCareer platform

Readme

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):

    npm install
  2. Start the Vite dev server:

    npm run dev
  3. Build the distributable assets:

    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

    <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-componentDashboard 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:

    npm run storybook
  • Generate the static Storybook site:

    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:

npm install @igamingcareer/igaming-components
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>
  );
}

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.

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="[email protected]"
    />
  );
}

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):

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

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

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.

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:

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.

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:

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.

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

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.

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.

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:

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 startedemail_verified = false, profile_complete = false.
  2. Email verifiedemail_verified = true, profile_complete = false (profile step becomes pending).
  3. Onboarding completedemail_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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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: "[email protected]" }}
      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

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

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

Example 2: Removable and clickable tag badge

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

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.

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:

    <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:

    <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):

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.