initial commit: rocket.new export of broadcastbeat
This commit is contained in:
316
src/app/reading-list/page.tsx
Normal file
316
src/app/reading-list/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user