'use client' import { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { JefflixLogo } from "@/components/jefflix-logo" import { GlobeLoader, type GlobePoint } from "@/components/globe/globe-loader" import { countryToLatLng } from "@/lib/country-centroids" import { Search, CheckCircle, AlertCircle, ArrowLeft, X, Loader2, Globe2, List } from "lucide-react" import Link from 'next/link' interface Channel { id: string name: string country: string categories: string[] } export default function RequestChannelPage() { const [channels, setChannels] = useState([]) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState('') const [query, setQuery] = useState('') const [debouncedQuery, setDebouncedQuery] = useState('') const [selected, setSelected] = useState([]) const [email, setEmail] = useState('') const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle') const [errorMessage, setErrorMessage] = useState('') const debounceRef = useRef(null) // Globe view toggle const [view, setView] = useState<'search' | 'globe'>('search') const [globeCountry, setGlobeCountry] = useState(null) // Build globe points from channels (aggregate by country) const globePoints: GlobePoint[] = useMemo(() => { const countryMap = new Map() for (const ch of channels) { if (!ch.country) continue const existing = countryMap.get(ch.country) if (existing) { existing.count++ } else { const coords = countryToLatLng(ch.country) if (coords) { countryMap.set(ch.country, { count: 1, coords }) } } } return Array.from(countryMap.entries()).map(([country, { count, coords }]) => ({ lat: coords[0], lng: coords[1], id: country, label: `${country} (${count} channels)`, color: '#06b6d4', // cyan-500 size: Math.max(1, Math.min(8, Math.log2(count + 1) * 2)), })) }, [channels]) // Channels filtered for globe-selected country const globeFilteredChannels = useMemo(() => { if (!globeCountry) return [] return channels.filter((ch) => ch.country === globeCountry) }, [channels, globeCountry]) const handleGlobePointClick = useCallback((point: GlobePoint) => { setGlobeCountry(point.id) // id = country name }, []) // Fetch channel list on mount useEffect(() => { fetch('/api/channels') .then((res) => { if (!res.ok) throw new Error('Failed to load channels') return res.json() }) .then((data: Channel[]) => { setChannels(data) setLoading(false) }) .catch((err) => { setLoadError(err.message) setLoading(false) }) }, []) // Debounce search input useEffect(() => { debounceRef.current = setTimeout(() => setDebouncedQuery(query), 200) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query]) const results = debouncedQuery.length < 2 ? [] : channels.filter((ch) => { const q = debouncedQuery.toLowerCase() return ( ch.name.toLowerCase().includes(q) || ch.country.toLowerCase().includes(q) || ch.categories.some((c) => c.toLowerCase().includes(q)) ) }) const totalMatches = results.length const displayResults = results.slice(0, 50) const toggleChannel = useCallback((ch: Channel) => { setSelected((prev) => prev.some((s) => s.id === ch.id) ? prev.filter((s) => s.id !== ch.id) : prev.length < 20 ? [...prev, ch] : prev ) }, []) const removeChannel = useCallback((id: string) => { setSelected((prev) => prev.filter((s) => s.id !== id)) }, []) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (selected.length === 0) return setStatus('submitting') setErrorMessage('') try { const res = await fetch('/api/request-channel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, channels: selected.map(({ id, name, country, categories }) => ({ id, name, country, categories, })), }), }) const data = await res.json() if (!res.ok) throw new Error(data.error || 'Failed to submit request') setStatus('success') } catch (err) { setStatus('error') setErrorMessage(err instanceof Error ? err.message : 'Something went wrong') } } if (status === 'success') { return (

Request Submitted!

We'll look into adding {selected.length === 1 ? 'this channel' : `these ${selected.length} channels`} and let you know once {selected.length === 1 ? "it's" : "they're"} available.

Most channels are added within a few days.

) } return (
{/* Header */}
{/* Main Content */}

Request a Channel

Search the iptv-org catalog and select channels you'd like us to add to the Live TV lineup.

{/* View toggle */} {!loading && !loadError && (
)}
{loading ? (

Loading channel catalog...

) : loadError ? (

{loadError}

) : view === 'globe' ? ( /* Globe View */
{/* Selected chips (shared with search view) */} {selected.length > 0 && (
{selected.map((ch) => ( removeChannel(ch.id)} > {ch.name} ))}
)} {/* Country channel list */} {globeCountry && (

{globeCountry}

{globeFilteredChannels.length} channels

{globeFilteredChannels.map((ch) => { const isSelected = selected.some((s) => s.id === ch.id) return ( ) })}
)} {!globeCountry && (

Click a point on the globe to browse channels by country

)} {/* Email + Submit (same as search view) */}
setEmail(e.target.value)} className="w-full px-4 py-3 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-cyan-500" placeholder="your@email.com" />
{status === 'error' && (

{errorMessage}

)}
) : (
{/* Search input */}
setQuery(e.target.value)} className="w-full pl-12 pr-4 py-3 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-cyan-500" placeholder="Search by channel name, country, or category..." autoFocus />
{/* Selected chips */} {selected.length > 0 && (
{selected.map((ch) => ( removeChannel(ch.id)} > {ch.name} ))}
)} {/* Results */} {debouncedQuery.length >= 2 && (
{displayResults.length === 0 ? (
No channels found for "{debouncedQuery}"
) : ( <> {displayResults.map((ch) => { const isSelected = selected.some((s) => s.id === ch.id) return ( ) })} {totalMatches > 50 && (
{totalMatches - 50} more — refine your search
)} )}
)} {debouncedQuery.length > 0 && debouncedQuery.length < 2 && (

Type at least 2 characters to search

)} {/* Email */}
setEmail(e.target.value)} className="w-full px-4 py-3 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-cyan-500" placeholder="your@email.com" />

We'll let you know when the channels are added

{/* Error */} {status === 'error' && (

{errorMessage}

)} {/* Submit */}
)}

How does this work?

This catalog comes from iptv-org, a community-maintained collection of publicly available IPTV streams. Select the channels you want and we'll map them into the Jefflix Live TV lineup.

Not all channels have working streams—we'll do our best and let you know the result.

) }