# Fans United Frontend Components

A collection of interactive components for Fans United widgets providing engaging experiences and tools.

## Installation

```bash
npm install fansunited-frontend-components fansunited-frontend-core
```

```bash
yarn add fansunited-frontend-components fansunited-frontend-core
```

> **Note**: Both packages are required. `fansunited-frontend-core` provides types and utilities, while `fansunited-frontend-components` provides the UI components.

## Quick Start

```tsx
import React from "react";
import { ClassicQuizPlay } from "fansunited-frontend-components";
import { ClassicQuizPlayProps, WidgetTemplate } from "fansunited-frontend-core";
import { FansUnitedSDK } from "fansunited-sdk-esm";

const sdk = FansUnitedSDK({
  // your config here
});

const App: React.FC = () => {
  const props: ClassicQuizPlayProps = {
    entityId: "your-quiz-id",
    sdk,
    template: WidgetTemplate.STANDARD, // Also SPLIT and OVERLAY templates are available
    language: "en", // "bg" | "en" | "ro" | "pt" | "sr" | "fr" | "de" | "it" | "fr-be" | "fr" | "pl" | "pt" | "pt-br" | "sk
  };

  return (
    <div>
      <h1>My App</h1>
      <ClassicQuizPlay {...props} />
    </div>
  );
};

export default App;
```

## Components

### ClassicQuizPlay

Interactive quiz component with multiple templates and customization options.

#### Required Props

| Prop       | Type                 | Description             |
| ---------- | -------------------- | ----------------------- |
| `entityId` | `string`             | Classic Quiz identifier |
| `sdk`      | `FansUnitedSDKModel` | SDK instance            |
| `template` | `WidgetTemplate`     | Layout template         |
| `language` | `LanguageType`       | Display language        |

#### Optional Props

| Prop                         | Type                    | Description                                 |
| ---------------------------- | ----------------------- | ------------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`    | Theme configuration                         |
| `showAnswerExplanations`     | `boolean`               | Show explanations after quiz completion     |
| `leads`                      | `LeadsOptions`          | Lead collection settings                    |
| `imagePosition`              | `"left" \| "right"`     | Image position (STANDARD template only)     |
| `defaultImagePlaceholderUrl` | `string`                | URL for default image placeholder           |
| `userIsLoggedIn`             | `boolean`               | Determine if the user is logged in          |
| `signInCTA`                  | `SignInCTADetails`      | Sign in call to action button configuration |
| `additionalCTA`              | `AdditionalCTADetails`  | Additional call to action button configuration |
| `rulesDisplay`               | `RulesDisplay`          | Game rules display configuration            |

#### Templates

```tsx
import { WidgetTemplate, ClassicQuizPlayProps } from "fansunited-frontend-core";

// Standard template with optional image positioning
const standardProps: ClassicQuizPlayProps = {
  template: WidgetTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Other templates
const overlayProps: ClassicQuizPlayProps = {
  template: WidgetTemplate.OVERLAY, // WidgetTemplate.SPLIT
  // imagePosition not available for non-standard templates
  // ... other props
};
```

#### Theming

Customize the appearance using theme options:

```tsx
import { CustomThemeOptions } from "fansunited-frontend-core";

// Default theme options
const themeOptions: CustomThemeOptions = {
  mode: "light", // or 'dark'
  colorSchemes: {
    light: {
      palette: {
        success: {
          plainColor: "#4CAF50",
          outlinedBorder: "#4CAF50",
          softBg: "#E3FBE3",
        },
        danger: {
          softBg: "#FEE4E2",
          plainColor: "#F44336",
          outlinedBorder: "#F44336",
        },
        primary: {
          plainColor: "#1A77D2",
          outlinedBorder: "#1A77D2",
          onPrimary: "#FAFAFA",
          primaryContainer: "#2397F3",
        },
        warning: {
          softBg: "#FEF0C7",
          plainColor: "#DC6803",
        },
      },
      textPrimary: "#212121",
      textSecondary: "#212121",
      textBody: "#424242",
      textColor: "#212121",
      textDisabled: "#212121",
      surface: "#FFFFFF",
      onSurface: "#F5F5F5",
      surfaceVariant: "#EEEEEE",
      surfaceTintDim: "#212121",
      surfaceInverse: "#F5F5F5",
      outlineEnabledBorder: "#E0E0E0",
      secondaryContainer: "#BDBDBD",
    },
    dark: {
      palette: {
        primary: {
          plainColor: "#1A77D2",
          outlinedBorder: "#1A77D2",
          onPrimary: "#FAFAFA",
          primaryContainer: "#2397F3",
        },
        success: {
          plainColor: "#4CAF50",
          outlinedBorder: "#4CAF50",
          softBg: "#042F04",
        },
        danger: {
          softBg: "#430A0A",
          plainColor: "#F44336",
          outlinedBorder: "#F44336",
        },
        warning: {
          softBg: "#FEF0C7",
          plainColor: "#DC6803",
        },
      },
      textPrimary: "#FAFAFA",
      textSecondary: "#FAFAFA",
      textBody: "#FAFAFA",
      textColor: "#FAFAFA",
      textDisabled: "#FAFAFA",
      surface: "#424242",
      onSurface: "#212121",
      surfaceVariant: "#616161",
      surfaceTintDim: "#FAFAFA",
      surfaceInverse: "#FAFAFA",
      outlineEnabledBorder: "#757575",
      secondaryContainer: "#757575",
    },
  },
  customBreakpoints: {
    values: {
      xs: 0,
      sm: 444,
      md: 600,
      lg: 900,
      xl: 1200,
      xxl: 1536,
    },
  },
  spacingScale: {
    "3xs": "2px",
    "2xs": "4px",
    xs: "8px",
    sm: "12px",
    md: "16px",
    lg: "24px",
    xl: "32px",
    "2xl": "40px",
    "3xl": "48px",
  },
  customFontFamily: {
    light: {
      primary: "Ubuntu, sans-serif",
      secondary: "Roboto, sans-serif",
    },
    dark: {
      primary: "Ubuntu, sans-serif",
      secondary: "Roboto, sans-serif",
    },
  },
  customRadius: {
    light: {
      none: "0px",
      "2xs": "2px",
      xs: "4px",
      sm: "8px",
      md: "12px",
      lg: "16px",
      xl: "24px",
      "2xl": "232px",
      full: "1000px",
    },
    dark: {
      none: "0px",
      "2xs": "2px",
      xs: "4px",
      sm: "8px",
      md: "12px",
      lg: "16px",
      xl: "24px",
      "2xl": "232px",
      full: "1000px",
    },
  },
  border: {
    light: {
      size: "1px",
    },
    dark: {
      size: "2px",
    },
  },
  imageBackgroundGradient: {
    light: {
      standard:
        "linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, rgba(18, 18, 18, 0.8) 100%)",
      split:
        "linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, rgba(18, 18, 18, 0.8) 100%)",
    },
    dark: {
      standard:
        "linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, rgba(18, 18, 18, 0.8) 100%)",
      split:
        "linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, rgba(18, 18, 18, 0.8) 100%)",
      overlay:
        "linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, rgba(18, 18, 18, 0.8) 100%)",
    },
  },
};

<ClassicQuizPlay {...otherProps} themeOptions={themeOptions} />;
```

#### Lead Collection

Enable lead capture functionality:

```tsx
import { LeadsOptions } from "fansunited-frontend-core";

const leads: LeadsOptions = {
  position: "before", // "before" | "after"
  fields: ["fullName"], // Available: "fullName" | "firstName" | "lastName" | "email" | "gender" | "country" | "phoneCountryCode" | "phoneNumber"
  campaignId: "test-quizzes",
  campaignName: "Test Quizzes",
  phoneCountryCode: "359", // When not provided will set the phone country code from component language prop. So for example if the language is "bg" and phoneCountryCode is not provided, it will fallback to "359"
  syncWithProfile: true, // Optional: sync form data to user profile after successful submission
};

<ClassicQuizPlay {...otherProps} leads={leads} />;
```

#### Sign in Configuration

Control user authentication and sign-in behavior using the `userIsLoggedIn` and `signInCTA` props.

```tsx
import { SignInCTADetails } from "fansunited-frontend-core";

// Basic sign-in with onClick handler
const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign In",
  onClick: () => {
    // Handle sign-in logic
    console.log("Sign in clicked");
  },
};

// Sign-in with URL navigation
const signInCTA: SignInCTADetails = {
  defaultLabel: "Login",
  url: "https://your-auth-provider.com/login",
  target: "_blank", // or "_self"
};

// Custom sign-in component
const signInCTA: SignInCTADetails = {
  component: <CustomSignInButton />,
};

<ClassicQuizPlay
  {...otherProps}
  userIsLoggedIn={false}
  signInCTA={signInCTA}
/>;
```

**Sign-in Priority Order:**

1. **Custom Component** - If `signInCTA.component` is provided, it will be rendered
2. **Click Handler** - If `signInCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `signInCTA.url` is provided, a button will be rendered and clicking will navigate to the URL
4. **Disabled** - If no `signInCTA` is provided, a disabled button will be shown

**Behavior:**

- When `userIsLoggedIn` is `false` and the quiz requires authentication (`authRequirement: "REGISTERED"`), the sign-in screen will be displayed instead of the quiz

#### Additional CTA Configuration

Add an extra call-to-action button to the quiz completion screen using the `additionalCTA` prop.

```tsx
import { AdditionalCTADetails } from "fansunited-frontend-core";

// Custom component
const additionalCTA: AdditionalCTADetails = {
  component: <CustomCTAButton />,
};

// Basic CTA with onClick handler
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "See More",
  onClick: () => {
    // Handle click logic
    console.log("Additional CTA clicked");
  },
};

// CTA with URL navigation
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "Visit Website",
  url: "https://your-website.com",
  target: "_blank", // or "_self"
};

<ClassicQuizPlay
  {...otherProps}
  additionalCTA={additionalCTA}
/>;
```

**Priority Order:**

1. **Custom Component** - If `additionalCTA.component` is provided, it will be rendered
2. **Click Handler** - If `additionalCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `additionalCTA.url` is provided, a button will be rendered and clicking will navigate to the URL

**Placement:**

- **Standard variant**: Displayed after "Play Again" and "Share Result" buttons
- **Split variant**: Displayed after "Play Again" button in the thank you container
- **Overlay variant**: Displayed after "Play Again" and "Share Result" buttons

#### Rules Display Configuration

Display game rules and instructions to users with a clickable info icon in the component headline using the `rulesDisplay` prop.

```tsx
import { RulesDisplay } from "fansunited-frontend-core";

// Display rules in a modal dialog (default)
const rulesDisplay: RulesDisplay = {
  type: "modal",
};

// Display rules as a clickable link
const rulesDisplay: RulesDisplay = {
  type: "link",
  url: "https://your-website.com/quiz-rules",
  target: "_blank", // or "_self"
};

// Custom rules content component
const rulesDisplay: RulesDisplay = {
  type: "modal",
  component: <CustomRulesContent />,
};

<ClassicQuizPlay
  {...otherProps}
  rulesDisplay={rulesDisplay}
/>;
```

**Display Types:**

1. **Modal** - Opens rules in a modal dialog overlay (default behavior)
   - Uses the quiz's `rules` field from the backend
   - Displays in a centered modal with close button
   - Supports HTML content rendering

2. **Link** - Opens rules URL in a new tab/window
   - Requires `url` property
   - Optional `target` property (`_blank` or `_self`)
   - Useful for external documentation or detailed rule pages

**Features:**

- **Info Icon**: Displays a small info icon (ⓘ) next to the component headline
- **Responsive**: Adapts to all three templates (Standard, Split, Overlay)
- **Theme-Aware**: Automatically adjusts colors based on theme mode and variant
- **Accessible**: Keyboard navigable and screen reader friendly
- **Non-Intrusive**: Icon only appears when rules are configured

**Behavior:**

- The rules icon appears in the headline section across all component states (main view, sign-in, error, score state, lead collection)
- Modal dialogs can be closed by clicking the close button, clicking outside, or pressing Escape
- Link type opens in a new tab by default for better user experience

#### TypeScript Support

This package is built with TypeScript and provides full type definitions. Import types from `fansunited-frontend-core`:

```tsx
import {
  ClassicQuizPlayProps,
  WidgetTemplate,
  MainProps,
  CustomThemeOptions,
  LeadsOptions,
  RulesDisplay,
  SignInCTADetails,
  AdditionalCTADetails,
  AnalyticsEvent,
  ChanceGameProps,
  ChanceGameTemplate,
  ChanceGameVariant,
} from "fansunited-frontend-core";
```

#### Peer Dependencies

```json
{
  "react": ">=16.8.0",
  "react-dom": ">=16.8.0"
}
```

#### Bundle

The component uses shadow DOM and emotion for isolated styling, ensuring no CSS conflicts with your application.

#### Examples

##### Basic Quiz

```tsx
import { ClassicQuizPlay } from "fansunited-frontend-components";
import { WidgetTemplate } from "fansunited-frontend-core";

<ClassicQuizPlay
  entityId="quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
/>;
```

##### Customized Quiz

```tsx
<ClassicQuizPlay
  entityId="quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  imagePosition="right"
  language="en"
  showAnswerExplanations={true}
  themeOptions={{
    mode: "dark",
  }}
  leads={{
    phoneCountryCode: "44",
    syncWithProfile: true,
  }}
/>
```

### PollVote

Interactive poll voting component with real-time results, multiple choice support, and customizable templates. Perfect for engaging audiences with opinion polls, surveys, and voting campaigns.

**Key Features:**

- Single and multiple choice voting
- Real-time vote results and percentages
- Three responsive templates (Standard, Split, Overlay)
- Lead collection integration
- Authentication support
- Vote attempt limits
- Custom theming and branding

#### Required Props

| Prop       | Type                 | Description             |
| ---------- | -------------------- | ----------------------- |
| `entityId` | `string`             | Classic Quiz identifier |
| `sdk`      | `FansUnitedSDKModel` | SDK instance            |
| `template` | `WidgetTemplate`     | Layout template         |
| `language` | `LanguageType`       | Display language        |

#### Optional Props

| Prop                         | Type                    | Description                                 |
| ---------------------------- | ----------------------- | ------------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`    | Theme configuration                         |
| `leads`                      | `LeadsOptions`          | Lead collection settings                    |
| `imagePosition`              | `"left" \| "right"`     | Image position (STANDARD template only)     |
| `defaultImagePlaceholderUrl` | `string`                | URL for default image placeholder           |
| `userIsLoggedIn`             | `boolean`               | Determine if the user is logged in          |
| `signInCTA`                  | `SignInCTADetails`      | Sign in call to action button configuration |
| `additionalCTA`              | `AdditionalCTADetails`  | Additional call to action button configuration |
| `rulesDisplay`               | `RulesDisplay`          | Poll rules display configuration            |

#### Templates

```tsx
import { WidgetTemplate, PollVoteProps } from "fansunited-frontend-core";

// Standard template with optional image positioning
const standardProps: PollVoteProps = {
  template: WidgetTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Split template - side-by-side layout
const splitProps: PollVoteProps = {
  template: WidgetTemplate.SPLIT,
  // imagePosition not available for non-standard templates
  // ... other props
};

// Overlay template - full-screen immersive experience
const overlayProps: PollVoteProps = {
  template: WidgetTemplate.OVERLAY,
  // ... other props
};
```

#### Sign in Configuration

Control user authentication and voting permissions using the `userIsLoggedIn` and `signInCTA` props.

```tsx
import { SignInCTADetails } from "fansunited-frontend-core";

// Custom sign-in component
const signInCTA: SignInCTADetails = {
  component: <CustomSignInButton />,
};

// Basic sign-in with onClick handler
const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign in to vote",
  onClick: () => {
    // Handle sign-in logic
    console.log("Sign in clicked");
  },
};

// Sign-in with URL navigation
const signInCTA: SignInCTADetails = {
  defaultLabel: "Login to participate",
  url: "https://your-auth-provider.com/login",
  target: "_blank",
};

<PollVote {...otherProps} userIsLoggedIn={false} signInCTA={signInCTA} />;
```

**Sign-in Priority Order:**

1. **Custom Component** - If `signInCTA.component` is provided, it will be rendered
2. **Click Handler** - If `signInCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `signInCTA.url` is provided, a button will be rendered and clicking will navigate to the URL
4. **Disabled** - If no `signInCTA` is provided, a disabled button will be shown

**Behavior:**

- When `userIsLoggedIn` is `false` and the poll requires authentication (`authRequirement: "REGISTERED"`), the sign-in screen will be displayed instead of the poll

#### Additional CTA Configuration

Add an extra call-to-action button to the poll results screen using the `additionalCTA` prop.

```tsx
import { AdditionalCTADetails } from "fansunited-frontend-core";

// Custom component
const additionalCTA: AdditionalCTADetails = {
  component: <CustomCTAButton />,
};

// Basic CTA with onClick handler
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "Learn More",
  onClick: () => {
    // Handle click logic
    console.log("Additional CTA clicked");
  },
};

// CTA with URL navigation
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "View Details",
  url: "https://your-website.com/poll-details",
  target: "_blank", // or "_self"
};

<PollVote
  {...otherProps}
  additionalCTA={additionalCTA}
/>;
```

**Priority Order:**

1. **Custom Component** - If `additionalCTA.component` is provided, it will be rendered
2. **Click Handler** - If `additionalCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `additionalCTA.url` is provided, a button will be rendered and clicking will navigate to the URL

**Placement:**

- **Standard variant**: Displayed before "Vote Again" button
- **Split variant**: Displayed in the same container as "Vote Again" button
- **Overlay variant**: Displayed after "Vote Again" button

#### Lead Collection

Capture user information before or after voting:

```tsx
import { LeadsOptions } from "fansunited-frontend-core";

const leads: LeadsOptions = {
  position: "before", // "before" | "after"
  fields: ["fullName", "email"], // Available: "fullName" | "firstName" | "lastName" | "email" | "gender" | "country" | "phoneCountryCode" | "phoneNumber"
  campaignId: "poll-campaign-2024",
  campaignName: "User Opinion Poll",
  phoneCountryCode: "44", // Default country code for phone fields
  syncWithProfile: true, // Optional: sync form data to user profile after successful submission
};

<PollVote {...otherProps} leads={leads} />;
```

#### Profile Sync

When `syncWithProfile` is enabled in the leads configuration, form data will be automatically synced to the user's profile after successful submission:

```tsx
import { LeadsOptions } from "fansunited-frontend-core";

const leads: LeadsOptions = {
  position: "after",
  fields: ["firstName", "lastName", "email", "phoneNumber", "country"],
  campaignId: "user-profile-sync",
  campaignName: "Profile Sync Campaign",
  syncWithProfile: true, // Enable profile synchronization
};

<PollVote {...otherProps} leads={leads} />
```

**How it works:**

- Only fields that are both **included in the form configuration** AND **have non-empty values** will be synced
- Supported fields: `fullName`, `firstName`, `lastName`, `email`, `phoneNumber`, `phoneCountryCode`, `country`, `gender`
- Uses the SDK profile namespace: `sdk.profile.getOwn().setName().setEmail()...update()`
- Profile sync failures are logged but don't break the lead collection flow
- Sync happens automatically after successful lead form submission

**Name Field Priority:**
- If `fullName` is configured and has a value, it takes priority for the profile name
- If both `firstName` and `lastName` are configured and have values, they are combined: `"firstName lastName"`
- If only `firstName` is configured and has a value, it becomes the profile name
- If only `lastName` is configured and has a value, it becomes the profile name

#### Examples

##### Basic Poll

```tsx
import { PollVote } from "fansunited-frontend-components";
import { WidgetTemplate } from "fansunited-frontend-core";

<PollVote
  entityId="poll-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
/>;
```

##### Customized Poll

```tsx
<PollVote
  entityId="poll-123"
  sdk={sdkInstance}
  template={WidgetTemplate.OVERLAY}
  language="en"
  userIsLoggedIn={false}
  signInCTA={{
    defaultLabel: "Sign in to vote",
    onClick: () => handleSignIn(),
  }}
  leads={{
    position: "after",
    fields: ["fullName", "email"],
    campaignId: "poll-campaign",
    campaignName: "User Survey 2024",
    phoneCountryCode: "44",
    syncWithProfile: true,
  }}
  themeOptions={{
    mode: "dark",
  }}
/>
```

### CollectLead

Lead collection component for capturing user information with customizable forms, branding, and success actions. Perfect for newsletter signups, contact forms, and user registration flows.

**Key Features:**

- Customizable form fields (name, email, phone, country, gender, custom fields)
- Consent management and privacy policy integration
- Custom branding and theming support
- Success page with custom CTAs
- Three responsive templates (Standard, Split, Overlay)
- Phone number validation with country codes
- Multi-language support

#### Required Props

| Prop       | Type                 | Description                     |
| ---------- | -------------------- | ------------------------------- |
| `entityId` | `string`             | Lead collection form identifier |
| `sdk`      | `FansUnitedSDKModel` | SDK instance                    |
| `template` | `WidgetTemplate`     | Layout template                 |
| `language` | `LanguageType`       | Display language                |

#### Optional Props

| Prop                         | Type                  | Description                               |
| ---------------------------- | --------------------- | ----------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`  | Theme configuration                       |
| `fields`                     | `LeadField[]`         | Form fields to display                    |
| `labels`                     | `LeadLabels`          | Custom labels for form elements           |
| `imagePosition`              | `"left" \| "right"`   | Image position (STANDARD template only)   |
| `defaultImagePlaceholderUrl` | `string`              | URL for default image placeholder         |
| `phoneCountryCode`           | `string`              | Default country code for phone fields     |
| `syncWithProfile`            | `boolean`             | Sync form data to user profile after successful submission |
| `onSuccessCTA`               | `OnSuccessCTADetails` | Success page call to action configuration |
| `customFields`               | `LeadCustomField[]`   | Additional custom form fields             |
| `consents`                   | `LeadConsent[]`       | Consent checkboxes for privacy compliance |
| `content`                    | `ContentInfo`         | Content metadata                          |
| `campaign`                   | `CampaignInfo`        | Campaign information                      |
| `branding`                   | `BrandingInfo`        | Branding configuration                    |
| `images`                     | `ImagesModel`         | Image assets                              |

#### Templates

```tsx
import { WidgetTemplate, CollectLeadProps } from "fansunited-frontend-core";

// Standard template with optional image positioning
const standardProps: CollectLeadProps = {
  template: WidgetTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Split template - side-by-side layout
const splitProps: CollectLeadProps = {
  template: WidgetTemplate.SPLIT,
  // imagePosition not available for non-standard templates
  // ... other props
};

// Overlay template - full-screen immersive experience
const overlayProps: CollectLeadProps = {
  template: WidgetTemplate.OVERLAY,
  // ... other props
};
```

#### Form Configuration

Configure which fields to display and their behavior:

```tsx
import {
  LeadField,
  LeadCustomField,
  LeadConsent,
} from "fansunited-frontend-core";

// Standard form fields
const fields: LeadField[] = [
  "fullName",
  "email",
  "phoneNumber",
  "country",
  "gender",
];

// Custom fields for additional data collection
const customFields: LeadCustomField[] = [
  {
    key: "company",
    label: "Company Name",
    type: "input",
    required: true,
    placeholder: "Enter your company name",
  },
  {
    key: "message",
    label: "Message",
    type: "textarea",
    required: false,
    placeholder: "Tell us about your needs...",
  },
];

// Consent checkboxes for privacy compliance
const consents: LeadConsent[] = [
  {
    id: "marketing",
    label: "I agree to receive marketing communications",
  },
  {
    id: "newsletter",
    label: "Subscribe to our newsletter",
  },
];

<CollectLead
  {...otherProps}
  fields={fields}
  customFields={customFields}
  consents={consents}
/>;
```

#### Custom Labels

Customize all text displayed in the form:

```tsx
import { LeadLabels } from "fansunited-frontend-core";

const labels: LeadLabels = {
  title: "Join Our Community",
  description: "Get exclusive updates and early access to new features",
  formTitle: "Sign Up Today",
  formDescription: "Fill out the form below to get started",
  formFullNameLabel: "Full Name",
  formFullNamePlaceholder: "Enter your full name",
  formEmailLabel: "Email Address",
  formGenderLabel: "Gender",
  formGenderPlaceholder: "Select your gender",
  formCountryLabel: "Country",
  formCountryPlaceholder: "Select your country",
  formPhoneNumberLabel: "Phone Number",
  formPhoneNumberPlaceholder: "Enter your phone number",
  submitButtonLabel: "Subscribe Now",
  successTitle: "Welcome Aboard!",
  successDescription:
    "Thank you for joining our community. Check your email for next steps.",
  privacyPolicyUrlLabel: "Privacy Policy",
  termsAndConditionsUrlLabel: "Terms & Conditions",
  presentedByLabel: "Powered by",
};

<CollectLead {...otherProps} labels={labels} />;
```

#### Success Actions

Configure what happens after successful form submission:

```tsx
import { OnSuccessCTADetails } from "fansunited-frontend-core";

// Custom success action with onClick handler
const onSuccessCTA: OnSuccessCTADetails = {
  defaultLabel: "Continue to Dashboard",
  onClick: () => {
    // Handle post-submission logic
    window.location.href = "/dashboard";
  },
};

// Success action with URL navigation
const onSuccessCTA: OnSuccessCTADetails = {
  defaultLabel: "Visit Our Website",
  url: "https://your-website.com",
  target: "_blank",
};

// Custom success component
const onSuccessCTA: OnSuccessCTADetails = {
  component: <CustomSuccessButton />,
};

<CollectLead {...otherProps} onSuccessCTA={onSuccessCTA} />;
```

#### Profile Sync

Enable automatic profile synchronization to update user profiles with form data:

```tsx
<CollectLead
  entityId="lead-form-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
  fields={["firstName", "lastName", "email", "phoneNumber", "country", "gender"]}
  syncWithProfile={true} // Enable profile synchronization
  labels={{
    title: "Update Your Profile",
    submitButtonLabel: "Save & Update Profile",
  }}
/>
```

**Profile Sync Features:**

- **Automatic Sync**: Form data is automatically synced to the user's profile after successful submission
- **Field Mapping**: Only configured fields with non-empty values are synced to the profile
- **Supported Fields**: `fullName`, `firstName`, `lastName`, `email`, `phoneNumber`, `phoneCountryCode`, `country`, `gender`
- **SDK Integration**: Uses the profile builder pattern: `sdk.profile.getOwn().setName().setEmail()...update()`
- **Error Handling**: Profile sync failures are logged but don't prevent lead collection
- **Optional**: Defaults to `false`, completely opt-in behavior

**Name Field Handling:**
- **Priority 1**: `fullName` (if configured and has value) → sets profile name directly
- **Priority 2**: `firstName` + `lastName` (if both configured and have values) → combined as `"firstName lastName"`
- **Priority 3**: `firstName` only (if configured and has value) → sets profile name
- **Priority 4**: `lastName` only (if configured and has value) → sets profile name

**Use Cases:**

- User registration forms that also update profiles
- Newsletter signups that capture additional profile information
- Contact forms that enhance user profiles with new data
- Profile update forms with lead tracking

#### Examples

##### Basic Lead Form

```tsx
import { CollectLead } from "fansunited-frontend-components";
import { WidgetTemplate } from "fansunited-frontend-core";

<CollectLead
  entityId="lead-form-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
  fields={["fullName", "email"]}
/>;
```

##### Advanced Lead Form

```tsx
<CollectLead
  entityId="lead-form-123"
  sdk={sdkInstance}
  template={WidgetTemplate.OVERLAY}
  language="en"
  fields={["fullName", "email", "phoneNumber", "country"]}
  phoneCountryCode="44"
  syncWithProfile={true}
  customFields={[
    {
      key: "company",
      label: "Company",
      type: "input",
      required: true,
    },
  ]}
  consents={[
    {
      id: "marketing",
      label: "I agree to receive marketing emails",
    },
  ]}
  labels={{
    title: "Get Early Access",
    submitButtonLabel: "Join Waitlist",
    successTitle: "You're In!",
    successDescription: "We'll notify you when we launch.",
  }}
  onSuccessCTA={{
    defaultLabel: "Learn More",
    url: "https://example.com/learn-more",
    target: "_blank",
  }}
  themeOptions={{
    mode: "dark",
  }}
/>
```

### PersonalityQuizPlay

Interactive personality quiz component that matches users with personas based on their answers. Features comprehensive result displays with persona matching, detailed explanations, and customizable templates.

**Key Features:**

- Personality matching algorithm with percentage scores
- Multiple persona results with detailed descriptions
- Tabbed result interface with top match highlighting
- Answer explanations and persona insights
- Three responsive templates (Standard, Split, Overlay)
- Lead collection integration
- Authentication support
- Custom theming and branding
- Result sharing capabilities

#### Required Props

| Prop       | Type                 | Description                 |
| ---------- | -------------------- | --------------------------- |
| `entityId` | `string`             | Personality Quiz identifier |
| `sdk`      | `FansUnitedSDKModel` | SDK instance                |
| `template` | `WidgetTemplate`     | Layout template             |
| `language` | `LanguageType`       | Display language            |

#### Optional Props

| Prop                         | Type                    | Description                                 |
| ---------------------------- | ----------------------- | ------------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`    | Theme configuration                         |
| `showAnswerExplanations`     | `boolean`               | Show explanations after quiz completion     |
| `leads`                      | `LeadsOptions`          | Lead collection settings                    |
| `imagePosition`              | `"left" \| "right"`     | Image position (STANDARD template only)     |
| `defaultImagePlaceholderUrl` | `string`                | URL for default image placeholder           |
| `userIsLoggedIn`             | `boolean`               | Determine if the user is logged in          |
| `signInCTA`                  | `SignInCTADetails`      | Sign in call to action button configuration |
| `additionalCTA`              | `AdditionalCTADetails`  | Additional call to action button configuration |
| `optionsLayout`              | `OptionsLayout`         | Layout for answer options                   |
| `rulesDisplay`               | `RulesDisplay`          | Quiz rules display configuration            |

#### Templates

```tsx
import {
  WidgetTemplate,
  PersonalityQuizPlayProps,
} from "fansunited-frontend-core";

// Standard template with optional image positioning
const standardProps: PersonalityQuizPlayProps = {
  template: WidgetTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Split template - side-by-side layout
const splitProps: PersonalityQuizPlayProps = {
  template: WidgetTemplate.SPLIT,
  // imagePosition not available for non-standard templates
  // ... other props
};

// Overlay template - full-screen immersive experience
const overlayProps: PersonalityQuizPlayProps = {
  template: WidgetTemplate.OVERLAY,
  // ... other props
};
```

#### Options Layout

Configure how answer options are displayed:

```tsx
import { OptionsLayout } from "fansunited-frontend-core";

// Two-by-two grid layout
const twoByTwoLayout: PersonalityQuizPlayProps = {
  optionsLayout: "twoByTwo",
  // ... other props
};

// Horizontal row layout
const rowLayout: PersonalityQuizPlayProps = {
  optionsLayout: "row",
  // ... other props
};

// Vertical column layout
const columnLayout: PersonalityQuizPlayProps = {
  optionsLayout: "column",
  // ... other props
};
```

#### Sign in Configuration

Control user authentication and quiz access using the `userIsLoggedIn` and `signInCTA` props.

```tsx
import { SignInCTADetails } from "fansunited-frontend-core";

// Custom sign-in component
const signInCTA: SignInCTADetails = {
  component: <CustomSignInButton />,
};

// Basic sign-in with onClick handler
const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign in to take quiz",
  onClick: () => {
    // Handle sign-in logic
    console.log("Sign in clicked");
  },
};

// Sign-in with URL navigation
const signInCTA: SignInCTADetails = {
  defaultLabel: "Login to discover your personality",
  url: "https://your-auth-provider.com/login",
  target: "_blank",
};

<PersonalityQuizPlay
  {...otherProps}
  userIsLoggedIn={false}
  signInCTA={signInCTA}
/>;
```

**Sign-in Priority Order:**

1. **Custom Component** - If `signInCTA.component` is provided, it will be rendered
2. **Click Handler** - If `signInCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `signInCTA.url` is provided, a button will be rendered and clicking will navigate to the URL
4. **Disabled** - If no `signInCTA` is provided, a disabled button will be shown

**Behavior:**

- When `userIsLoggedIn` is `false` and the quiz requires authentication (`authRequirement: "REGISTERED"`), the sign-in screen will be displayed instead of the quiz

#### Additional CTA Configuration

Add an extra call-to-action button to the quiz results screen using the `additionalCTA` prop.

```tsx
import { AdditionalCTADetails } from "fansunited-frontend-core";

// Custom component
const additionalCTA: AdditionalCTADetails = {
  component: <CustomCTAButton />,
};

// Basic CTA with onClick handler
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "Explore More",
  onClick: () => {
    // Handle click logic
    console.log("Additional CTA clicked");
  },
};

// CTA with URL navigation
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "Discover Your Type",
  url: "https://your-website.com/personality-types",
  target: "_blank", // or "_self"
};

<PersonalityQuizPlay
  {...otherProps}
  additionalCTA={additionalCTA}
/>;
```

**Priority Order:**

1. **Custom Component** - If `additionalCTA.component` is provided, it will be rendered
2. **Click Handler** - If `additionalCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `additionalCTA.url` is provided, a button will be rendered and clicking will navigate to the URL

**Placement:**

- **Standard variant**: Displayed before "Play Again" button
- **Split variant**: Displayed in the thank you container after "Play Again" button
- **Overlay variant**: Displayed after "Play Again" button if available

#### Lead Collection

Capture user information before or after taking the personality quiz:

```tsx
import { LeadsOptions } from "fansunited-frontend-core";

const leads: LeadsOptions = {
  position: "before", // "before" | "after"
  fields: ["fullName", "email"], // Available: "fullName" | "firstName" | "lastName" | "email" | "gender" | "country" | "phoneCountryCode" | "phoneNumber"
  campaignId: "personality-quiz-2024",
  campaignName: "Personality Assessment Campaign",
  phoneCountryCode: "1", // Default country code for phone fields
  syncWithProfile: true, // Optional: sync form data to user profile after successful submission
};

<PersonalityQuizPlay {...otherProps} leads={leads} />;
```

#### Answer Explanations

Enable detailed explanations for quiz answers and persona matching:

```tsx
<PersonalityQuizPlay {...otherProps} showAnswerExplanations={true} />
```

When enabled, users will see:

- Explanations for each answer choice
- Detailed persona descriptions
- Matching algorithm insights
- Percentage breakdowns for each persona

#### Examples

##### Basic Personality Quiz

```tsx
import { PersonalityQuizPlay } from "fansunited-frontend-components";
import { WidgetTemplate } from "fansunited-frontend-core";

<PersonalityQuizPlay
  entityId="personality-quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
/>;
```

##### Advanced Personality Quiz

```tsx
<PersonalityQuizPlay
  entityId="personality-quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.OVERLAY}
  language="en"
  showAnswerExplanations={true}
  optionsLayout="twoByTwo"
  userIsLoggedIn={false}
  signInCTA={{
    defaultLabel: "Sign in to save results",
    onClick: () => handleSignIn(),
  }}
  leads={{
    position: "after",
    fields: ["fullName", "email"],
    campaignId: "personality-campaign",
    campaignName: "Personality Assessment 2024",
    phoneCountryCode: "44",
    syncWithProfile: true,
  }}
  themeOptions={{
    mode: "dark",
    colorSchemes: {
      dark: {
        textPrimary: "#ffffff",
        surface: "#1a1a1a",
      },
    },
  }}
/>
```

##### Customized Layout

```tsx
<PersonalityQuizPlay
  entityId="personality-quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  imagePosition="right"
  language="en"
  optionsLayout="column"
  showAnswerExplanations={true}
  themeOptions={{
    mode: "light",
    customFontFamily: {
      light: {
        primary: "Inter, sans-serif",
        secondary: "Roboto, sans-serif",
      },
    },
  }}
/>
```

### PickThePair

Interactive pair matching game component that challenges users to match related items from two columns. Features two game modes (casual and challenge), optional countdown timer, real-time accuracy tracking, and customizable templates. Perfect for educational quizzes, trivia games, and engaging brand experiences.

**Key Features:**

- Two game modes: Casual (relaxed matching) and Challenge (accuracy-based with success threshold)
- Optional countdown timer with configurable time limits
- Real-time accuracy tracking and statistics
- Visual feedback for correct/incorrect matches
- Dynamic image reveal on selection
- Three responsive templates (Standard, Landing Page, Overlay)
- Lead collection integration
- Custom theming and branding
- Mobile-optimized interface

#### Required Props

| Prop       | Type                    | Description                |
| ---------- | ----------------------- | -------------------------- |
| `entityId` | `string`                | Pick The Pair identifier   |
| `sdk`      | `FansUnitedSDKModel`    | SDK instance               |
| `template` | `PickThePairTemplate`   | Layout template            |
| `language` | `LanguageType`          | Display language           |

#### Optional Props

| Prop                         | Type                   | Description                               |
| ---------------------------- | ---------------------- | ----------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`   | Theme configuration                       |
| `leads`                      | `LeadsOptions`         | Lead collection settings                  |
| `imagePosition`              | `"left" \| "right"`    | Image position (STANDARD template only)   |
| `defaultImagePlaceholderUrl` | `string`               | URL for default image placeholder         |

#### Templates

```tsx
import { PickThePairTemplate, PickThePairProps } from "fansunited-frontend-core";

// Standard template - traditional card-based layout with optional image positioning
const standardProps: PickThePairProps = {
  template: PickThePairTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Landing Page template - full-width immersive experience
const landingPageProps: PickThePairProps = {
  template: PickThePairTemplate.LANDING_PAGE,
  // imagePosition not available for non-standard templates
  // ... other props
};

// Overlay template - full-screen overlay experience
const overlayProps: PickThePairProps = {
  template: PickThePairTemplate.OVERLAY,
  // imagePosition not available for non-standard templates
  // ... other props
};
```

**Template Differences:**

- **Standard**: Traditional card-based layout with clear separation between game area and branding. Supports `imagePosition` prop to control image placement.
- **Landing Page**: Full-width design optimized for standalone pages with prominent branding
- **Overlay**: Full-screen immersive experience with background image and overlay effects

#### Game Modes

PickThePair supports two distinct game modes:

**Casual Mode:**
- Relaxed matching experience without scoring pressure
- No accuracy requirements or success thresholds
- Players can take their time to match all pairs
- Perfect for educational content and brand awareness

**Challenge Mode:**
- Competitive experience with accuracy tracking
- Success threshold requirement (configurable percentage)
- Real-time statistics display showing:
  - Total attempts made
  - Current accuracy percentage
  - Success threshold indicator
  - Matched pairs count
- Players must meet the accuracy threshold to proceed
- Ideal for competitive campaigns and skill-based challenges

#### Countdown Timer

Enable time-limited challenges with the countdown timer feature:

**How it works:**
- Timer is configured in Info Pages Management in Pick The Pair configuration time limit (in seconds)
- Set to `0` for no time limit
- When `> 0`, a countdown timer is displayed
- Timer starts automatically when the game loads
- Visual countdown shows remaining time
- When time expires:
  - All selections are disabled
  - "Time's Up" (label timeUp) overlay is displayed
  - Players cannot continue matching
  - Submit button becomes disabled

**Timer Display:**
- Shows minutes and seconds in MM:SS format
- Updates in real-time
- Styled with branding colors
- Responsive design for mobile and desktop

#### Accuracy Tracking

In Challenge Mode, the component tracks and displays detailed statistics:

**Tracked Metrics:**
- **Total Attempts**: Number of match attempts made
- **Correct Matches**: Successfully matched pairs
- **Incorrect Attempts**: Failed match attempts
- **Accuracy Percentage**: (Correct Matches / Total Attempts) × 100
- **Success Threshold**: Required accuracy to proceed

**Visual Indicators:**
- Real-time accuracy display
- Color-coded threshold indicator (green when met, red when not met)
- Progress bar showing matched pairs vs total pairs
- Success/failure state based on threshold

**Behavior:**
- Players must achieve the required accuracy percentage to submit
- Submit button is disabled until threshold is met (in Challenge Mode)
- Statistics update in real-time after each match attempt

#### Matching Mechanics

**How Matching Works:**

1. **Selection**: Click an item from the left column and an item from the right column or vice-versa
2. **Validation**: The component checks if both items have the same pair ID
3. **Feedback**:
   - **Correct Match**: Items remain selected, images are revealed (if available), pair is marked as matched
   - **Incorrect Match**: Red error animation plays for 800ms, both selections are cleared
4. **Completion**: All pairs must be matched to enable the submit/continue button

**Visual States:**

- **Unselected**: Default state with option text visible
- **Selected**: Highlighted with primary color, image revealed (if available)
- **Matched**: Success color (green), image remains visible, item disabled
- **Error**: Danger color (red) animation on incorrect match
- **Disabled**: Grayed out when time is up or during error animation

**Image Behavior:**

- Images are hidden by default
- Revealed when an item is selected or matched
- Left column: Image appears on the right side of the text
- Right column: Image appears on the left side of the text
- Supports both desktop (`main`) and mobile (`mobile`) image variants
- Gracefully handles pairs without images

#### Lead Collection

Capture user information before or after the matching game:

```tsx
import { LeadsOptions } from "fansunited-frontend-core";

const leads: LeadsOptions = {
  position: "after", // only after is supported
  fields: ["fullName", "email"], // Available: "fullName" | "firstName" | "lastName" | "email" | "gender" | "country" | "phoneCountryCode" | "phoneNumber"
  campaignId: "pick-the-pair-campaign",
  campaignName: "Brand Matching Challenge",
  phoneCountryCode: "1", // Default country code for phone fields
};

<PickThePair {...otherProps} leads={leads} />;
```

**Lead Collection Features:**

- **Flexible Fields**: Choose which user information to collect
- **Profile Sync**: Optionally sync collected data to user profiles
- **Custom Branding**: Lead forms inherit game branding and theme
- **Success State**: Confirmation message after successful submission

#### Examples

##### Basic Pair Matching Game

```tsx
import { PickThePair } from "fansunited-frontend-components";
import { PickThePairTemplate } from "fansunited-frontend-core";

<PickThePair
  entityId="pair-matching-123"
  sdk={sdkInstance}
  template={PickThePairTemplate.STANDARD}
  language="en"
/>;
```

##### Challenge Mode with Timer

```tsx
// Backend configuration:
// {
//   "mode": "challenge",
//   "timeLimit": 120,
//   "successThresholdPercent": 75
// }

<PickThePair
  entityId="timed-challenge-123"
  sdk={sdkInstance}
  template={PickThePairTemplate.LANDING_PAGE}
  language="en"
  themeOptions={{
    mode: "dark",
  }}
/>
```

##### Complete Setup with Lead Collection

```tsx
<PickThePair
  entityId="brand-quiz-123"
  sdk={sdkInstance}
  template={PickThePairTemplate.OVERLAY}
  language="en"
  leads={{
    position: "after",
    fields: ["fullName", "email", "phoneNumber"],
    campaignId: "brand-awareness-2024",
    campaignName: "Product Knowledge Challenge",
    phoneCountryCode: "44",
    syncWithProfile: true,
  }}
  themeOptions={{
    mode: "light",
    colorSchemes: {
      light: {
        palette: {
          primary: {
            plainColor: "#FF5722",
            primaryContainer: "#FF7043",
          },
          success: {
            plainColor: "#4CAF50",
            softBg: "#E8F5E9",
          },
          danger: {
            plainColor: "#F44336",
            softBg: "#FFEBEE",
          },
        },
      },
    },
  }}
  defaultImagePlaceholderUrl="https://example.com/placeholder.png"
/>
```

##### Casual Mode for Education

```tsx
// Backend configuration:
// {
//   "mode": "casual",
//   "timeLimit": 0,
//   "successThresholdPercent": 0
// }

<PickThePair
  entityId="educational-pairs-123"
  sdk={sdkInstance}
  template={PickThePairTemplate.STANDARD}
  imagePosition="right"
  language="en"
  themeOptions={{
    mode: "light",
    customFontFamily: {
      light: {
        primary: "Poppins, sans-serif",
        secondary: "Open Sans, sans-serif",
      },
    },
  }}
/>
```

### MatchQuizPlay

Interactive match prediction quiz component that allows users to make predictions on various football match markets. Features multiple fixture types, real-time countdown timers, points system, and comprehensive result tracking with leaderboards.

**Key Features:**

- Multiple prediction markets (1X2, Correct Score, Player Markets, Over/Under Goals, Corners, etc.)
- Real-time countdown timer for prediction cutoff
- Points system with loyalty integration
- Leaderboard and ranking system
- Prediction summary and editing capabilities
- Three responsive templates (Standard, Split, Overlay)
- Lead collection integration
- Authentication support
- Custom theming and branding
- Detailed prediction results and explanations

#### Required Props

| Prop       | Type                 | Description           |
| ---------- | -------------------- | --------------------- |
| `entityId` | `string`             | Match Quiz identifier |
| `sdk`      | `FansUnitedSDKModel` | SDK instance          |
| `template` | `WidgetTemplate`     | Layout template       |
| `language` | `LanguageType`       | Display language      |

#### Optional Props

| Prop                         | Type                    | Description                                       |
| ---------------------------- | ----------------------- | ------------------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`    | Theme configuration                               |
| `leads`                      | `LeadsOptions`          | Lead collection settings                          |
| `imagePosition`              | `"left" \| "right"`     | Image position (STANDARD template only)           |
| `defaultImagePlaceholderUrl` | `string`                | URL for default image placeholder                 |
| `userIsLoggedIn`             | `boolean`               | Determine if the user is logged in                |
| `signInCTA`                  | `SignInCTADetails`      | Sign in call to action button configuration       |
| `additionalCTA`              | `AdditionalCTADetails`  | Additional call to action button configuration    |
| `showCountdown`              | `boolean`               | Show countdown timer when Match Quiz is open      |
| `showTeamLabels`             | `boolean`               | Show team name labels in fixture components       |
| `showPoints`                 | `boolean`               | Show points display for each fixture              |
| `showPredictionDetails`      | `boolean`               | Show detailed prediction results after completion |
| `rulesDisplay`               | `RulesDisplay`          | Match quiz rules display configuration            |

#### Templates

```tsx
import { WidgetTemplate, MatchQuizPlayProps } from "fansunited-frontend-core";

// Standard template with optional image positioning
const standardProps: MatchQuizPlayProps = {
  template: WidgetTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Split template - side-by-side layout
const splitProps: MatchQuizPlayProps = {
  template: WidgetTemplate.SPLIT,
  // imagePosition not available for non-standard templates
  // ... other props
};

// Overlay template - full-screen immersive experience
const overlayProps: MatchQuizPlayProps = {
  template: WidgetTemplate.OVERLAY,
  // ... other props
};
```

#### Supported Markets

MatchQuizPlay supports a wide variety of football prediction markets:

**Match Result Markets:**

- `FT_1X2` - Full Time 1X2 (Home Win, Draw, Away Win)
- `HT_1X2` - Half Time 1X2

**Score Markets:**

- `CORRECT_SCORE` - Exact final score prediction
- `CORRECT_SCORE_HT` - Exact half-time score prediction
- `CORRECT_SCORE_ADVANCED` - Advanced correct score with alternative points

**Player Markets:**

- `PLAYER_SCORE_FIRST_GOAL` - First goalscorer prediction
- `PLAYER_SCORE` - Anytime goalscorer
- `PLAYER_YELLOW_CARD` - Player to receive yellow card
- `PLAYER_RED_CARD` - Player to receive red card
- `PLAYER_SCORE_HATTRICK` - Player to score hat-trick
- `PLAYER_SCORE_TWICE` - Player to score twice

**Over/Under Markets:**

- `OVER_GOALS_0_5` to `OVER_GOALS_6_5` - Total goals over/under
- `OVER_CORNERS_6_5` to `OVER_CORNERS_13_5` - Total corners over/under

**Special Markets:**

- `CORNERS_MATCH` - Total corners in the match
- `PENALTY_MATCH` - Penalty awarded in the match
- `RED_CARD_MATCH` - Red card shown in the match

#### Game Status States

Match quizzes have 5 distinct status states:

- **`PENDING`** - Not yet open for predictions
- **`OPEN`** - Ready for predictions (default state)
- **`LIVE`** - Match has started, no new predictions accepted
- **`CLOSED`** - Match finished, backend resolving predictions
- **`SETTLED`** - Predictions resolved, leaderboard available

#### Countdown Timer

Enable countdown timer to show time remaining for predictions:

```tsx
<MatchQuizPlay {...otherProps} showCountdown={true} />
```

The countdown timer:

- Displays when Match Quiz status is `OPEN`
- Shows days, hours, minutes, and seconds remaining
- Uses `predictionsCutoffTime` from the Match Quiz data
- Automatically hides when cutoff time is reached

#### Points System

Enable points display to show potential rewards:

```tsx
<MatchQuizPlay {...otherProps} showPoints={true} />
```

Points system features:

- Shows potential points for each fixture
- Displays earned vs possible points
- Integrates with loyalty system configuration
- Supports alternative point structures for advanced markets
- Mobile-friendly tooltip display

#### Prediction Details

Show detailed prediction results and explanations:

```tsx
<MatchQuizPlay {...otherProps} showPredictionDetails={true} />
```

When enabled, users see:

- Tabulated interface
- Correct vs incorrect prediction highlighting
- Detailed market results
- Points breakdown per fixture

#### Sign in Configuration

Control user authentication and quiz access using the `userIsLoggedIn` and `signInCTA` props.

```tsx
import { SignInCTADetails } from "fansunited-frontend-core";

// Custom sign-in component
const signInCTA: SignInCTADetails = {
  component: <CustomSignInButton />,
};

// Basic sign-in with onClick handler
const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign in to predict",
  onClick: () => {
    // Handle sign-in logic
    console.log("Sign in clicked");
  },
};

// Sign-in with URL navigation
const signInCTA: SignInCTADetails = {
  defaultLabel: "Login to join the competition",
  url: "https://your-auth-provider.com/login",
  target: "_blank",
};

<MatchQuizPlay {...otherProps} userIsLoggedIn={false} signInCTA={signInCTA} />;
```

**Sign-in Priority Order:**

1. **Custom Component** - If `signInCTA.component` is provided, it will be rendered
2. **Click Handler** - If `signInCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `signInCTA.url` is provided, a button will be rendered and clicking will navigate to the URL
4. **Disabled** - If no `signInCTA` is provided, a disabled button will be shown

**Behavior:**

- When `userIsLoggedIn` is `false` and the quiz requires authentication (`authRequirement: "REGISTERED"`), the sign-in screen will be displayed instead of the quiz

#### Additional CTA Configuration

Add an extra call-to-action button to the match quiz results screen using the `additionalCTA` prop.

```tsx
import { AdditionalCTADetails } from "fansunited-frontend-core";

// Custom component
const additionalCTA: AdditionalCTADetails = {
  component: <CustomCTAButton />,
};

// Basic CTA with onClick handler
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "View Leaderboard",
  onClick: () => {
    // Handle click logic
    console.log("Additional CTA clicked");
  },
};

// CTA with URL navigation
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "Match Details",
  url: "https://your-website.com/match-details",
  target: "_blank", // or "_self"
};

<MatchQuizPlay
  {...otherProps}
  additionalCTA={additionalCTA}
/>;
```

**Priority Order:**

1. **Custom Component** - If `additionalCTA.component` is provided, it will be rendered
2. **Click Handler** - If `additionalCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `additionalCTA.url` is provided, a button will be rendered and clicking will navigate to the URL

**Placement:**

- **Standard variant**: Displayed on the same line as the branding logo
- **Split variant**: Displayed centered after main content
- **Overlay variant**: Displayed on the same line as the branding logo

#### Lead Collection

Capture user information before or after making predictions:

```tsx
import { LeadsOptions } from "fansunited-frontend-core";

const leads: LeadsOptions = {
  position: "before", // "before" | "after"
  fields: ["fullName", "email"], // Available: "fullName" | "firstName" | "lastName" | "email" | "gender" | "country" | "phoneCountryCode" | "phoneNumber"
  campaignId: "match-quiz-2024",
  campaignName: "Football Prediction Campaign",
  phoneCountryCode: "44", // Default country code for phone fields
  syncWithProfile: true, // Optional: sync form data to user profile after successful submission
};

<MatchQuizPlay {...otherProps} leads={leads} />;
```

#### Examples

##### Basic Match Quiz

```tsx
import { MatchQuizPlay } from "fansunited-frontend-components";
import { WidgetTemplate } from "fansunited-frontend-core";

<MatchQuizPlay
  entityId="match-quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
/>;
```

##### Advanced Match Quiz

```tsx
<MatchQuizPlay
  entityId="match-quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.OVERLAY}
  language="en"
  showCountdown={true}
  showTeamLabels={true}
  showPoints={true}
  showPredictionDetails={true}
  userIsLoggedIn={false}
  signInCTA={{
    defaultLabel: "Sign in to compete",
    onClick: () => handleSignIn(),
  }}
  leads={{
    position: "after",
    fields: ["fullName", "email"],
    campaignId: "match-prediction-campaign",
    campaignName: "Football Predictions 2024",
    phoneCountryCode: "44",
    syncWithProfile: true,
  }}
  themeOptions={{
    mode: "dark",
    colorSchemes: {
      dark: {
        textPrimary: "#ffffff",
        surface: "#1a1a1a",
      },
    },
  }}
/>
```

##### Customized Layout

```tsx
<MatchQuizPlay
  entityId="match-quiz-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  imagePosition="right"
  language="en"
  showCountdown={true}
  showTeamLabels={false}
  showPoints={true}
  themeOptions={{
    mode: "light",
    customFontFamily: {
      light: {
        primary: "Inter, sans-serif",
        secondary: "Roboto, sans-serif",
      },
    },
  }}
/>
```

### EventGamePlay

Interactive event prediction component for sports and entertainment events. Features flexible fixture types, real-time countdown timers, points system, and comprehensive result tracking with leaderboards.

**Key Features:**

- Multiple outcome types (Boolean, Number, Enum, Free Input)
- Real-time countdown timer for prediction cutoff
- Points system with loyalty integration
- Leaderboard and ranking system
- Prediction summary and editing capabilities
- Three responsive templates (Standard, Split, Overlay)
- Custom theming

#### Required Props

| Prop       | Type                 | Description           |
| ---------- | -------------------- | --------------------- |
| `entityId` | `string`             | Event Game identifier |
| `sdk`      | `FansUnitedSDKModel` | SDK instance          |
| `template` | `WidgetTemplate`     | Layout template       |
| `language` | `LanguageType`       | Display language      |

#### Optional Props

| Prop                         | Type                    | Description                                       |
| ---------------------------- | ----------------------- | ------------------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`    | Theme configuration                               |
| `leads`                      | `LeadsOptions`          | Lead collection settings                          |
| `imagePosition`              | `"left" \| "right"`     | Image position (STANDARD template only)           |
| `defaultImagePlaceholderUrl` | `string`                | URL for default image placeholder                 |
| `userIsLoggedIn`             | `boolean`               | Determine if the user is logged in                |
| `signInCTA`                  | `SignInCTADetails`      | Sign in call to action button configuration       |
| `additionalCTA`              | `AdditionalCTADetails`  | Additional call to action button configuration    |
| `showCountdown`              | `boolean`               | Show countdown timer when Event Game is open      |
| `showPoints`                 | `boolean`               | Show points display for each fixture              |
| `showPredictionDetails`      | `boolean`               | Show detailed prediction results after completion |
| `rulesDisplay`               | `RulesDisplay`          | Event game rules display configuration            |

#### Templates

```tsx
import { WidgetTemplate, EventGamePlayProps } from "fansunited-frontend-core";

// Standard template with optional image positioning
const standardProps: EventGamePlayProps = {
  template: WidgetTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Split template - side-by-side layout
const splitProps: EventGamePlayProps = {
  template: WidgetTemplate.SPLIT,
  // imagePosition not available for non-standard templates
  // ... other props
};

// Overlay template - full-screen immersive experience
const overlayProps: EventGamePlayProps = {
  template: WidgetTemplate.OVERLAY,
  // ... other props
};
```

#### Supported Outcome Types

EventGamePlay supports flexible prediction types for various event scenarios:

**Boolean Outcomes:**

- `BOOLEAN` - Yes/No predictions (e.g., "Will the total points exceed 225?")

**Number Outcomes:**

- `NUMBER` - Numeric predictions with optional min/max validation (e.g., "How many total points will be scored?")

**Enum Outcomes:**

- `ENUM` - Multiple choice selection from predefined options (e.g., "Who will be the game's leading scorer?")

**Free Input Outcomes:**

- `FREE_INPUT` - Open text input for custom predictions (e.g., "What will be the exact final score?")

#### Game Status States

Event games have 5 distinct status states:

- **`PENDING`** - Not yet open for predictions
- **`OPEN`** - Ready for predictions (default state)
- **`LIVE`** - Event has started, no new predictions accepted
- **`CLOSED`** - Event finished, backend resolving predictions
- **`SETTLED`** - Predictions resolved, leaderboard available

#### Countdown Timer

Enable countdown timer to show time remaining for predictions:

```tsx
<EventGamePlay {...otherProps} showCountdown={true} />
```

The countdown timer:

- Displays when Event Game status is `OPEN`
- Shows days, hours, minutes, and seconds remaining
- Uses `predictionsCutoff` from the Event Game data
- Automatically hides when cutoff time is reached

#### Points System

Enable points display to show potential rewards:

```tsx
<EventGamePlay {...otherProps} showPoints={true} />
```

Points system features:

- Shows potential points for each fixture
- Mobile-friendly tooltip display

#### Prediction Details

Show detailed prediction results and explanations:

```tsx
<EventGamePlay {...otherProps} showPredictionDetails={true} />
```

When enabled, users see:

- Tabulated interface showing all predictions
- Correct vs incorrect prediction highlighting
- Comparison with correct outcomes

#### Additional CTA Configuration

Add an extra call-to-action button to the event game results screen using the `additionalCTA` prop.

```tsx
import { AdditionalCTADetails } from "fansunited-frontend-core";

// Custom component
const additionalCTA: AdditionalCTADetails = {
  component: <CustomCTAButton />,
};

// Basic CTA with onClick handler
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "View Leaderboard",
  onClick: () => {
    // Handle click logic
    console.log("Additional CTA clicked");
  },
};

// CTA with URL navigation
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "Event Details",
  url: "https://your-website.com/event-details",
  target: "_blank", // or "_self"
};

<EventGamePlay
  {...otherProps}
  additionalCTA={additionalCTA}
/>;
```

**Priority Order:**

1. **Custom Component** - If `additionalCTA.component` is provided, it will be rendered
2. **Click Handler** - If `additionalCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `additionalCTA.url` is provided, a button will be rendered and clicking will navigate to the URL

**Placement:**

- **Standard variant**: Displayed after prediction summary
- **Split variant**: Displayed after prediction summary
- **Overlay variant**: Displayed after prediction summary

#### Examples

##### Basic Event Game

```tsx
import { EventGamePlay } from "fansunited-frontend-components";
import { WidgetTemplate } from "fansunited-frontend-core";

<EventGamePlay
  entityId="event-game-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
/>;
```

##### Advanced Event Game

```tsx
<EventGamePlay
  entityId="event-game-123"
  sdk={sdkInstance}
  template={WidgetTemplate.OVERLAY}
  language="en"
  showCountdown={true}
  showPoints={true}
  showPredictionDetails={true}
  themeOptions={{
    mode: "dark",
    colorSchemes: {
      dark: {
        textPrimary: "#ffffff",
        surface: "#1a1a1a",
      },
    },
  }}
/>
```

##### Customized Layout

```tsx
<EventGamePlay
  entityId="event-game-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  imagePosition="right"
  language="en"
  showCountdown={true}
  showPoints={true}
  themeOptions={{
    mode: "light",
    customFontFamily: {
      light: {
        primary: "Inter, sans-serif",
        secondary: "Roboto, sans-serif",
      },
    },
  }}
/>
```

### Discussion

Interactive discussion/commenting component that enables threaded conversations on any piece of content. Features real-time post creation, nested replies, reactions, reporting, moderation awareness, and multi-language support.

**Key Features:**

- Threaded 2-level comments (posts and replies)
- Four sort modes: Latest, Oldest, Popular, Most Replied
- Cursor-based pagination with configurable posts-per-page
- Post reporting with reason and optional details
- Delete own posts with confirmation dialog
- Authentication-aware UI (sign-in prompt for unauthenticated users)
- Moderated and deleted post handling
- Custom theming and multi-language support

#### Required Props

| Prop               | Type                 | Description                                                    |
| ------------------ | -------------------- | -------------------------------------------------------------- |
| `entityId`         | `string`             | Unique identifier for this discussion (e.g. article ID)        |
| `sdk`              | `FansUnitedSDKModel` | SDK instance                                                   |
| `language`         | `LanguageType`       | Display language                                               |

#### Optional Props

| Prop              | Type                   | Default      | Description                                             |
| ----------------- | ---------------------- | ------------ | ------------------------------------------------------- |
| `themeOptions`    | `CustomThemeOptions`   | —            | Theme configuration                                     |
| `discussionLabel` | `string`               | —            | Title/label used when the discussion is created for the first time |
| `discussionUrl`   | `string`               | —            | URL associated with the discussion content              |
| `defaultSort`     | `DiscussionSortType`   | `"LATEST"`   | Initial sort order for posts                            |
| `postsPerPage`    | `number`               | `20`         | Number of posts loaded per page                         |
| `showReactions`      | `boolean`              | `true`       | Show/hide reaction buttons on posts                     |
| `showReplies`        | `boolean`              | `true`       | Enable/disable reply functionality                      |
| `showReportPost`     | `boolean`              | `true`       | Show/hide the report option in the post menu            |
| `showModeratedPosts` | `boolean`              | `false`      | When `true`, moderated posts are fetched and displayed with a masked design — the original content is replaced by the moderation reason |
| `infiniteScroll`     | `boolean`              | `false`      | Load more posts automatically on scroll instead of showing a "Load More" button |
| `userIsLoggedIn`  | `boolean`              | `false`      | Whether the current user is authenticated               |
| `onSignInClick`   | `() => void`           | —            | Callback triggered when the sign-in prompt is clicked   |

#### Sort Modes

```tsx
import { DiscussionSortType } from "fansunited-frontend-core";

// Sort options
type DiscussionSortType = "LATEST" | "OLDEST" | "POPULAR" | "INTERACTED";
//                         ^newest   ^oldest   ^most       ^most
//                          first    first     reactions   replies
```

#### Authentication & Sign-in

Control user access and the sign-in prompt using the `userIsLoggedIn` and `onSignInClick` props.

```tsx
<Discussion
  {...otherProps}
  userIsLoggedIn={false}
  onSignInClick={() => {
    // Handle sign-in navigation or modal
    console.log("Sign in clicked");
  }}
/>
```

**Behavior:**

- When `userIsLoggedIn` is `false`, the post composer is replaced with a sign-in prompt
- Reactions, replies, and delete/report actions are also gated behind authentication
- Passing `onSignInClick` makes the sign-in button active; omitting it renders a disabled button

#### Reactions

Eight reaction types are available when `showReactions={true}` (the default):

```tsx
// Reaction types available on every post
type ReactionType = "LIKE" | "DISLIKE" | "LOVE" | "LAUGH" | "CARE" | "WOW" | "SAD" | "ANGRY";
```

Users can toggle their reaction by clicking an emoji. Re-clicking the same reaction removes it. Only authenticated users can react.

#### Theming

Customize the appearance using the same `CustomThemeOptions` interface shared across all components:

```tsx
import { CustomThemeOptions } from "fansunited-frontend-core";

const themeOptions: CustomThemeOptions = {
  mode: "dark",
  colorSchemes: {
    dark: {
      surface: "#1a1a1a",
      surfaceVariant: "#2a2a2a",
      outlineEnabledBorder: "#3a3a3a",
      textPrimary: "#FAFAFA",
      textSecondary: "#9E9E9E",
      textBody: "#E0E0E0",
      palette: {
        primary: {
          plainColor: "#1A77D2",
          onPrimary: "#FAFAFA",
        },
      },
    },
  },
};

<Discussion {...otherProps} themeOptions={themeOptions} />;
```

#### TypeScript Support

Import types from `fansunited-frontend-core`:

```tsx
import {
  DiscussionProps,
  DiscussionSortType,
  CustomThemeOptions,
  LanguageType,
} from "fansunited-frontend-core";
```

#### Examples

##### Basic Discussion

```tsx
import { Discussion } from "fansunited-frontend-components";

<Discussion
  entityId="article-123"
  sdk={sdkInstance}
  language="en"
  discussionLabel="Liverpool vs Man City — Match Reaction"
/>;
```

##### Authenticated User with All Features

```tsx
<Discussion
  entityId="article-123"
  sdk={sdkInstance}
  language="en"
  discussionLabel="Liverpool vs Man City — Match Reaction"
  discussionUrl="https://your-site.com/articles/liverpool-vs-man-city"
  userIsLoggedIn={true}
  defaultSort="POPULAR"
  postsPerPage={10}
  showReactions={true}
  showReplies={true}
  showReportPost={true}
  themeOptions={{
    mode: "light",
  }}
/>
```

##### Show Moderated Posts

Fetch and display moderated comments with a masked design. The original content is replaced by the moderation reason (or a localised fallback). All other post elements — avatar, username, timestamp, reactions, and reply button — remain visible.

```tsx
<Discussion
  entityId="article-123"
  sdk={sdkInstance}
  language="en"
  discussionLabel="Post Comments"
  userIsLoggedIn={true}
  showModeratedPosts={true}
/>
```

##### Minimal Read-Only Style

Disable reactions, replies, and reporting for a simplified read-only-like experience:

```tsx
<Discussion
  entityId="article-123"
  sdk={sdkInstance}
  language="en"
  discussionLabel="Post Comments"
  userIsLoggedIn={false}
  showReactions={false}
  showReplies={false}
  showReportPost={false}
  onSignInClick={() => openSignInModal()}
/>
```

##### Dark Theme with Sign-in Integration

```tsx
<Discussion
  entityId="match-456"
  sdk={sdkInstance}
  language="en"
  discussionLabel="Match Day Chat"
  userIsLoggedIn={isUserLoggedIn}
  onSignInClick={() => router.push("/login")}
  defaultSort="LATEST"
  postsPerPage={20}
  themeOptions={{
    mode: "dark",
    colorSchemes: {
      dark: {
        surface: "#212121",
        textPrimary: "#FAFAFA",
        palette: {
          primary: {
            plainColor: "#2397F3",
            primaryContainer: "#1A77D2",
            onPrimary: "#FFFFFF",
          },
        },
      },
    },
  }}
/>
```

---

### ChanceGame

Interactive chance-based game component featuring three distinct game variants — Prize Wheel, Penalty Shootout, and Pick One of X. Driven entirely by InfoPage configuration, it supports flexible selection modes, configurable animation speed, per-segment result screens with CTAs, and optional visual decorations.

**Key Features:**

- Three game variants: Prize Wheel, Penalty Shootout, and Pick One of X
- Three responsive templates (Embed, Standard, Overlay)
- Three selection modes: fair (equal odds), weighted (probability-based), and win (always a winner)
- Configurable animation speed (fast, normal, slow)
- Per-segment result screens with primary and secondary CTAs
- Optional visual decorations
- Custom branding via InfoPage configuration (colors, logos, background images)
- Shadow DOM style isolation — no CSS conflicts with the host application

#### Required Props

| Prop       | Type                 | Description               |
| ---------- | -------------------- | ------------------------- |
| `entityId` | `string`             | Info Page identifier      |
| `sdk`      | `FansUnitedSDKModel` | SDK instance              |
| `template` | `ChanceGameTemplate` | Layout template           |
| `variant`  | `ChanceGameVariant`  | Game variant              |
| `language` | `LanguageType`       | Display language          |

#### Optional Props

| Prop                         | Type                 | Description                               |
| ---------------------------- | -------------------- | ----------------------------------------- |
| `themeOptions`               | `CustomThemeOptions` | Theme configuration                       |
| `defaultImagePlaceholderUrl` | `string`             | URL for default image placeholder         |
| `imagePosition`              | `"left" \| "right"`  | Image position (STANDARD template only)   |

#### Templates

```tsx
import { ChanceGameTemplate, ChanceGameVariant, ChanceGameProps } from "fansunited-frontend-core";

// Embed template — compact, inline layout
const embedProps: ChanceGameProps = {
  template: ChanceGameTemplate.EMBED,
  variant: ChanceGameVariant.PRIZE_WHEEL,
  // ... other props
};

// Standard template — card-based layout with optional side image
const standardProps: ChanceGameProps = {
  template: ChanceGameTemplate.STANDARD,
  variant: ChanceGameVariant.PICK_ONE_OF_X,
  imagePosition: "left", // or "right"
  // ... other props
};

// Overlay template — full-screen immersive experience (always dark mode)
const overlayProps: ChanceGameProps = {
  template: ChanceGameTemplate.OVERLAY,
  variant: ChanceGameVariant.PRIZE_WHEEL,
  // imagePosition not available for non-standard templates
  // ... other props
};
```

**Template Differences:**

- **Embed**: Compact radial-gradient layout optimized for inline widgets, sidebars, and embedded content. Fits the container width automatically.
- **Standard**: Card-based layout with a configurable side image. Supports `imagePosition` to control image placement.
- **Overlay**: Full-screen background overlay with a frosted-glass content container. Always renders in dark mode.

#### Game Variants

**Prize Wheel** (`ChanceGameVariant.PRIZE_WHEEL`)

An animated spinning wheel where users click a button to spin and reveal their outcome. Supported in all three templates.

- Animated wheel spin with configurable pace
- Confetti animation on winning outcomes
- Shake animation on losing outcomes
- Outer ring decoration and pointer indicator

**Penalty Shootout** (`ChanceGameVariant.PENALTY_SHOOTOUT`)

A soccer penalty kick game where users click a target on the goal to shoot. Only supported with the **Embed** template.

- Soccer stadium field visualization
- Ball trajectory animations

**Pick One of X** (`ChanceGameVariant.PICK_ONE_OF_X`)

A card or object picking game where users select one item to reveal their outcome. Supported in all three templates. Operates in two display modes automatically determined by segment configuration:

- **Flip card mode** (no `images` on segments) — Mystery cards with a "?" that flip to reveal a label
- **Object mode** (segments include `images.idle`) — Custom objects (e.g. treasure chests) that transition from idle to revealed state with a crossfade animation

When `revealAll: true` is set in the InfoPage config, remaining items are revealed one-by-one after the user picks, using staggered 200 ms delays.

#### Game Configuration

All game logic is configured in the InfoPage body as a JSON string. This includes the segment definitions and the game behavior config:

```json
{
  "segments": [
    {
      "id": "segment-1",
      "label": "+50 POINTS",
      "isWinning": true,
      "weight": 3,
      "color": "#4CAF50",
      "images": {
        "idle": "https://example.com/chest-closed.png",
        "revealed": "https://example.com/chest-open.png"
      },
      "results": {
        "title": "YOU WON!",
        "description": "Congratulations! Your reward is on its way.",
        "primaryCTA": {
          "title": "YOUR REWARD",
          "description": "Claim your 50 bonus points now.",
          "label": "Claim Reward",
          "link": "https://example.com/claim"
        },
        "secondaryCTA": {
          "label": "Play Again",
          "link": "https://example.com/play"
        }
      }
    },
    {
      "id": "segment-2",
      "label": "TRY AGAIN",
      "isWinning": false,
      "weight": 7,
      "results": {
        "title": "BETTER LUCK NEXT TIME",
        "primaryCTA": {
          "label": "Try Again",
          "link": "https://example.com/play"
        }
      }
    }
  ],
  "config": {
    "mode": "weighted",
    "pace": "normal",
    "decorations": false,
    "revealAll": false
  }
}
```

**Segment Fields:**

| Field     | Type            | Required | Description                                             |
| --------- | --------------- | -------- | ------------------------------------------------------- |
| `id`      | `string`        | Yes      | Unique segment identifier                               |
| `label`   | `string`        | Yes      | Text shown on the segment (e.g. "+50 POINTS")           |
| `isWinning` | `boolean`     | Yes      | Whether this segment is a winning outcome               |
| `weight`  | `number`        | Yes      | Relative probability weight (used in `weighted` mode)   |
| `color`   | `string`        | No       | Custom hex color for the segment                        |
| `images`  | `object`        | No       | `idle` and optional `revealed` image URLs (Pick One of X) |
| `results` | `SegmentResult` | No       | Result screen shown after the animation completes       |

**SegmentResult Fields:**

| Field          | Type          | Required | Description                        |
| -------------- | ------------- | -------- | ---------------------------------- |
| `title`        | `string`      | Yes      | Main headline on the result screen |
| `description`  | `string`      | No       | Supporting text (supports `\n`)    |
| `primaryCTA`   | `SegmentCTA`  | No       | Featured action button             |
| `secondaryCTA` | `SegmentCTA`  | No       | Secondary action button            |

**SegmentCTA Fields:**

| Field         | Type     | Required | Description                         |
| ------------- | -------- | -------- | ----------------------------------- |
| `label`       | `string` | Yes      | Button label text                   |
| `link`        | `string` | Yes      | URL the button navigates to         |
| `title`       | `string` | No       | Section title above the button      |
| `description` | `string` | No       | Descriptive text above the button   |

#### Selection Modes

| Mode       | Behavior                                                                 |
| ---------- | ------------------------------------------------------------------------ |
| `fair`     | All segments have equal probability regardless of their `weight` value   |
| `weighted` | Probability is proportional to each segment's `weight` value             |
| `win`      | The outcome is always selected from segments where `isWinning` is `true` |

#### Config Options

| Option        | Type      | Default    | Description                                                         |
| ------------- | --------- | ---------- | ------------------------------------------------------------------- |
| `mode`        | `string`  | `weighted` | Selection algorithm: `fair`, `weighted`, or `win`                   |
| `pace`        | `string`  | `normal`   | Animation speed: `fast`, `normal`, or `slow`                        |
| `decorations` | `boolean` | `false`    | Enable visual effects (glowing title, frame, patterns) |
| `revealAll`   | `boolean` | `false`    | Pick One of X only — reveal remaining items after selection         |

#### Examples

##### Basic Prize Wheel

```tsx
import { ChanceGame } from "fansunited-frontend-components";
import { ChanceGameTemplate, ChanceGameVariant } from "fansunited-frontend-core";

<ChanceGame
  entityId="info-page-123"
  sdk={sdkInstance}
  template={ChanceGameTemplate.EMBED}
  variant={ChanceGameVariant.PRIZE_WHEEL}
/>;
```

##### Pick One of X with Object Mode

Configure segments in the InfoPage body with `images.idle` (and optionally `images.revealed`) to switch the component into object mode automatically:

```tsx
<ChanceGame
  entityId="info-page-456"
  sdk={sdkInstance}
  template={ChanceGameTemplate.STANDARD}
  variant={ChanceGameVariant.PICK_ONE_OF_X}
  imagePosition="right"
/>;
```

```json
// InfoPage body (configured in the backend)
{
  "segments": [
    {
      "id": "chest-win",
      "label": "JACKPOT",
      "isWinning": true,
      "weight": 1,
      "images": {
        "idle": "https://example.com/chest-closed.png",
        "revealed": "https://example.com/chest-open-gold.png"
      },
      "results": {
        "title": "YOU HIT THE JACKPOT!",
        "primaryCTA": { "label": "Claim Prize", "link": "https://example.com/claim" }
      }
    },
    {
      "id": "chest-lose-1",
      "label": "EMPTY",
      "isWinning": false,
      "weight": 4,
      "images": {
        "idle": "https://example.com/chest-closed.png",
        "revealed": "https://example.com/chest-open-empty.png"
      },
      "results": {
        "title": "NOTHING HERE...",
        "primaryCTA": { "label": "Try Again", "link": "https://example.com/play" }
      }
    }
  ],
  "config": { "mode": "weighted", "pace": "normal", "revealAll": true }
}
```

##### Fully Configured with Custom Theme

```tsx
<ChanceGame
  entityId="info-page-789"
  sdk={sdkInstance}
  template={ChanceGameTemplate.OVERLAY}
  variant={ChanceGameVariant.PRIZE_WHEEL}
  language="en"
  themeOptions={{
    mode: "dark",
    colorSchemes: {
      dark: {
        palette: {
          primary: {
            plainColor: "#FFD700",
            primaryContainer: "#FFC107",
          },
          success: {
            plainColor: "#4CAF50",
            softBg: "#042F04",
          },
        },
        surface: "#1a1a1a",
        textPrimary: "#FAFAFA",
      },
    },
  }}
/>;
```

### EitherOrPlay

Interactive Either/Or game component where users choose between two options across multiple rounds. Features a tutorial screen, timed gameplay, score tracking, optional standings panel, and customizable templates. Perfect for fan engagement, opinion battles, and brand preference campaigns.

**Key Features:**

- Side-by-side binary choice gameplay (option A vs. option B)
- Tutorial screen before gameplay starts
- Countdown timer per round
- Score tracking and results screen
- Optional standings/leaderboard panel
- Two responsive templates (Standard, Overlay)
- Lead collection integration
- Authentication support
- Custom theming and branding

#### Required Props

| Prop       | Type                 | Description             |
| ---------- | -------------------- | ----------------------- |
| `entityId` | `string`             | Either/Or game identifier |
| `sdk`      | `FansUnitedSDKModel` | SDK instance            |
| `template` | `WidgetTemplate`     | Layout template         |
| `language` | `LanguageType`       | Display language        |

#### Optional Props

| Prop                         | Type                    | Description                                       |
| ---------------------------- | ----------------------- | ------------------------------------------------- |
| `themeOptions`               | `CustomThemeOptions`    | Theme configuration                               |
| `leads`                      | `LeadsOptions`          | Lead collection settings                          |
| `imagePosition`              | `"left" \| "right"`     | Image position (STANDARD template only)           |
| `defaultImagePlaceholderUrl` | `string`                | URL for default image placeholder                 |
| `userIsLoggedIn`             | `boolean`               | Determine if the user is logged in                |
| `signInCTA`                  | `SignInCTADetails`      | Sign in call to action button configuration       |
| `additionalCTA`              | `AdditionalCTADetails`  | Additional call to action button configuration    |
| `rulesDisplay`               | `RulesDisplay`          | Game rules display configuration                  |
| `showStandings`              | `boolean`               | Show standings/leaderboard panel after results    |

#### Templates

```tsx
import { WidgetTemplate, EitherOrPlayProps } from "fansunited-frontend-core";

// Standard template with optional image positioning
const standardProps: EitherOrPlayProps = {
  template: WidgetTemplate.STANDARD,
  imagePosition: "left", // or 'right'
  // ... other props
};

// Overlay template - full-screen immersive experience
const overlayProps: EitherOrPlayProps = {
  template: WidgetTemplate.OVERLAY,
  // imagePosition not available for non-standard templates
  // ... other props
};
```

#### Standings Panel

Show a leaderboard after the results screen using the `showStandings` prop:

```tsx
<EitherOrPlay
  {...otherProps}
  showStandings={true}
/>
```

When enabled, a standings panel is displayed on the results screen showing how the user ranks compared to other participants.

#### Sign in Configuration

Control user authentication and game access using the `userIsLoggedIn` and `signInCTA` props.

```tsx
import { SignInCTADetails } from "fansunited-frontend-core";

// Basic sign-in with onClick handler
const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign In",
  onClick: () => {
    // Handle sign-in logic
    console.log("Sign in clicked");
  },
};

// Sign-in with URL navigation
const signInCTA: SignInCTADetails = {
  defaultLabel: "Login",
  url: "https://your-auth-provider.com/login",
  target: "_blank", // or "_self"
};

// Custom sign-in component
const signInCTA: SignInCTADetails = {
  component: <CustomSignInButton />,
};

<EitherOrPlay
  {...otherProps}
  userIsLoggedIn={false}
  signInCTA={signInCTA}
/>;
```

**Sign-in Priority Order:**

1. **Custom Component** - If `signInCTA.component` is provided, it will be rendered
2. **Click Handler** - If `signInCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `signInCTA.url` is provided, a button will be rendered and clicking will navigate to the URL
4. **Disabled** - If no `signInCTA` is provided, a disabled button will be shown

**Behavior:**

- When `userIsLoggedIn` is `false` and the game requires authentication (`authRequirement: "REGISTERED"`), the sign-in screen will be displayed instead of the game

#### Additional CTA Configuration

Add an extra call-to-action button to the results screen using the `additionalCTA` prop.

```tsx
import { AdditionalCTADetails } from "fansunited-frontend-core";

// Custom component
const additionalCTA: AdditionalCTADetails = {
  component: <CustomCTAButton />,
};

// Basic CTA with onClick handler
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "See More",
  onClick: () => {
    console.log("Additional CTA clicked");
  },
};

// CTA with URL navigation
const additionalCTA: AdditionalCTADetails = {
  defaultLabel: "Visit Website",
  url: "https://your-website.com",
  target: "_blank", // or "_self"
};

<EitherOrPlay
  {...otherProps}
  additionalCTA={additionalCTA}
/>;
```

**Priority Order:**

1. **Custom Component** - If `additionalCTA.component` is provided, it will be rendered
2. **Click Handler** - If `additionalCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `additionalCTA.url` is provided, a button will be rendered and clicking will navigate to the URL

#### Lead Collection

Capture user information before or after playing the game:

```tsx
import { LeadsOptions } from "fansunited-frontend-core";

const leads: LeadsOptions = {
  position: "before", // "before" | "after"
  fields: ["fullName", "email"], // Available: "fullName" | "firstName" | "lastName" | "email" | "gender" | "country" | "phoneCountryCode" | "phoneNumber"
  campaignId: "either-or-campaign-2024",
  campaignName: "Fan Choice Campaign",
  phoneCountryCode: "44", // Default country code for phone fields
  syncWithProfile: true, // Optional: sync form data to user profile after successful submission
};

<EitherOrPlay {...otherProps} leads={leads} />;
```

#### Examples

##### Basic Either/Or Game

```tsx
import { EitherOrPlay } from "fansunited-frontend-components";
import { WidgetTemplate } from "fansunited-frontend-core";

<EitherOrPlay
  entityId="either-or-123"
  sdk={sdkInstance}
  template={WidgetTemplate.STANDARD}
  language="en"
/>;
```

##### Customized Either/Or Game

```tsx
<EitherOrPlay
  entityId="either-or-123"
  sdk={sdkInstance}
  template={WidgetTemplate.OVERLAY}
  language="en"
  showStandings={true}
  userIsLoggedIn={false}
  signInCTA={{
    defaultLabel: "Sign in to play",
    onClick: () => handleSignIn(),
  }}
  leads={{
    position: "after",
    fields: ["fullName", "email"],
    campaignId: "either-or-campaign",
    campaignName: "Fan Preference Survey 2024",
    phoneCountryCode: "44",
    syncWithProfile: true,
  }}
  themeOptions={{
    mode: "dark",
  }}
/>
```

### Leaderboard

Ranked leaderboard component for displaying player standings across one or more game entities. Supports Classic Quizzes, Match Quizzes, Top X games, and Template-based games with paginated results, user rank highlighting, and an optional entity selector dropdown.

**Key Features:**

- Multi-entity support with dropdown navigation
- Aggregated "Overall" ranking when multiple Classic Quiz entities are provided
- Highlighted current user rank
- Paginated results (configurable page size)
- Custom branding colors sourced from each entity
- Light and dark theme support

#### Required Props

| Prop       | Type                  | Description                                       |
| ---------- | --------------------- | ------------------------------------------------- |
| `entities` | `LeaderboardEntity[]` | One or more game entities to display rankings for |
| `sdk`      | `FansUnitedSDKModel`  | SDK instance                                      |
| `language` | `LanguageType`        | Display language                                  |

#### Optional Props

| Prop           | Type                 | Description                                       |
| -------------- | -------------------- | ------------------------------------------------- |
| `themeOptions` | `CustomThemeOptions` | Theme configuration                               |
| `pageSize`     | `number`             | Number of entries per page (integer between 1–50) |

#### Entity Configuration

Each entry in the `entities` array must be a `LeaderboardEntity` object with the following shape:

```tsx
import { LeaderboardEntity } from "fansunited-frontend-core";

const entity: LeaderboardEntity = {
  id: "your-entity-id",   // Required: string identifier
  type: "CLASSIC_QUIZ",   // Required: case-insensitive entity type
};
```

**Supported entity types** (case-insensitive):

| Type           | Description                         |
| -------------- | ----------------------------------- |
| `CLASSIC_QUIZ` | Classic Quiz game leaderboard       |
| `MATCH_QUIZ`   | Match Quiz prediction leaderboard   |
| `TOP_X`        | Top X picks game leaderboard        |
| `TEMPLATE`     | Template-based game leaderboard     |

#### Multi-Entity Behavior

When more than one entity is provided, a dropdown selector is shown so users can switch between leaderboards:

- **Mixed game types**: The first entity is selected by default. Users can switch via the dropdown.
- **Multiple Classic Quiz entities**: An extra **"Overall"** option is prepended to the dropdown, aggregating rankings across all Classic Quiz entities. The "Overall" view is selected by default.
- **Single entity**: No dropdown is shown; the entity title and description are displayed as a heading instead.

#### Examples

##### Single-Entity Leaderboard

```tsx
import { Leaderboard } from "fansunited-frontend-components";

<Leaderboard
  entities={[{ id: "quiz-123", type: "classic_quiz" }]}
  sdk={sdkInstance}
  language="en"
/>;
```

##### Multiple Classic Quiz Entities (with Overall)

```tsx
import { Leaderboard } from "fansunited-frontend-components";

<Leaderboard
  entities={[
    { id: "quiz-123", type: "classic_quiz" },
    { id: "quiz-456", type: "classic_quiz" },
  ]}
  sdk={sdkInstance}
  language="en"
  pageSize={20}
/>;
```

##### Mixed Game Types

```tsx
<Leaderboard
  entities={[
    { id: "topx-789", type: "top_x" },
    { id: "match-quiz-321", type: "match_quiz" },
    { id: "quiz-123", type: "classic_quiz" },
    { id: "template-654", type: "template" },
  ]}
  sdk={sdkInstance}
  language="en"
  pageSize={10}
  themeOptions={{
    mode: "dark",
  }}
/>
```

### Predictor

Full-featured football score predictor component with a multi-tab interface for match predictions, global leaderboard, private leagues, game rules, and prizes. Designed as a season-long engagement tool for sports platforms.

**Key Features:**

- Multi-tab navigation: Play, Leaderboard, Private Leagues, Rules, and Prizes
- Match score prediction interface with grouped matchweeks
- Global leaderboard with paginated standings and user rank highlighting
- Private leagues — create and join custom competitive groups
- Consent management with required/optional consent gates before predicting
- Customisable banners in 6 positions within the Play tab
- Authentication-aware UI with configurable sign-in CTA
- Built-in Betslip integration — renders the Betslip widget automatically via the `betslip` prop with two trigger modes
- Custom theming and multi-language support

#### Required Props

| Prop       | Type                 | Description                      |
| ---------- | -------------------- | -------------------------------- |
| `entityId` | `string`             | Predictor template identifier    |
| `sdk`      | `FansUnitedSDKModel` | SDK instance                     |
| `language` | `LanguageType`       | Display language                 |

#### Optional Props

| Prop                         | Type                 | Description                                         |
| ---------------------------- | -------------------- | --------------------------------------------------- |
| `themeOptions`               | `CustomThemeOptions` | Theme configuration                                 |
| `userIsLoggedIn`             | `boolean`            | Determine if the user is logged in (default: false) |
| `signInCTA`                  | `SignInCTADetails`   | Sign in call to action button configuration         |
| `tabs`                       | `PredictorTab[]`     | Tabs to enable (default: all tabs)                  |
| `defaultImagePlaceholderUrl` | `string`             | Fallback image URL for the hero header              |
| `playTabBanners`             | `PlayTabBanner[]`    | Custom banners rendered in the Play tab             |
| `consents`                   | `ConsentDef[]`       | Consent definitions required before predicting      |
| `matchCardBgImageUrl`        | `string`                 | Background image URL for match prediction cards                                              |
| `betslip`                    | `PredictorBetslipConfig` | Betslip integration config. Omit to disable. See [Betslip Integration](#betslip-integration) |

#### Tabs

Configure which tabs are visible using the `tabs` prop. All tabs are enabled by default.

```tsx
import { PredictorTab } from "fansunited-frontend-core";

type PredictorTab = "play" | "leaderboard" | "private-leagues" | "rules" | "prizes";

// Show only the Play and Leaderboard tabs
<Predictor
  {...otherProps}
  tabs={["play", "leaderboard"]}
/>
```

| Tab                | Description                                               |
| ------------------ | --------------------------------------------------------- |
| `"play"`           | Main prediction interface — submit match score forecasts  |
| `"leaderboard"`    | Global rankings and user standings with pagination        |
| `"private-leagues"` | Create and join private leagues to compete with friends  |
| `"rules"`          | Searchable game rules and scoring system                  |
| `"prizes"`         | Prize distribution and rewards information                |

#### Sign in Configuration

Control user authentication using the `userIsLoggedIn` and `signInCTA` props.

```tsx
import { SignInCTADetails } from "fansunited-frontend-core";

// Basic sign-in with onClick handler
const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign In to Predict",
  onClick: () => {
    // Handle sign-in logic
    console.log("Sign in clicked");
  },
};

// Sign-in with URL navigation
const signInCTA: SignInCTADetails = {
  defaultLabel: "Login to Compete",
  url: "https://your-auth-provider.com/login",
  target: "_blank", // or "_self"
};

// Custom sign-in component
const signInCTA: SignInCTADetails = {
  component: <CustomSignInButton />,
};

<Predictor {...otherProps} userIsLoggedIn={false} signInCTA={signInCTA} />;
```

**Sign-in Priority Order:**

1. **Custom Component** - If `signInCTA.component` is provided, it will be rendered
2. **Click Handler** - If `signInCTA.onClick` is provided, a button with the handler will be rendered
3. **URL Navigation** - If `signInCTA.url` is provided, a button will be rendered and clicking will navigate to the URL
4. **Disabled** - If no `signInCTA` is provided, a disabled button will be shown

#### Play Tab Banners

Inject custom banner content into specific positions inside the Play tab using the `playTabBanners` prop.

```tsx
import { PlayTabBanner, PlayTabBannerPosition } from "fansunited-frontend-core";

const playTabBanners: PlayTabBanner[] = [
  {
    position: "above-group-nav",
    render: () => <PromoBanner campaign="matchday-sponsor" />,
  },
  {
    position: "below-matches",
    render: () => <AdSlot id="predictor-bottom" />,
  },
];

<Predictor {...otherProps} playTabBanners={playTabBanners} />;
```

**Available Positions:**

| Position             | Description                                          |
| -------------------- | ---------------------------------------------------- |
| `"above-group-nav"`  | Above the matchweek group navigation                 |
| `"below-group-nav"`  | Below the matchweek group navigation                 |
| `"below-matches"`    | Below the list of match prediction cards             |
| `"bottom"`           | At the very bottom of the Play tab                   |
| `"left"`             | Left sidebar (desktop layouts)                       |
| `"right"`            | Right sidebar (desktop layouts)                      |

#### Consent Configuration

Require users to accept consent agreements before submitting predictions using the `consents` prop.

```tsx
import { ConsentDef } from "fansunited-frontend-core";

const consents: ConsentDef[] = [
  {
    consentId: "terms_of_service",
    body: 'I agree to the <a href="https://example.com/tos" target="_blank">Terms of Service</a>.',
    required: true,
    defaultChecked: false,
  },
  {
    consentId: "marketing",
    body: "I'd like to receive promotional emails and offers.",
    required: false,
    defaultChecked: false,
  },
];

<Predictor {...otherProps} consents={consents} />;
```

**Consent Fields:**

| Field            | Type      | Description                                                       |
| ---------------- | --------- | ----------------------------------------------------------------- |
| `consentId`      | `string`  | Unique identifier for the consent (e.g. `"tos"`, `"marketing"`)   |
| `body`           | `string`  | HTML content of the consent label (supports links and formatting) |
| `required`       | `boolean` | When `true`, the user must accept before they can predict         |
| `defaultChecked` | `boolean` | Pre-check the consent checkbox on load                            |

**Behavior:**

- Required consents are gated behind a modal that appears before the first prediction submission
- Optional consents are shown in the same modal but do not block progression
- Once accepted, consents are stored and the modal does not reappear

#### Betslip Integration

When the `betslip` prop is provided, the Predictor automatically renders a `Betslip` widget as a sibling alongside its own UI — you do not need to add a separate `<Betslip />` component to the page. The widget is configured entirely through `PredictorBetslipConfig`.

```tsx
import { PredictorBetslipConfig } from "fansunited-frontend-core";

const betslipConfig: PredictorBetslipConfig = {
  trigger: "predictions-only",
  position: "side-right",
  currency: "£",
  stakePresets: [5, 10, 25, 50],
  ctaUrlTemplate:
    "https://your-bookmaker.com/bet?ids={selectionIds}&stake={stake}&ref={currentUrl}",
};

<Predictor {...otherProps} betslip={betslipConfig} />;
```

**`PredictorBetslipConfig` fields:**

| Field                 | Type                      | Default              | Description                                                                               |
| --------------------- | ------------------------- | -------------------- | ----------------------------------------------------------------------------------------- |
| `trigger`             | `PredictorBetslipTrigger` | `"predictions-only"` | When to send selections to the betslip                                                    |
| `position`            | `BetslipPosition`         | `"bottom-right"`     | Viewport position of the Betslip widget                                                   |
| `maxSelections`       | `number`                  | `20`                 | Hard cap on simultaneous selections                                                       |
| `stakePresets`        | `number[]`                | `[5, 10, 20]`        | Quick-select stake buttons                                                                |
| `oddsPollingInterval` | `number`                  | `30000`              | Odds refresh interval in milliseconds                                                     |
| `currency`            | `string`                  | `"€"`                | Currency symbol                                                                           |
| `ctaUrlTemplate`      | `string`                  | —                    | Bookmaker deep-link URL with `{placeholder}` substitution                                 |
| `brandingLogoUrl`     | `string`                  | —                    | Operator logo URL                                                                         |
| `labels`              | `BetslipLabels`           | —                    | UI text overrides                                                                         |
| `themeOptions`        | `CustomThemeOptions`      | —                    | Theme for the Betslip widget. Falls back to the Predictor's `themeOptions` when omitted   |

**Trigger modes (`PredictorBetslipTrigger`):**

```tsx
type PredictorBetslipTrigger =
  | "predictions-only" // score changes feed betslip (before submit)
  | "odds-only";       // odds button click feeds betslip (after submit)
```

| Mode                  | When selections are sent                                                                                                                                                                                                                       |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"predictions-only"`  | Every time the user edits a score (home/away increment or decrement). If the user submits without editing (e.g. a default 0:0), the selection is sent at submit time as a fallback. Derived outcome: `"1"` (home win), `"X"` (draw), `"2"` (away win). |
| `"odds-only"`         | Only when the user clicks an odds button on the post-submit odds display. The odds button becomes a command-bus trigger instead of a direct bookmaker link.                                                                                     |

**Selection format sent by Predictor:**

```
"{matchId}:FT_1X2:{outcomeKey}"
// Examples:
// "fb:m:451634:FT_1X2:1"  → home win
// "fb:m:451634:FT_1X2:X"  → draw
// "fb:m:451634:FT_1X2:2"  → away win
```

**Automatic removal:** When a user deletes a submitted prediction, the corresponding selection is automatically removed from the betslip via `betslipApi.removeSelection()`. When a score is edited after submission (in `"predictions-only"` mode), the new outcome replaces the old one in the betslip (upsert — no explicit removal needed).

**Theme fallback:** If `betslip.themeOptions` is not provided, the Betslip widget inherits the Predictor's own `themeOptions`. A single `themeOptions` on the Predictor is enough to theme both components consistently.

#### TypeScript Support

Import types from `fansunited-frontend-core`:

```tsx
import {
  PredictorProps,
  PredictorTab,
  PredictorBetslipConfig,
  PredictorBetslipTrigger,
  PlayTabBanner,
  PlayTabBannerPosition,
  ConsentDef,
  SignInCTADetails,
  CustomThemeOptions,
  LanguageType,
} from "fansunited-frontend-core";
```

#### Examples

##### Basic Predictor

```tsx
import { Predictor } from "fansunited-frontend-components";

<Predictor
  entityId="predictor-template-123"
  sdk={sdkInstance}
  language="en"
/>;
```

##### Authenticated with Selected Tabs

```tsx
<Predictor
  entityId="predictor-template-123"
  sdk={sdkInstance}
  language="en"
  userIsLoggedIn={true}
  tabs={["play", "leaderboard", "private-leagues"]}
  matchCardBgImageUrl="https://your-cdn.com/stadium-bg.jpg"
  themeOptions={{
    mode: "dark",
  }}
/>
```

##### Full Setup with Banners and Consents

```tsx
<Predictor
  entityId="predictor-template-123"
  sdk={sdkInstance}
  language="en"
  userIsLoggedIn={false}
  signInCTA={{
    defaultLabel: "Sign in to Predict",
    onClick: () => handleSignIn(),
  }}
  tabs={["play", "leaderboard", "private-leagues", "rules", "prizes"]}
  matchCardBgImageUrl="https://your-cdn.com/matchcard-bg.jpg"
  defaultImagePlaceholderUrl="https://your-cdn.com/placeholder.png"
  playTabBanners={[
    {
      position: "above-group-nav",
      render: () => <SponsorBanner />,
    },
    {
      position: "right",
      render: () => <AdSlot id="predictor-sidebar" />,
    },
  ]}
  consents={[
    {
      consentId: "tos",
      body: 'I accept the <a href="https://example.com/tos" target="_blank">Terms of Service</a>.',
      required: true,
      defaultChecked: false,
    },
  ]}
  themeOptions={{
    mode: "light",
    colorSchemes: {
      light: {
        palette: {
          primary: {
            plainColor: "#1A77D2",
            primaryContainer: "#2397F3",
            onPrimary: "#FAFAFA",
          },
        },
        surface: "#FFFFFF",
        textPrimary: "#212121",
      },
    },
  }}
/>
```

##### With Betslip — Predictions Trigger

Selections are sent to the Betslip automatically as the user edits scores, before they submit.

```tsx
<Predictor
  entityId="predictor-template-123"
  sdk={sdkInstance}
  language="en"
  userIsLoggedIn={true}
  tabs={["play", "leaderboard"]}
  betslip={{
    trigger: "predictions-only",
    position: "side-right",
    currency: "£",
    stakePresets: [5, 10, 25, 50],
    ctaUrlTemplate:
      "https://your-bookmaker.com/bet?ids={selectionIds}&stake={stake}&ref={currentUrl}",
    labels: {
      disclaimer: "18+ | Please gamble responsibly",
    },
  }}
  themeOptions={{ mode: "dark" }}
/>
```

##### With Betslip — Odds Trigger

Selections are sent only when the user explicitly clicks an odds button on the post-submit view.

```tsx
<Predictor
  entityId="predictor-template-123"
  sdk={sdkInstance}
  language="en"
  userIsLoggedIn={true}
  betslip={{
    trigger: "odds-only",
    position: "bottom-right",
    currency: "€",
    ctaUrlTemplate:
      "https://your-bookmaker.com/bet?ids={selectionIds}&stake={stake}",
  }}
/>
```

---

### Betslip

A floating betslip component that collects selections from any external source and routes users to a bookmaker via a configurable deep-link URL. The component communicates with the rest of the page through a singleton command bus (`betslipApi`), so it decouples cleanly from host-page code.

**Key Features:**

- Six viewport positions: four corner anchors and two full-height side panels
- Singleton command bus with pre-mount queuing — selections sent before the widget mounts are replayed on load
- Automatic odds polling from the SDK with configurable interval
- Bookmaker branding color automatically used as the primary accent (overridable via `themeOptions`)
- Configurable CTA deep-link URL template with dynamic placeholder substitution
- Shadow DOM style isolation — no CSS conflicts with the host application
- Multi-language support and fully overridable UI text labels

#### Quick Start

```tsx
import React from "react";
import { Betslip, betslipApi } from "fansunited-frontend-components";
import { FansUnitedSDK } from "fansunited-sdk-esm";

const sdk = FansUnitedSDK({ /* your config */ });

const App: React.FC = () => (
  <>
    {/* Place once anywhere in the tree */}
    <Betslip
      sdk={sdk}
      language="en"
      position="bottom-right"
      ctaUrlTemplate="https://your-bookmaker.com/betslip?selections={selectionIds}&stake={stake}"
    />

    {/* From anywhere in your app, add or remove selections */}
    <button onClick={() => betslipApi.setSelection("fb:m:451678:FT_1X2:1")}>
      Back Home Win
    </button>
  </>
);
```

#### Required Props

| Prop       | Type                 | Description      |
| ---------- | -------------------- | ---------------- |
| `sdk`      | `FansUnitedSDKModel` | SDK instance     |
| `language` | `LanguageType`       | Display language |

#### Optional Props

| Prop                   | Type                 | Default          | Description                                                     |
| ---------------------- | -------------------- | ---------------- | --------------------------------------------------------------- |
| `position`             | `BetslipPosition`    | `"bottom-right"` | Viewport anchor. See [Positions](#positions) below              |
| `maxSelections`        | `number`             | `20`             | Hard cap on simultaneous selections                             |
| `stakePresets`         | `number[]`           | `[5, 10, 20]`    | Quick-select stake buttons shown in the footer                  |
| `oddsPollingInterval`  | `number`             | `30000`          | Odds refresh interval in milliseconds                           |
| `currency`             | `string`             | `"€"`            | Currency symbol shown next to stake and potential win           |
| `ctaUrlTemplate`       | `string`             | —                | Deep-link URL template with `{placeholder}` substitution        |
| `brandingLogoUrl`      | `string`             | —                | Operator logo URL that overrides the bookmaker logo             |
| `labels`               | `BetslipLabels`      | —                | UI text overrides. See [Labels](#labels) below                  |
| `themeOptions`         | `CustomThemeOptions` | —                | Theme tokens. See [Theming](#theming-1) below                   |

#### Positions

```tsx
import { BetslipPosition } from "fansunited-frontend-core";

type BetslipPosition =
  | "bottom-right"  // Fixed corner anchor, slides up on open (default)
  | "bottom-left"   // Fixed corner anchor, slides up on open
  | "top-right"     // Fixed corner anchor, slides down on open
  | "top-left"      // Fixed corner anchor, slides down on open
  | "side-right"    // Full-height panel, slides in from the right
  | "side-left";    // Full-height panel, slides in from the left
```

**Corner positions** (`bottom-*`, `top-*`):
- 320 px wide, fixed 16 px from the viewport edges
- Header shows an expand/collapse chevron
- Collapsed state renders a compact summary (combined odds, stake, potential win)
- Expanded state shows the full selection list (scrollable at 300 px max height) plus the stake/CTA footer

**Side positions** (`side-*`):
- 340 px wide panel covering full viewport height (100 vh)
- A 32 × 64 px tab handle peeks out at 33 % from the top when closed — it shows the bookmaker/branding logo rotated 90°, or three grip lines when no logo is available
- A selection count badge appears on the closed tab when `maxSelections` is set
- Smooth 0.3 s slide-in/out transition

#### Command Bus (`betslipApi`)

The betslip exposes a singleton command bus for external code to manage selections and read betslip state without needing a direct reference to the React component.

> **Important:** Always import `betslipApi` from `fansunited-frontend-components`. The component bundles its own singleton instance — importing from the components package guarantees your code and the widget share the same bus.

```tsx
import { betslipApi } from "fansunited-frontend-components";

// Add or replace a selection (same eventId + market = outcome replaced)
betslipApi.setSelection("fb:m:451678:FT_1X2:1");

// Remove a selection by its exact id
betslipApi.removeSelection("fb:m:451678:FT_1X2:1");
```

**Selection ID format:**

```
"{eventId}:{market}:{outcome}"
```

| Segment    | Examples                                       |
| ---------- | ---------------------------------------------- |
| `eventId`  | `fb:m:451678`                                  |
| `market`   | `FT_1X2`, `DOUBLE_CHANCE`, `OVER_GOALS_2_5`, `CORRECT_SCORE`, `PLAYER_SCORE_FIRST_GOAL` |
| `outcome`  | `1` (home), `x` (draw), `2` (away), `1x`, `yes`, `no`, `1-2` (correct score) |

**Reading state:**

`betslipApi.getState()` returns a snapshot of the current betslip state. `betslipApi.subscribe()` lets you react to every change — useful for syncing a selection count badge, enabling a custom CTA elsewhere on the page, or forwarding stake changes to your own backend.

```tsx
import { betslipApi } from "fansunited-frontend-components";
import type { BetslipState } from "fansunited-frontend-components";

// One-shot read
const { selections, stake, totalOdds } = betslipApi.getState();

// Reactive — fires on every selection, stake, or odds change; returns unsubscribe
const unsubscribe = betslipApi.subscribe((state: BetslipState) => {
  setBadgeCount(state.selections.length);
});

// Stop listening
unsubscribe();
```

**`BetslipState` shape:**

```ts
interface BetslipState {
  selections: BetslipSelectionInput[]; // raw inputs (same shape as setSelection input)
  stake: number;                        // parsed numeric stake (0 when empty)
  totalOdds: number;                    // product of all selection decimal odds (0 when no selections)
}
```

**Pre-mount queuing:**

Calls to `betslipApi.setSelection` made before the `<Betslip />` component has mounted are queued and automatically replayed once the widget finishes initializing. There is no need to delay external code to wait for the widget.

```tsx
import { betslipApi } from "fansunited-frontend-components";

// Safe to call before <Betslip /> is rendered
betslipApi.setSelection("fb:m:451678:FT_1X2:1");

// Later, when <Betslip /> mounts, it will receive the queued selection
ReactDOM.render(<Betslip sdk={sdk} language="en" />, root);
```

#### Predictor Integration

The `Predictor` component has first-class Betslip support via its `betslip` prop. When configured, the Predictor renders the `Betslip` widget internally — no separate `<Betslip />` component is needed. See the [Predictor — Betslip Integration](#betslip-integration) section for full details.

```tsx
import { Predictor } from "fansunited-frontend-components";

<Predictor
  entityId="predictor-123"
  sdk={sdk}
  language="en"
  betslip={{
    trigger: "predictions-only", // or "odds-only"
    position: "side-right",
    currency: "£",
    stakePresets: [5, 10, 25, 50],
    ctaUrlTemplate: "https://your-bookmaker.com/bet?ids={selectionIds}&stake={stake}",
  }}
/>
```

**Trigger modes:**

| Mode                  | When selections are sent to Betslip                                               |
| --------------------- | --------------------------------------------------------------------------------- |
| `"predictions-only"`  | Each time the user changes a score prediction (before submitting)                 |
| `"odds-only"`         | When the user clicks an odds button on the post-submit odds view                  |

#### CTA URL Template

The `ctaUrlTemplate` prop defines the bookmaker deep-link. Placeholders are replaced at click time with live values from the current betslip state.

```tsx
ctaUrlTemplate="https://betano.bg/mybet/?selection_ids={selectionIds}&stake={stake}&return_url={currentUrl}"
```

**Available placeholders:**

| Placeholder       | Value                                                                     |
| ----------------- | ------------------------------------------------------------------------- |
| `{selectionIds}`  | Comma-separated SDK `selection.id` values (up to the first 10)            |
| `{stake}`         | Current stake input value (float)                                         |
| `{currentUrl}`    | `window.location.href`, URI-encoded                                       |
| `{eventId}`       | `eventId` of the first selection                                          |
| `{oddsDecimal}`   | Decimal odds of the first selection                                       |
| `{market}`        | Market code of the first selection                                        |
| `{outcome}`       | Outcome label of the first selection                                      |

#### Theming

The Betslip derives its primary accent color from a three-level priority chain:

1. **Custom primary** — `themeOptions.colorSchemes[mode].palette.primary.plainColor`
2. **Bookmaker color** — `bookmaker.branding.backgroundColor` (or `bookmaker.assets[0].backgroundColor` as fallback) extracted live from the SDK odds response
3. **Default theme primary**

This means you get bookmaker brand colors for free when odds are available, and can override them at any time via `themeOptions`.

```tsx
import { CustomThemeOptions } from "fansunited-frontend-core";

// Override primary accent — disables the bookmaker color fallback
const themeOptions: CustomThemeOptions = {
  mode: "dark",
  colorSchemes: {
    dark: {
      palette: {
        primary: {
          plainColor: "#FF5722",       // your brand color
          primaryContainer: "#FF7043",
          onPrimary: "#FFFFFF",
        },
        success: {
          plainColor: "#4CAF50",
          softBg: "#042F04",
        },
        danger: {
          plainColor: "#F44336",
          softBg: "#430A0A",
        },
      },
      surface: "#212121",
      onSurface: "#121212",
      surfaceVariant: "#333333",
      textPrimary: "#FAFAFA",
      textSecondary: "#BDBDBD",
      outlineEnabledBorder: "#555555",
    },
  },
  customFontFamily: {
    dark: {
      primary: "Inter, sans-serif",
      secondary: "Roboto, sans-serif",
    },
  },
};

<Betslip sdk={sdk} language="en" themeOptions={themeOptions} />;
```

> **Note:** When neither a custom primary nor bookmaker odds are present, the widget falls back to the default theme primary (`#1A77D2`).

#### Labels

Override any piece of UI text without switching languages:

```tsx
import { BetslipLabels } from "fansunited-frontend-core";

const labels: BetslipLabels = {
  title: "My Betslip",
  placeBetLabel: "Place Bet",
  emptyTitle: "Your betslip is empty",
  emptyDescription: "Add selections to get started",
  continueLabel: "Continue",
  disclaimer: "18+ | Gamble responsibly",
  combinedOddsLabel: "Combined Odds",
  stakeLabel: "Stake",
  totalStakeLabel: "Total Stake",
  potentialWinLabel: "Potential Win",
  oddsUnavailableLabel: "Odds N/A",
  suspendedLabel: "Suspended",

  // Override individual outcome labels
  outcomeLabels: {
    yes: "Over",
    no: "Under",
    draw: "Draw",
    or: "/",
    nobody: "No Scorer",
    noGoal: "No Goal",
  },

  // Override market display names
  marketLabels: {
    FT_1X2: "Full Time",
    DOUBLE_CHANCE: "Double Chance",
    OVER_GOALS_2_5: "Over/Under 2.5 Goals",
  },
};

<Betslip sdk={sdk} language="en" labels={labels} />;
```

#### Conflict Detection

The betslip automatically detects selections that are mutually exclusive — i.e., two selections with the same `eventId` and market. When conflicts are detected:

- A warning banner appears at the top of the expanded selection list
- Conflicting selection cards are highlighted with danger styling
- The CTA button is disabled until the conflict is resolved

Conflicts resolve automatically when the user removes one of the conflicting selections. No extra configuration is required.

#### Odds Behavior

- **Polling**: Odds are fetched from `sdk.odds.getByMatchIds()` on mount and refreshed every `oddsPollingInterval` ms (default 30 s)
- **Movement indicator**: A green ▲ or red ▼ is shown when the bookmaker response includes a `movement` field (`"UP"` or `"DOWN"`) on the matched selection. No indicator is shown when `movement` is absent
- **Unavailable**: When no odds are returned for a selection, the card shows `oddsUnavailableLabel` and the CTA is disabled
- **Suspended**: Selections with status `"suspended"` are highlighted and block the CTA
- **Settled states**: Cards display ✓ (won), ✗ (lost), or ≈ (void) once a selection is settled

**Over/Under markets:** The `outcome` code in the selection ID uses `"yes"` for Over and `"no"` for Under (not `"over"`/`"under"`). The widget handles the conversion and label display internally.

**Correct Score market:** Outcomes use hyphen notation, e.g. `"1-2"`. The widget parses both `-` and `:` as separators.

#### TypeScript Support

Import types from `fansunited-frontend-core`:

```tsx
import {
  BetslipProps,
  BetslipLabels,
  BetslipPosition,
  BetslipSelectionInput,
  BetslipOutcomeLabels,
  SelectionStatus,
  CustomThemeOptions,
  LanguageType,
  betslipApi,
} from "fansunited-frontend-core";
```

#### Examples

##### Basic Betslip

```tsx
import { Betslip } from "fansunited-frontend-components";
import { FansUnitedSDK } from "fansunited-sdk-esm";

const sdk = FansUnitedSDK({ /* config */ });

<Betslip
  sdk={sdk}
  language="en"
  ctaUrlTemplate="https://your-bookmaker.com/bet?selections={selectionIds}"
/>;
```

##### Side Panel with Custom Branding

```tsx
import { Betslip } from "fansunited-frontend-components";

<Betslip
  sdk={sdk}
  language="en"
  position="side-right"
  maxSelections={10}
  stakePresets={[5, 10, 25, 50]}
  currency="£"
  brandingLogoUrl="https://your-cdn.com/logo.png"
  ctaUrlTemplate="https://your-bookmaker.com/bet?ids={selectionIds}&stake={stake}&ref={currentUrl}"
  labels={{
    title: "Your Selections",
    placeBetLabel: "Bet Now",
    disclaimer: "18+ | Please gamble responsibly | BeGambleAware.org",
  }}
  themeOptions={{
    mode: "dark",
    colorSchemes: {
      dark: {
        palette: {
          primary: {
            plainColor: "#FF5722",
            primaryContainer: "#FF7043",
            onPrimary: "#FFFFFF",
          },
        },
        surface: "#1A1A1A",
        textPrimary: "#FAFAFA",
      },
    },
  }}
/>
```

##### Programmatic Selection Management

```tsx
import { betslipApi } from "fansunited-frontend-core";

// Add selections (safe to call before the widget mounts)
betslipApi.setSelection("fb:m:451678:FT_1X2:1");
betslipApi.setSelection("fb:m:451679:DOUBLE_CHANCE:1x");

// Replace an outcome for the same event+market
betslipApi.setSelection("fb:m:451678:FT_1X2:2"); // replaces the home-win

// Remove a specific selection
betslipApi.removeSelection("fb:m:451679:DOUBLE_CHANCE:1x");
```

##### Predictor + Betslip Integration

The `Predictor` renders the `Betslip` widget internally when `betslip` is provided — no separate `<Betslip />` is needed.

```tsx
import { Predictor } from "fansunited-frontend-components";

<Predictor
  entityId="predictor-123"
  sdk={sdk}
  language="en"
  betslip={{
    trigger: "predictions-only",
    position: "side-right",
    currency: "€",
    stakePresets: [5, 10, 20, 50],
    ctaUrlTemplate:
      "https://your-bookmaker.com/bet?ids={selectionIds}&stake={stake}",
    labels: {
      disclaimer: "18+ | Gamble responsibly",
    },
  }}
  themeOptions={{ mode: "light" }}
/>
```

---

## Available Components

This package exports the following components:

- **`ClassicQuizPlay`** - Traditional quiz with scoring and explanations
- **`PollVote`** - Interactive polling with real-time results
- **`CollectLead`** - Lead collection forms with custom fields
- **`PersonalityQuizPlay`** - Personality assessment with persona matching
- **`MatchQuizPlay`** - Football match prediction quiz with multiple markets
- **`EventGamePlay`** - Event prediction game with flexible outcome types
- **`ChanceGame`** - Chance-based game with Prize Wheel, Penalty Shootout, and Pick One of X variants
- **`EitherOrPlay`** - Binary choice game with standings panel and customizable templates
- **`Discussion`** - Threaded comments with reactions, replies, reporting, and authentication support
- **`Leaderboard`** - Ranked standings for Classic Quiz, Match Quiz, Top X, and Template entities with multi-entity support
- **`Predictor`** - Multi-tab football score predictor with leaderboard, private leagues, rules, and prizes
- **`Betslip`** - Floating betslip with bookmaker deep-link integration, six viewport positions, and command bus API

## Related Packages

- [`fansunited-frontend-core`](https://www.npmjs.com/package/fansunited-frontend-core) - Core types and utilities
