initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import AppImage from "@/components/ui/AppImage";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/lib/supabase/client";
interface SavedArticle {
id: string;
articleSlug: string;
articleTitle: string;
articleExcerpt: string;
articleImage: string;
articleImageAlt: string;
articleCategory: string;
articleAuthor: string;
articleReadTime: string;
articleDate: string;
savedAt: string;
}
export default function ReadingListPage() {
const [savedArticles, setSavedArticles] = useState<SavedArticle[]>([]);
const [loading, setLoading] = useState(true);
const [currentUser, setCurrentUser] = useState<any>(null);
const [removingId, setRemovingId] = useState<string | null>(null);
const [toast, setToast] = useState<string | null>(null);
const supabase = createClient();
useEffect(() => {
const init = async () => {
const { data } = await supabase?.auth?.getUser();
setCurrentUser(data?.user ?? null);
if (data?.user) {
await fetchReadingList(data.user.id);
} else {
setLoading(false);
}
};
init();
}, []);
const fetchReadingList = async (userId: string) => {
setLoading(true);
try {
const { data, error } = await supabase
?.from("reading_list")
?.select("*")
?.eq("user_id", userId)
?.order("saved_at", { ascending: false });
if (!error && data) {
setSavedArticles(
data.map((row: any) => ({
id: row.id,
articleSlug: row.article_slug,
articleTitle: row.article_title,
articleExcerpt: row.article_excerpt ?? "",
articleImage: row.article_image ?? "",
articleImageAlt: row.article_image_alt ?? row.article_title,
articleCategory: row.article_category ?? "",
articleAuthor: row.article_author ?? "",
articleReadTime: row.article_read_time ?? "",
articleDate: row.article_date ?? "",
savedAt: row.saved_at,
}))
);
}
} catch {
// Silent fail
} finally {
setLoading(false);
}
};
const handleRemove = async (id: string, slug: string) => {
if (!currentUser) return;
setRemovingId(id);
try {
await supabase
?.from("reading_list")
?.delete()
?.eq("id", id)
?.eq("user_id", currentUser.id);
setSavedArticles((prev) => prev.filter((a) => a.id !== id));
setToast("Removed from reading list");
setTimeout(() => setToast(null), 2500);
} catch {
setToast("Failed to remove article");
setTimeout(() => setToast(null), 2500);
} finally {
setRemovingId(null);
}
};
const handleClearAll = async () => {
if (!currentUser || savedArticles.length === 0) return;
try {
await supabase
?.from("reading_list")
?.delete()
?.eq("user_id", currentUser.id);
setSavedArticles([]);
setToast("Reading list cleared");
setTimeout(() => setToast(null), 2500);
} catch {
setToast("Failed to clear reading list");
setTimeout(() => setToast(null), 2500);
}
};
return (
<>
<Header />
<main className="bg-[#111111] min-h-screen">
{/* Toast */}
{toast && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-[#1a2535] border border-[#3b82f6] text-[#e0e0e0] font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2.5" aria-hidden="true">
<polyline points="20 6 9 17 4 12" />
</svg>
{toast}
</div>
)}
{/* Page Header */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e]">
<div className="max-w-container mx-auto px-4 py-2.5">
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">Home</Link>
<span aria-hidden="true">/</span>
<span className="text-[#888]">Reading List</span>
</nav>
</div>
</div>
<div className="max-w-container mx-auto px-4 py-8 md:py-10">
{/* Title Row */}
<div className="flex items-center justify-between mb-8 pb-4 border-b border-[#2a2a2a]">
<div className="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2" aria-hidden="true">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
<h1 className="font-heading text-xl md:text-2xl font-bold text-[#e8e8e8]">Reading List</h1>
{savedArticles.length > 0 && (
<span className="font-body text-xs text-[#666] bg-[#1a1a1a] border border-[#2a2a2a] px-2 py-0.5 rounded-sm">
{savedArticles.length} {savedArticles.length === 1 ? "article" : "articles"}
</span>
)}
</div>
{savedArticles.length > 0 && (
<button
onClick={handleClearAll}
className="font-body text-xs text-[#555] hover:text-red-400 transition-colors uppercase tracking-wide focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
Clear All
</button>
)}
</div>
{/* States */}
{loading ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 animate-pulse">
<div className="flex gap-4">
<div className="w-28 h-20 bg-[#252525] rounded-sm flex-shrink-0" />
<div className="flex-1 space-y-2">
<div className="h-3 bg-[#252525] rounded w-1/4" />
<div className="h-4 bg-[#252525] rounded w-3/4" />
<div className="h-3 bg-[#252525] rounded w-1/2" />
</div>
</div>
</div>
))}
</div>
) : !currentUser ? (
<div className="text-center py-20">
<div className="w-16 h-16 mx-auto mb-5 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] rounded-full">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#555" strokeWidth="1.5" aria-hidden="true">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
</div>
<h2 className="font-heading text-lg font-bold text-[#e0e0e0] mb-2">Sign in to view your reading list</h2>
<p className="font-body text-sm text-[#666] mb-6 max-w-sm mx-auto">
Create an account or sign in to save articles and access them anytime.
</p>
<Link
href="/home-page"
className="inline-block btn-subscribe px-6 py-2.5 text-sm font-body font-bold uppercase tracking-wide">
Browse Articles
</Link>
</div>
) : savedArticles.length === 0 ? (
<div className="text-center py-20">
<div className="w-16 h-16 mx-auto mb-5 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] rounded-full">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#555" strokeWidth="1.5" aria-hidden="true">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
</div>
<h2 className="font-heading text-lg font-bold text-[#e0e0e0] mb-2">Your reading list is empty</h2>
<p className="font-body text-sm text-[#666] mb-6 max-w-sm mx-auto">
Save articles from any article page to read them later.
</p>
<Link
href="/home-page"
className="inline-block btn-subscribe px-6 py-2.5 text-sm font-body font-bold uppercase tracking-wide">
Discover Articles
</Link>
</div>
) : (
<div className="space-y-4">
{savedArticles.map((item) => (
<div
key={item.id}
className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm hover:border-[#3a3a3a] transition-colors group">
<div className="flex gap-4 p-4">
{/* Thumbnail */}
<Link
href={`/articles/${item.articleSlug}`}
className="flex-shrink-0 w-28 h-20 overflow-hidden rounded-sm img-zoom">
{item.articleImage ? (
<AppImage
src={item.articleImage}
alt={item.articleImageAlt}
width={112}
height={80}
className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-[#252525] flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
)}
</Link>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
{item.articleCategory && (
<span className="inline-block font-body text-[9px] font-bold uppercase tracking-widest text-[#3b82f6] mb-1">
{item.articleCategory}
</span>
)}
<Link href={`/articles/${item.articleSlug}`}>
<h2 className="font-heading text-sm md:text-base font-bold text-[#e0e0e0] group-hover:text-[#3b82f6] transition-colors leading-snug line-clamp-2 mb-1">
{item.articleTitle}
</h2>
</Link>
{item.articleExcerpt && (
<p className="font-body text-xs text-[#777] leading-relaxed line-clamp-2 mb-2 hidden sm:block">
{item.articleExcerpt}
</p>
)}
<div className="flex flex-wrap items-center gap-2 text-[10px] font-body text-[#555]">
{item.articleAuthor && <span>{item.articleAuthor}</span>}
{item.articleAuthor && item.articleDate && <span>·</span>}
{item.articleDate && <span>{item.articleDate}</span>}
{item.articleReadTime && (
<>
<span>·</span>
<span className="text-[#444]">{item.articleReadTime}</span>
</>
)}
</div>
</div>
{/* Remove Button */}
<button
onClick={() => handleRemove(item.id, item.articleSlug)}
disabled={removingId === item.id}
aria-label={`Remove "${item.articleTitle}" from reading list`}
className="flex-shrink-0 w-7 h-7 flex items-center justify-center text-[#444] hover:text-red-400 hover:bg-[#2a1a1a] rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] disabled:opacity-50">
{removingId === item.id ? (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin" aria-hidden="true">
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" />
</svg>
) : (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
)}
</button>
</div>
</div>
</div>
{/* Read Article Link */}
<div className="border-t border-[#222] px-4 py-2 flex items-center justify-between">
<span className="font-body text-[10px] text-[#444] uppercase tracking-wide">
Saved {new Date(item.savedAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</span>
<Link
href={`/articles/${item.articleSlug}`}
className="font-body text-[10px] font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors">
Read Article
</Link>
</div>
</div>
))}
</div>
)}
</div>
</main>
<Footer />
</>
);
}