import React, { useMemo, useRef, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Download, FileUp, Plus, Trash2 } from "lucide-react"; import Papa from "papaparse"; import jsPDF from "jspdf"; import html2canvas from "html2canvas"; import { motion } from "framer-motion"; /** * Channel Lineup → PDF (Single‑file React App) * * Features * - Upload CSV or paste text to import channels * - Manual add/edit rows * - Supports channel number, name, and logo URL * - Live preview with sorting & quick validation * - Exports a clean, branded PDF (multi‑column grid) * * CSV headers accepted (case‑insensitive): * number, name, logo, logo_url * * Example CSV: * number,name,logo * 2,CBS,https://logo.clearbit.com/cbs.com * 5,NBC,https://logo.clearbit.com/nbc.com * 7,ABC,https://logo.clearbit.com/abc.com */ export default function ChannelLineupToPDF() { const [brand, setBrand] = useState({ title: "EmpireTV Channel Lineup", subtitle: "Your favorite channels at a glance", footer: "© " + new Date().getFullYear() + " EmpireTV — empiretv.co", }); const [channels, setChannels] = useState([]); const [pasted, setPasted] = useState(""); const [error, setError] = useState(null); const [exporting, setExporting] = useState(false); const printRef = useRef(null); type ChannelRow = { id: string; number: string; // stored as string to preserve formats like 100.1 name: string; logo?: string; }; const nextId = () => Math.random().toString(36).slice(2, 9); const sortedChannels = useMemo(() => { return [...channels].sort((a, b) => { const aNum = parseFloat(a.number); const bNum = parseFloat(b.number); if (isNaN(aNum) && isNaN(bNum)) return a.name.localeCompare(b.name); if (isNaN(aNum)) return 1; if (isNaN(bNum)) return -1; return aNum - bNum; }); }, [channels]); function parseCSVText(text: string) { const result = Papa.parse(text.trim(), { header: true, skipEmptyLines: true }); if (result.errors?.length) { setError("CSV parse error: " + result.errors[0].message); } else { setError(null); const rows = (result.data as any[]).map((r) => ({ id: nextId(), number: String(r.number ?? r.Number ?? r.NUMBER ?? r.channel ?? r["channel number"] ?? ""), name: String(r.name ?? r.Name ?? r.NAME ?? r["channel name"] ?? r["Channel"] ?? ""), logo: String(r.logo ?? r.logo_url ?? r.Logo ?? r.LOGO ?? ""), })); const clean = rows.filter((r) => r.name || r.number); setChannels((prev) => [...prev, ...clean]); } } function parsePlainText(text: string) { // Accept formats like: "2 CBS", "7 | ABC", "12, FOX, https://..." const lines = text.split(/\n+/).map((l) => l.trim()).filter(Boolean); const rows: ChannelRow[] = []; for (const line of lines) { const parts = line.split(/[|,\t]+/).map((p) => p.trim()); if (parts.length >= 2) { const [numberMaybe, nameMaybe, logoMaybe] = parts; rows.push({ id: nextId(), number: numberMaybe, name: nameMaybe, logo: logoMaybe }); } else { // Try split on first space const m = line.match(/^(\S+)\s+(.+)$/); if (m) rows.push({ id: nextId(), number: m[1], name: m[2] }); } } setChannels((prev) => [...prev, ...rows]); } function onFileUpload(file?: File | null) { if (!file) return; if (!/\.csv$/i.test(file.name)) { setError("Please upload a .csv file."); return; } const reader = new FileReader(); reader.onload = (e) => { const text = String(e.target?.result || ""); parseCSVText(text); }; reader.readAsText(file); } function addRow() { setChannels((prev) => [ ...prev, { id: nextId(), number: "", name: "", logo: "" }, ]); } function removeRow(id: string) { setChannels((prev) => prev.filter((c) => c.id !== id)); } function updateCell(id: string, key: keyof ChannelRow, value: string) { setChannels((prev) => prev.map((c) => (c.id === id ? { ...c, [key]: value } : c))); } async function exportPDF() { try { setExporting(true); setError(null); const node = printRef.current; if (!node) throw new Error("Nothing to print yet."); // Ensure web fonts render await new Promise((r) => setTimeout(r, 50)); const canvas = await html2canvas(node, { scale: 2, backgroundColor: "#ffffff", useCORS: true, allowTaint: true, }); const imgData = canvas.toDataURL("image/png"); const pdf = new jsPDF({ unit: "pt", format: "letter" }); const pageWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); const imgWidth = pageWidth; const imgHeight = (canvas.height * imgWidth) / canvas.width; // If content is taller than a page, paginate let position = 0; let remaining = imgHeight; let pageCanvasY = 0; while (remaining > 0) { pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight); remaining -= pageHeight; pageCanvasY += pageHeight; if (remaining > 0) pdf.addPage(); position = 0; } pdf.save("channel-lineup.pdf"); } catch (e: any) { setError(e?.message || "Failed to export PDF"); } finally { setExporting(false); } } return (
Channel Lineup → PDF

Upload or paste your channels, preview, then export a polished PDF. Supports CSV and free‑form text.

{/* Left: Inputs */}
Import
onFileUpload(e.target.files?.[0])} />

Headers: number, name, logo (URL)