diff --git a/app/api/request-channel/route.ts b/app/api/request-channel/route.ts new file mode 100644 index 0000000..774f11f --- /dev/null +++ b/app/api/request-channel/route.ts @@ -0,0 +1,111 @@ +import { NextRequest, NextResponse } from 'next/server' +import nodemailer from 'nodemailer' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { channelName, category, url, notes, email } = body + + // Validate required fields + if (!channelName || !email) { + return NextResponse.json( + { error: 'Channel name and email are required' }, + { status: 400 } + ) + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + return NextResponse.json( + { error: 'Invalid email format' }, + { status: 400 } + ) + } + + const smtpHost = process.env.SMTP_HOST + const smtpUser = process.env.SMTP_USER + const smtpPass = process.env.SMTP_PASS + if (!smtpHost || !smtpUser || !smtpPass) { + console.error('SMTP credentials not configured') + return NextResponse.json( + { error: 'Email service not configured' }, + { status: 500 } + ) + } + + const adminEmail = process.env.ADMIN_EMAIL || 'jeff@jeffemmett.com' + + const transporter = nodemailer.createTransport({ + host: smtpHost, + port: Number(process.env.SMTP_PORT) || 587, + secure: false, + auth: { user: smtpUser, pass: smtpPass }, + tls: { rejectUnauthorized: false }, + }) + + await transporter.sendMail({ + from: `Jefflix <${smtpUser}>`, + to: adminEmail, + subject: `[Jefflix] Channel Request: ${escapeHtml(channelName)}`, + html: ` +

New Channel Request

+

Someone has requested a new Live TV channel:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Channel:${escapeHtml(channelName)}
Category:${escapeHtml(category || 'Not specified')}
URL/Link:${url ? `${escapeHtml(url)}` : 'Not provided'}
Notes:${escapeHtml(notes || 'None')}
Email:${escapeHtml(email)}
Requested:${new Date().toLocaleString()}
+

To add this channel:

+
    +
  1. Check iptv-org for an existing stream
  2. +
  3. If found, add to Threadfin channel list and map the stream
  4. +
  5. If not in iptv-org, add as a custom M3U source in Threadfin
  6. +
  7. Reply to ${escapeHtml(email)} to let them know it's been added
  8. +
+
+

This is an automated message from Jefflix.

+ `, + }) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error('Channel request error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + } + return text.replace(/[&<>"']/g, (char) => map[char]) +} diff --git a/app/page.tsx b/app/page.tsx index ef2e8f4..1bb68d3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -79,6 +79,41 @@ export default function JefflixPage() { +
+ + + +
@@ -231,6 +266,26 @@ export default function JefflixPage() { +
+ + + +

Or learn how to set up your own Jellyfin server and join the movement

diff --git a/app/request-channel/page.tsx b/app/request-channel/page.tsx new file mode 100644 index 0000000..37833e6 --- /dev/null +++ b/app/request-channel/page.tsx @@ -0,0 +1,216 @@ +'use client' + +import { useState } from 'react' +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { JefflixLogo } from "@/components/jefflix-logo" +import { Radio, CheckCircle, AlertCircle, ArrowLeft } from "lucide-react" +import Link from 'next/link' + +const categories = ['Sports', 'News', 'Entertainment', 'Music', 'Movies', 'Kids', 'Other'] + +export default function RequestChannelPage() { + const [formData, setFormData] = useState({ + channelName: '', + category: '', + url: '', + notes: '', + email: '', + }) + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') + const [errorMessage, setErrorMessage] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setStatus('loading') + setErrorMessage('') + + try { + const response = await fetch('/api/request-channel', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to submit request') + } + + setStatus('success') + } catch (error) { + setStatus('error') + setErrorMessage(error instanceof Error ? error.message : 'Something went wrong') + } + } + + if (status === 'success') { + return ( +
+
+
+ +
+

Request Submitted!

+

+ We'll look into adding the channel and let you know once it's available. +

+

+ Most channels are added within a few days. +

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

Request a Channel

+

+ Can't find a channel you're looking for? Let us know and we'll try to add it + to the Live TV lineup. +

+ + 3,468 Channels & Growing + +
+ +
+
+ + setFormData({ ...formData, channelName: 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="e.g. BBC World News, ESPN2" + /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, url: 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="M3U URL or channel website" + /> +

+ If you know where to find a stream URL or M3U link, paste it here +

+
+ +
+ +