Skip to main content
Complete React component examples for embedding Kash prediction markets with TypeScript support and hooks.

Basic Embed Component

Simple reusable thread embed component:
// components/KashEmbed.tsx
import { useEffect, useRef, useState } from 'react';

interface KashEmbedProps {
  threadId: string;
  source?: string;
  height?: number;
  className?: string;
  onReady?: (data: { threadId: string; marketCount: number; totalVolume: number }) => void;
  onMarketClick?: (data: { threadId: string; marketId: string }) => void;
}

export function KashEmbed({
  threadId,
  source = 'my-site',
  height = 900,
  className = '',
  onReady,
  onMarketClick,
}: KashEmbedProps) {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  useEffect(() => {
    function handleMessage(event: MessageEvent) {
      if (event.origin !== 'https://app.kash.bot') return;

      const { type, data } = event.data;

      switch (type) {
        case 'kash-iframe-ready':
          onReady?.(data);
          break;
        case 'kash-market-clicked':
          onMarketClick?.(data);
          break;
      }
    }

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [onReady, onMarketClick]);

  const embedUrl = `https://app.kash.bot/threads/${threadId}/iframe?source=${encodeURIComponent(source)}`;

  return (
    <iframe
      ref={iframeRef}
      src={embedUrl}
      width="100%"
      height={height}
      frameBorder="0"
      className={className}
      title="Kash prediction markets"
      loading="lazy"
      allow="clipboard-write"
    />
  );
}
Usage:
import { KashEmbed } from '@/components/KashEmbed';

export default function MarketsPage() {
  return (
    <div className="container">
      <h1>2024 Election Markets</h1>
      <KashEmbed
        threadId="abc123"
        source="my-website"
        height={900}
        onReady={(data) => console.log(`Loaded ${data.marketCount} markets`)}
        onMarketClick={(data) => console.log(`Clicked: ${data.marketId}`)}
      />
    </div>
  );
}

Custom Hook for Embed Events

Reusable hook for listening to embed events:
// hooks/useKashEmbed.ts
import { useEffect, useState, useRef, useCallback } from 'react';

interface EmbedReadyData {
  threadId: string;
  marketCount: number;
  totalVolume: number;
}

interface MarketClickData {
  threadId: string;
  marketId: string;
}

interface UseKashEmbedOptions {
  onReady?: (data: EmbedReadyData) => void;
  onMarketClick?: (data: MarketClickData) => void;
}

export function useKashEmbed(options: UseKashEmbedOptions = {}) {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [isReady, setIsReady] = useState(false);
  const [marketCount, setMarketCount] = useState<number | null>(null);

  useEffect(() => {
    function handleMessage(event: MessageEvent) {
      if (event.origin !== 'https://app.kash.bot') return;

      const { type, data } = event.data;

      switch (type) {
        case 'kash-iframe-ready':
          setIsReady(true);
          setMarketCount(data.marketCount);
          options.onReady?.(data);
          break;
        case 'kash-market-clicked':
          options.onMarketClick?.(data);
          break;
      }
    }

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [options.onReady, options.onMarketClick]);

  return {
    iframeRef,
    isReady,
    marketCount,
  };
}
Usage:
import { useKashEmbed } from '@/hooks/useKashEmbed';

export function MarketsWidget({ threadId }: { threadId: string }) {
  const { iframeRef, isReady, marketCount } = useKashEmbed({
    onReady: (data) => {
      console.log(`Loaded ${data.marketCount} markets`);
    },
    onMarketClick: (data) => {
      console.log(`Market clicked: ${data.marketId}`);
    },
  });

  return (
    <div>
      <div className="status">
        {isReady 
          ? `✅ ${marketCount} markets loaded` 
          : 'Loading...'}
      </div>
      
      <iframe
        ref={iframeRef}
        src={`https://app.kash.bot/threads/${threadId}/iframe?source=my-site`}
        width="100%"
        height={900}
        frameBorder="0"
      />
    </div>
  );
}

Responsive Sidebar Widget

Compact widget component:
// components/MarketWidget.tsx
import { useState } from 'react';
import { useKashEmbed } from '@/hooks/useKashEmbed';

interface MarketWidgetProps {
  threadId: string;
  title: string;
}

export function MarketWidget({ threadId, title }: MarketWidgetProps) {
  const [status, setStatus] = useState('Loading...');

  const { iframeRef, isReady, marketCount } = useKashEmbed({
    onReady: (data) => {
      setStatus(`${data.marketCount} markets`);
    },
  });

  return (
    <div className="market-widget">
      <div className="widget-header">
        <h3 className="widget-title">📊 {title}</h3>
        <span className="widget-badge">{status}</span>
      </div>

      <iframe
        ref={iframeRef}
        src={`https://app.kash.bot/threads/${threadId}/iframe?source=my-site`}
        width="100%"
        height={400}
        frameBorder="0"
        className="widget-iframe"
      />

      <style jsx>{`
        .market-widget {
          background: white;
          border-radius: 12px;
          padding: 16px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
          max-width: 350px;
        }

        .widget-header {
          display: flex;
          align-items: center;
          justify-content: space-between;
          margin-bottom: 12px;
        }

        .widget-title {
          font-size: 18px;
          font-weight: 600;
          margin: 0;
        }

        .widget-badge {
          background: #dbeafe;
          color: #1e40af;
          padding: 4px 10px;
          border-radius: 12px;
          font-size: 12px;
          font-weight: 500;
        }

        .widget-iframe {
          border-radius: 8px;
          border: 0;
        }
      `}</style>
    </div>
  );
}

Multi-Thread Dashboard

Display multiple threads with dynamic loading:
// components/MarketDashboard.tsx
import { useState, useEffect } from 'react';
import { KashEmbed } from './KashEmbed';

interface Thread {
  id: string;
  title: string;
  category: string;
  marketCount: number;
}

export function MarketDashboard() {
  const [threads, setThreads] = useState<Thread[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchThreads() {
      try {
        const response = await fetch(
          'https://app.kash.bot/api/threads?status=active&limit=6'
        );
        const data = await response.json();
        setThreads(data.threads);
      } catch (error) {
        console.error('Failed to fetch threads:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchThreads();
  }, []);

  if (loading) {
    return <div className="loading">Loading threads...</div>;
  }

  return (
    <div className="dashboard">
      <h1>Market Dashboard</h1>

      <div className="thread-grid">
        {threads.map((thread) => (
          <div key={thread.id} className="thread-card">
            <div className="thread-header">
              <h2>{thread.title}</h2>
              <span className="thread-category">{thread.category}</span>
            </div>

            <KashEmbed
              threadId={thread.id}
              height={500}
              onReady={(data) => {
                console.log(`Thread ${thread.id}: ${data.marketCount} markets`);
              }}
            />
          </div>
        ))}
      </div>

      <style jsx>{`
        .dashboard {
          max-width: 1400px;
          margin: 0 auto;
          padding: 20px;
        }

        .thread-grid {
          display: grid;
          grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
          gap: 20px;
          margin-top: 20px;
        }

        .thread-card {
          background: white;
          border-radius: 8px;
          padding: 16px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
        }

        .thread-header {
          margin-bottom: 12px;
        }

        .thread-header h2 {
          font-size: 18px;
          margin: 0 0 4px 0;
        }

        .thread-category {
          font-size: 13px;
          color: #6b7280;
          text-transform: uppercase;
          letter-spacing: 0.5px;
        }
      `}</style>
    </div>
  );
}

Error Boundary Wrapper

Graceful error handling:
// components/EmbedErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class EmbedErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Embed error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className="error-container">
            <h3>Failed to load markets</h3>
            <p>Please try refreshing the page.</p>
            <button onClick={() => this.setState({ hasError: false })}>
              Retry
            </button>
          </div>
        )
      );
    }

    return this.props.children;
  }
}
Usage:
<EmbedErrorBoundary>
  <KashEmbed threadId="abc123" />
</EmbedErrorBoundary>

SSR-Safe Embed (Next.js)

Server-side rendering compatible component:
// components/SSRSafeEmbed.tsx
import dynamic from 'next/dynamic';

const KashEmbedClient = dynamic(
  () => import('./KashEmbed').then((mod) => mod.KashEmbed),
  {
    ssr: false,
    loading: () => (
      <div className="embed-skeleton">
        <div className="skeleton-header" />
        <div className="skeleton-card" />
        <div className="skeleton-card" />
        <div className="skeleton-card" />
      </div>
    ),
  }
);

export function SSRSafeEmbed(props: any) {
  return <KashEmbedClient {...props} />;
}

Complete Page Example

Full Next.js page with all features:
// app/markets/page.tsx
'use client';

import { useState } from 'react';
import { useKashEmbed } from '@/hooks/useKashEmbed';
import { EmbedErrorBoundary } from '@/components/EmbedErrorBoundary';

export default function MarketsPage() {
  const [status, setStatus] = useState('Loading markets...');

  const { iframeRef, isReady, marketCount } = useKashEmbed({
    onReady: (data) => {
      setStatus(`✅ Loaded ${data.marketCount} markets`);
    },
    onMarketClick: (data) => {
      setStatus(`👆 Clicked: ${data.marketId}`);
    },
  });

  return (
    <div className="container mx-auto px-4 py-8">
      <header className="mb-8">
        <h1 className="text-3xl font-bold mb-2">Prediction Markets</h1>
        <p className="text-gray-600">
          Live probabilities on politics, sports, and current events
        </p>
      </header>

      <div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-4 text-sm">
        {status}
      </div>

      <EmbedErrorBoundary>
        <iframe
          ref={iframeRef}
          src="https://app.kash.bot/threads/abc123/iframe?source=my-website"
          width="100%"
          height={900}
          frameBorder="0"
          className="rounded-lg border border-gray-200 shadow-sm"
          title="Prediction markets"
        />
      </EmbedErrorBoundary>
    </div>
  );
}

Next Steps