From 279bd30c728bf169b0cfa9be8e76abb5aa316776 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 31 Jan 2026 11:26:31 +0000 Subject: [PATCH] feat: add Live Sports button and user access request system - Add Live Sports button linking to Jellyfin Live TV section - Create /request-access page for users to request account access - Add API endpoint that sends email notifications via Resend - Add environment variables for RESEND_API_KEY and ADMIN_EMAIL - Add .gitignore to exclude build artifacts and node_modules Co-Authored-By: Claude Opus 4.5 --- .env.example | 6 ++ .gitignore | 40 ++++++++ app/api/request-access/route.ts | 109 ++++++++++++++++++++ app/page.tsx | 37 ++++++- app/request-access/page.tsx | 175 ++++++++++++++++++++++++++++++++ docker-compose.yml | 3 + 6 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 app/api/request-access/route.ts create mode 100644 app/request-access/page.tsx diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a116d76 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Email notifications via Resend (required for access request feature) +# Get your API key from https://resend.com +RESEND_API_KEY=re_xxxxxxxxxxxx + +# Admin email to receive access request notifications +ADMIN_EMAIL=jeff@jeffemmett.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fef173 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +.next/ +out/ +build/ +dist/ + +# Environment files (keep .env.example) +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# TypeScript +*.tsbuildinfo +next-env.d.ts + +# Vercel +.vercel + +# Testing +coverage/ diff --git a/app/api/request-access/route.ts b/app/api/request-access/route.ts new file mode 100644 index 0000000..28b962c --- /dev/null +++ b/app/api/request-access/route.ts @@ -0,0 +1,109 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { name, email, reason } = body + + // Validate required fields + if (!name || !email) { + return NextResponse.json( + { error: '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 } + ) + } + + // Send notification email via Resend + const resendApiKey = process.env.RESEND_API_KEY + if (!resendApiKey) { + console.error('RESEND_API_KEY not configured') + return NextResponse.json( + { error: 'Email service not configured' }, + { status: 500 } + ) + } + + const adminEmail = process.env.ADMIN_EMAIL || 'jeff@jeffemmett.com' + + const emailResponse = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${resendApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + from: 'Jefflix ', + to: adminEmail, + subject: `[Jefflix] New Access Request from ${name}`, + html: ` +

New Jefflix Access Request

+

Someone has requested access to Jefflix:

+ + + + + + + + + + + + + + + + + +
Name:${escapeHtml(name)}
Email:${escapeHtml(email)}
Reason:${escapeHtml(reason || 'Not provided')}
Requested:${new Date().toLocaleString()}
+

To approve this request:

+
    +
  1. Go to Jellyfin Dashboard
  2. +
  3. Navigate to Dashboard → Users → Add User
  4. +
  5. Create an account for ${escapeHtml(name)} (${escapeHtml(email)})
  6. +
  7. Reply to this email to let them know their account is ready
  8. +
+
+

This is an automated message from Jefflix.

+ `, + }), + }) + + if (!emailResponse.ok) { + const errorData = await emailResponse.json() + console.error('Resend API error:', errorData) + return NextResponse.json( + { error: 'Failed to send notification' }, + { status: 500 } + ) + } + + return NextResponse.json({ success: true }) + } catch (error) { + console.error('Request access 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 508ea4c..3daf808 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { Film, Music, Server, Users, Heart, Rocket, Tv, Home, FolderGitIcon as SolidarityFistIcon, HandHeart, Landmark, Palette } from "lucide-react" +import { Film, Music, Server, Users, Heart, Rocket, Tv, Home, FolderGitIcon as SolidarityFistIcon, HandHeart, Landmark, Palette, Radio } from "lucide-react" import { JefflixLogo } from "@/components/jefflix-logo" export default function JefflixPage() { @@ -78,6 +78,24 @@ export default function JefflixPage() { Music +
+ + + + Request Access + + +
@@ -230,6 +248,23 @@ export default function JefflixPage() { Music +
+ + + + Request Access + + +

Or learn how to set up your own Jellyfin server and join the movement diff --git a/app/request-access/page.tsx b/app/request-access/page.tsx new file mode 100644 index 0000000..dad32a7 --- /dev/null +++ b/app/request-access/page.tsx @@ -0,0 +1,175 @@ +'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 { UserPlus, CheckCircle, AlertCircle, ArrowLeft } from "lucide-react" +import Link from 'next/link' + +export default function RequestAccessPage() { + const [formData, setFormData] = useState({ + name: '', + email: '', + reason: '', + }) + 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-access', { + 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!

+

+ Your access request has been sent. You'll receive an email once your account is ready. +

+

+ This usually happens within 24-48 hours. +

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

Request Access

+

+ Jefflix is a community media server. To protect the community and ensure quality access, + we require approval for new accounts. +

+ + Required for Live Sports & Premium Content + +
+ +
+
+ + setFormData({ ...formData, name: 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-orange-500" + placeholder="Enter your name" + /> +
+ +
+ + setFormData({ ...formData, email: 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-orange-500" + placeholder="your@email.com" + /> +

+ We'll send your login details to this address +

+
+ +
+ +