Basic Embed Component
Simple reusable thread embed component:Copy
// 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"
/>
);
}
Copy
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:Copy
// 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,
};
}
Copy
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:Copy
// 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:Copy
// 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:Copy
// 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;
}
}
Copy
<EmbedErrorBoundary>
<KashEmbed threadId="abc123" />
</EmbedErrorBoundary>
SSR-Safe Embed (Next.js)
Server-side rendering compatible component:Copy
// 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:Copy
// 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>
);
}