843 lines
38 KiB
TypeScript
843 lines
38 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect } from "react";
|
||
import Link from "next/link";
|
||
import Header from "@/components/Header";
|
||
import Footer from "@/components/Footer";
|
||
import AppImage from "@/components/ui/AppImage";
|
||
import { createClient } from "@/lib/supabase/client";
|
||
import { useRouter } from "next/navigation";
|
||
|
||
interface UserProfile {
|
||
id: string;
|
||
email: string;
|
||
full_name: string;
|
||
avatar_url: string | null;
|
||
bio: string | null;
|
||
website: string | null;
|
||
location: string | null;
|
||
reputation: number;
|
||
created_at: string;
|
||
}
|
||
|
||
interface BookmarkItem {
|
||
id: string;
|
||
article_slug: string;
|
||
article_title: string;
|
||
article_excerpt: string | null;
|
||
article_image: string | null;
|
||
article_image_alt: string | null;
|
||
article_category: string | null;
|
||
article_author: string | null;
|
||
article_read_time: string | null;
|
||
article_date: string | null;
|
||
saved_at: string;
|
||
}
|
||
|
||
interface HistoryItem {
|
||
id: string;
|
||
article_slug: string;
|
||
article_title: string;
|
||
article_excerpt: string | null;
|
||
article_image: string | null;
|
||
article_image_alt: string | null;
|
||
article_category: string | null;
|
||
article_author: string | null;
|
||
article_read_time: string | null;
|
||
article_date: string | null;
|
||
viewed_at: string;
|
||
}
|
||
|
||
interface TopicPref {
|
||
id: string;
|
||
topic: string;
|
||
view_count: number;
|
||
last_viewed_at: string;
|
||
}
|
||
|
||
type ActiveTab = "profile" | "security" | "bookmarks" | "history" | "topics" | "preferences";
|
||
|
||
export default function AccountPage() {
|
||
const router = useRouter();
|
||
const supabase = createClient();
|
||
|
||
const [user, setUser] = useState<any>(null);
|
||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [activeTab, setActiveTab] = useState<ActiveTab>("profile");
|
||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||
|
||
// Profile form state
|
||
const [fullName, setFullName] = useState("");
|
||
const [bio, setBio] = useState("");
|
||
const [website, setWebsite] = useState("");
|
||
const [location, setLocation] = useState("");
|
||
|
||
// Security form state
|
||
const [newPassword, setNewPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
const [changingPassword, setChangingPassword] = useState(false);
|
||
|
||
// Digest preference state
|
||
const [weeklyDigest, setWeeklyDigest] = useState(false);
|
||
const [savingDigest, setSavingDigest] = useState(false);
|
||
|
||
// Bookmarks
|
||
const [bookmarks, setBookmarks] = useState<BookmarkItem[]>([]);
|
||
const [bookmarksLoading, setBookmarksLoading] = useState(false);
|
||
|
||
// Reading History
|
||
const [history, setHistory] = useState<HistoryItem[]>([]);
|
||
const [historyLoading, setHistoryLoading] = useState(false);
|
||
|
||
// Topic Preferences
|
||
const [topics, setTopics] = useState<TopicPref[]>([]);
|
||
const [topicsLoading, setTopicsLoading] = useState(false);
|
||
|
||
const showToast = (message: string, type: "success" | "error") => {
|
||
setToast({ message, type });
|
||
setTimeout(() => setToast(null), 3500);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const init = async () => {
|
||
const { data: { user: authUser } } = await supabase.auth.getUser();
|
||
if (!authUser) {
|
||
router.push("/login");
|
||
return;
|
||
}
|
||
setUser(authUser);
|
||
|
||
const { data: profileData } = await supabase
|
||
.from("user_profiles")
|
||
.select("*")
|
||
.eq("id", authUser.id)
|
||
.maybeSingle();
|
||
|
||
if (profileData) {
|
||
setProfile(profileData);
|
||
setFullName(profileData.full_name || "");
|
||
setBio(profileData.bio || "");
|
||
setWebsite(profileData.website || "");
|
||
setLocation(profileData.location || "");
|
||
}
|
||
setLoading(false);
|
||
};
|
||
init();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!user) return;
|
||
fetch("/api/digest/preferences")
|
||
.then((r) => r.json())
|
||
.then((d) => setWeeklyDigest(d.weekly_digest_enabled ?? false))
|
||
.catch(() => {});
|
||
}, [user]);
|
||
|
||
// Load bookmarks when tab is active
|
||
useEffect(() => {
|
||
if (activeTab !== "bookmarks" || !user) return;
|
||
setBookmarksLoading(true);
|
||
supabase
|
||
.from("reading_list")
|
||
.select("*")
|
||
.eq("user_id", user.id)
|
||
.order("saved_at", { ascending: false })
|
||
.then(({ data }) => {
|
||
setBookmarks(data || []);
|
||
setBookmarksLoading(false);
|
||
});
|
||
}, [activeTab, user]);
|
||
|
||
// Load reading history when tab is active
|
||
useEffect(() => {
|
||
if (activeTab !== "history" || !user) return;
|
||
setHistoryLoading(true);
|
||
supabase
|
||
.from("reading_history")
|
||
.select("*")
|
||
.eq("user_id", user.id)
|
||
.order("viewed_at", { ascending: false })
|
||
.limit(50)
|
||
.then(({ data }) => {
|
||
setHistory(data || []);
|
||
setHistoryLoading(false);
|
||
});
|
||
}, [activeTab, user]);
|
||
|
||
// Load topic preferences when tab is active
|
||
useEffect(() => {
|
||
if (activeTab !== "topics" || !user) return;
|
||
setTopicsLoading(true);
|
||
supabase
|
||
.from("user_topic_preferences")
|
||
.select("*")
|
||
.eq("user_id", user.id)
|
||
.order("view_count", { ascending: false })
|
||
.then(({ data }) => {
|
||
setTopics(data || []);
|
||
setTopicsLoading(false);
|
||
});
|
||
}, [activeTab, user]);
|
||
|
||
const handleSaveProfile = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!user) return;
|
||
setSaving(true);
|
||
try {
|
||
const { error } = await supabase
|
||
.from("user_profiles")
|
||
.upsert({
|
||
id: user.id,
|
||
email: user.email,
|
||
full_name: fullName,
|
||
bio,
|
||
website,
|
||
location,
|
||
});
|
||
if (error) throw error;
|
||
showToast("Profile updated successfully", "success");
|
||
} catch (err: any) {
|
||
showToast(err?.message || "Failed to update profile", "error");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleChangePassword = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (newPassword !== confirmPassword) {
|
||
showToast("Passwords do not match", "error");
|
||
return;
|
||
}
|
||
if (newPassword.length < 6) {
|
||
showToast("Password must be at least 6 characters", "error");
|
||
return;
|
||
}
|
||
setChangingPassword(true);
|
||
try {
|
||
const { error } = await supabase.auth.updateUser({ password: newPassword });
|
||
if (error) throw error;
|
||
setNewPassword("");
|
||
setConfirmPassword("");
|
||
showToast("Password updated successfully", "success");
|
||
} catch (err: any) {
|
||
showToast(err?.message || "Failed to update password", "error");
|
||
} finally {
|
||
setChangingPassword(false);
|
||
}
|
||
};
|
||
|
||
const handleSignOut = async () => {
|
||
await supabase.auth.signOut();
|
||
router.push("/home-page");
|
||
};
|
||
|
||
const handleToggleDigest = async (enabled: boolean) => {
|
||
setSavingDigest(true);
|
||
try {
|
||
const res = await fetch("/api/digest/preferences", {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ weekly_digest_enabled: enabled }),
|
||
});
|
||
if (!res.ok) throw new Error("Failed to update");
|
||
setWeeklyDigest(enabled);
|
||
showToast(enabled ? "Weekly digest enabled" : "Weekly digest disabled", "success");
|
||
} catch {
|
||
showToast("Failed to update digest preference", "error");
|
||
} finally {
|
||
setSavingDigest(false);
|
||
}
|
||
};
|
||
|
||
const handleRemoveBookmark = async (id: string, slug: string) => {
|
||
if (!user) return;
|
||
try {
|
||
await supabase.from("reading_list").delete().eq("id", id).eq("user_id", user.id);
|
||
setBookmarks((prev) => prev.filter((b) => b.id !== id));
|
||
showToast("Bookmark removed", "success");
|
||
} catch {
|
||
showToast("Failed to remove bookmark", "error");
|
||
}
|
||
};
|
||
|
||
const handleClearHistory = async () => {
|
||
if (!user) return;
|
||
try {
|
||
await supabase.from("reading_history").delete().eq("user_id", user.id);
|
||
setHistory([]);
|
||
showToast("Reading history cleared", "success");
|
||
} catch {
|
||
showToast("Failed to clear history", "error");
|
||
}
|
||
};
|
||
|
||
const tabs: { id: ActiveTab; label: string }[] = [
|
||
{ id: "profile", label: "Profile" },
|
||
{ id: "security", label: "Security" },
|
||
{ id: "bookmarks", label: "Bookmarks" },
|
||
{ id: "history", label: "History" },
|
||
{ id: "topics", label: "Topics" },
|
||
{ id: "preferences", label: "Preferences" },
|
||
];
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-screen bg-[#1c1815]">
|
||
<Header />
|
||
<div className="max-w-container mx-auto px-4 py-12">
|
||
<div className="max-w-3xl mx-auto space-y-4">
|
||
<div className="skeleton h-8 w-48 rounded-sm" />
|
||
<div className="skeleton h-[400px] rounded-sm" />
|
||
</div>
|
||
</div>
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#1c1815]">
|
||
<Header />
|
||
|
||
{/* Toast */}
|
||
{toast && (
|
||
<div
|
||
className={`fixed bottom-6 left-1/2 -translate-x-1/2 z-50 border font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2 animate-fade-in ${
|
||
toast.type === "success" ?"bg-[#231d18] border-[#F0A623] text-[#e0e0e0]" :"bg-[#1a0a0a] border-[#D85A30] text-[#e0e0e0]"
|
||
}`}>
|
||
{toast.type === "success" ? (
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#F0A623" strokeWidth="2.5" aria-hidden="true">
|
||
<polyline points="20 6 9 17 4 12" />
|
||
</svg>
|
||
) : (
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#D85A30" strokeWidth="2.5" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" />
|
||
</svg>
|
||
)}
|
||
{toast.message}
|
||
</div>
|
||
)}
|
||
|
||
{/* Breadcrumb */}
|
||
<div className="bg-[#1c1815] 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-[#F0A623] transition-colors">Home</Link>
|
||
<span aria-hidden="true">/</span>
|
||
<span className="text-[#888]">Account</span>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
|
||
<main className="max-w-container mx-auto px-4 py-8">
|
||
<div className="max-w-3xl mx-auto">
|
||
{/* Page Header */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h1 className="font-heading text-2xl font-bold text-[#FBEFE0]">Account Settings</h1>
|
||
<p className="font-body text-sm text-[#666] mt-1">{user?.email}</p>
|
||
</div>
|
||
<button
|
||
onClick={handleSignOut}
|
||
className="font-body text-xs font-bold uppercase tracking-wide text-[#888] hover:text-[#D85A30] border border-[#3a322b] hover:border-[#D85A30] px-4 py-2 rounded-sm transition-colors">
|
||
Sign Out
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div className="flex border-b border-[#3a322b] mb-6 overflow-x-auto scrollbar-none">
|
||
{tabs.map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id)}
|
||
className={`font-body text-sm font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors whitespace-nowrap ${
|
||
activeTab === tab.id
|
||
? "border-[#F0A623] text-[#F0A623]"
|
||
: "border-transparent text-[#666] hover:text-[#aaa]"
|
||
}`}>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Profile Tab */}
|
||
{activeTab === "profile" && (
|
||
<form onSubmit={handleSaveProfile} className="space-y-5">
|
||
<div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
|
||
Public Profile
|
||
</h2>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
|
||
Full Name
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={fullName}
|
||
onChange={(e) => setFullName(e.target.value)}
|
||
placeholder="Your full name"
|
||
className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
|
||
Bio
|
||
</label>
|
||
<textarea
|
||
value={bio}
|
||
onChange={(e) => setBio(e.target.value)}
|
||
placeholder="Tell us a bit about yourself..."
|
||
rows={3}
|
||
className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444] resize-none"
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
|
||
Website
|
||
</label>
|
||
<input
|
||
type="url"
|
||
value={website}
|
||
onChange={(e) => setWebsite(e.target.value)}
|
||
placeholder="https://yoursite.com"
|
||
className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
|
||
Location
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={location}
|
||
onChange={(e) => setLocation(e.target.value)}
|
||
placeholder="City, Country"
|
||
className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-3 pb-3 border-b border-[#3a322b]">
|
||
Account Info
|
||
</h2>
|
||
<div>
|
||
<label className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
|
||
Email Address
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={user?.email || ""}
|
||
disabled
|
||
className="w-full bg-[#1c1815] border border-[#222] text-[#555] font-body text-sm px-3 py-2.5 rounded-sm cursor-not-allowed"
|
||
/>
|
||
<p className="font-body text-xs text-[#555] mt-1.5">Email cannot be changed here.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end">
|
||
<button
|
||
type="submit"
|
||
disabled={saving}
|
||
className="bg-[#F0A623] hover:bg-[#BA7517] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
|
||
{saving ? "Saving..." : "Save Profile"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
)}
|
||
|
||
{/* Security Tab */}
|
||
{activeTab === "security" && (
|
||
<div className="space-y-5">
|
||
<form onSubmit={handleChangePassword}>
|
||
<div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
|
||
Change Password
|
||
</h2>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
|
||
New Password
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
placeholder="Enter new password"
|
||
minLength={6}
|
||
className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
|
||
Confirm New Password
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
placeholder="Confirm new password"
|
||
minLength={6}
|
||
className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="mt-5 flex justify-end">
|
||
<button
|
||
type="submit"
|
||
disabled={changingPassword || !newPassword || !confirmPassword}
|
||
className="bg-[#F0A623] hover:bg-[#BA7517] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
|
||
{changingPassword ? "Updating..." : "Update Password"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<div className="bg-[#1a0a0a] border border-[#3a1a1a] rounded-sm p-6">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-2">Danger Zone</h2>
|
||
<p className="font-body text-sm text-[#888] mb-4">
|
||
Once you sign out, you will need to sign back in to access your account.
|
||
</p>
|
||
<button
|
||
onClick={handleSignOut}
|
||
className="font-body text-xs font-bold uppercase tracking-wide text-[#D85A30] border border-[#D85A30]/40 hover:border-[#D85A30] hover:bg-[#D85A30]/10 px-5 py-2 rounded-sm transition-colors">
|
||
Sign Out of All Sessions
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Bookmarks Tab */}
|
||
{activeTab === "bookmarks" && (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0]">
|
||
Saved Articles
|
||
{bookmarks.length > 0 && (
|
||
<span className="ml-2 font-body text-xs text-[#666] bg-[#231d18] border border-[#3a322b] px-2 py-0.5 rounded-sm">
|
||
{bookmarks.length}
|
||
</span>
|
||
)}
|
||
</h2>
|
||
<Link href="/reading-list" className="font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
|
||
Full List →
|
||
</Link>
|
||
</div>
|
||
|
||
{bookmarksLoading ? (
|
||
<div className="space-y-3">
|
||
{[1, 2, 3].map((i) => (
|
||
<div key={i} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 animate-pulse">
|
||
<div className="flex gap-3">
|
||
<div className="w-20 h-14 bg-[#3a322b] rounded-sm flex-shrink-0" />
|
||
<div className="flex-1 space-y-2">
|
||
<div className="h-3 bg-[#3a322b] rounded w-1/4" />
|
||
<div className="h-4 bg-[#3a322b] rounded w-3/4" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : bookmarks.length === 0 ? (
|
||
<div className="text-center py-12 bg-[#231d18] border border-[#3a322b] rounded-sm">
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
|
||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||
</svg>
|
||
<p className="font-body text-sm text-[#666]">No saved articles yet.</p>
|
||
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
|
||
Browse News →
|
||
</Link>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{bookmarks.map((item) => (
|
||
<div key={item.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm hover:border-[#3a3a3a] transition-colors group">
|
||
<div className="flex gap-3 p-4">
|
||
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
|
||
{item.article_image ? (
|
||
<AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" />
|
||
) : (
|
||
<div className="w-full h-full bg-[#3a322b] flex items-center justify-center">
|
||
<svg width="16" height="16" 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>
|
||
<div className="flex-1 min-w-0">
|
||
{item.article_category && (
|
||
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#F0A623]">{item.article_category}</span>
|
||
)}
|
||
<Link href={`/articles/${item.article_slug}`}>
|
||
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#F0A623] transition-colors line-clamp-2 leading-snug mt-0.5">
|
||
{item.article_title}
|
||
</h3>
|
||
</Link>
|
||
<div className="flex items-center justify-between mt-1.5">
|
||
<span className="font-body text-[10px] text-[#555]">
|
||
Saved {new Date(item.saved_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||
</span>
|
||
<button
|
||
onClick={() => handleRemoveBookmark(item.id, item.article_slug)}
|
||
className="font-body text-[10px] text-[#555] hover:text-red-400 transition-colors uppercase tracking-wide">
|
||
Remove
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Reading History Tab */}
|
||
{activeTab === "history" && (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0]">
|
||
Reading History
|
||
{history.length > 0 && (
|
||
<span className="ml-2 font-body text-xs text-[#666] bg-[#231d18] border border-[#3a322b] px-2 py-0.5 rounded-sm">
|
||
{history.length}
|
||
</span>
|
||
)}
|
||
</h2>
|
||
{history.length > 0 && (
|
||
<button
|
||
onClick={handleClearHistory}
|
||
className="font-body text-xs text-[#555] hover:text-red-400 transition-colors uppercase tracking-wide">
|
||
Clear All
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{historyLoading ? (
|
||
<div className="space-y-3">
|
||
{[1, 2, 3].map((i) => (
|
||
<div key={i} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 animate-pulse">
|
||
<div className="flex gap-3">
|
||
<div className="w-20 h-14 bg-[#3a322b] rounded-sm flex-shrink-0" />
|
||
<div className="flex-1 space-y-2">
|
||
<div className="h-3 bg-[#3a322b] rounded w-1/4" />
|
||
<div className="h-4 bg-[#3a322b] rounded w-3/4" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : history.length === 0 ? (
|
||
<div className="text-center py-12 bg-[#231d18] border border-[#3a322b] rounded-sm">
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||
</svg>
|
||
<p className="font-body text-sm text-[#666]">No reading history yet.</p>
|
||
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
|
||
Browse News →
|
||
</Link>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{history.map((item) => (
|
||
<div key={item.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm hover:border-[#3a3a3a] transition-colors group">
|
||
<div className="flex gap-3 p-4">
|
||
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
|
||
{item.article_image ? (
|
||
<AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" />
|
||
) : (
|
||
<div className="w-full h-full bg-[#3a322b] flex items-center justify-center">
|
||
<svg width="16" height="16" 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>
|
||
<div className="flex-1 min-w-0">
|
||
{item.article_category && (
|
||
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#F0A623]">{item.article_category}</span>
|
||
)}
|
||
<Link href={`/articles/${item.article_slug}`}>
|
||
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#F0A623] transition-colors line-clamp-2 leading-snug mt-0.5">
|
||
{item.article_title}
|
||
</h3>
|
||
</Link>
|
||
<span className="font-body text-[10px] text-[#555] mt-1 block">
|
||
Viewed {new Date(item.viewed_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Topic Preferences Tab */}
|
||
{activeTab === "topics" && (
|
||
<div className="space-y-4">
|
||
<div className="mb-2">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0]">Topic Interests</h2>
|
||
<p className="font-body text-xs text-[#666] mt-1">
|
||
Automatically tracked based on the articles you read. Used to personalize your content feed.
|
||
</p>
|
||
</div>
|
||
|
||
{topicsLoading ? (
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||
<div key={i} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 animate-pulse h-16" />
|
||
))}
|
||
</div>
|
||
) : topics.length === 0 ? (
|
||
<div className="text-center py-12 bg-[#231d18] border border-[#3a322b] rounded-sm">
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
|
||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" /><line x1="7" y1="7" x2="7.01" y2="7" />
|
||
</svg>
|
||
<p className="font-body text-sm text-[#666]">No topic preferences tracked yet.</p>
|
||
<p className="font-body text-xs text-[#555] mt-1">Read articles to build your interest profile.</p>
|
||
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
|
||
Browse News →
|
||
</Link>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||
{topics.map((t) => {
|
||
const maxCount = topics[0]?.view_count || 1;
|
||
const pct = Math.round((t.view_count / maxCount) * 100);
|
||
return (
|
||
<div key={t.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 hover:border-[#3a3a3a] transition-colors">
|
||
<div className="flex items-start justify-between mb-2">
|
||
<span className="font-body text-xs font-bold uppercase tracking-wider text-[#e0e0e0] capitalize leading-tight">
|
||
{t.topic}
|
||
</span>
|
||
<span className="font-body text-[10px] text-[#F0A623] font-bold ml-2 flex-shrink-0">
|
||
{t.view_count}×
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-[#3a322b] rounded-full h-1">
|
||
<div
|
||
className="bg-[#F0A623] h-1 rounded-full transition-all"
|
||
style={{ width: `${pct}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
<p className="font-body text-xs text-[#555] text-center mt-2">
|
||
Based on {topics.reduce((s, t) => s + t.view_count, 0)} article views across {topics.length} topics
|
||
</p>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Preferences Tab */}
|
||
{activeTab === "preferences" && (
|
||
<div className="space-y-5">
|
||
<div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
|
||
Forum Reputation
|
||
</h2>
|
||
<div className="flex items-center gap-4">
|
||
<div className="flex flex-col items-center justify-center bg-[#111] border border-[#3a322b] rounded-sm px-6 py-4 min-w-[100px]">
|
||
<span className="font-heading text-3xl font-bold text-[#F0A623]">
|
||
{profile?.reputation ?? 0}
|
||
</span>
|
||
<span className="font-body text-xs text-[#666] mt-1 uppercase tracking-wider">Points</span>
|
||
</div>
|
||
<div className="flex-1">
|
||
<p className="font-body text-sm text-[#aaa] leading-relaxed">
|
||
Earn reputation by posting helpful threads and replies in the community forum.
|
||
</p>
|
||
<Link href="/forum" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
|
||
Go to Forum →
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
|
||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
|
||
Reading Preferences
|
||
</h2>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between py-3 border-b border-[#222]">
|
||
<div>
|
||
<p className="font-body text-sm font-bold text-[#e0e0e0]">Email Newsletter</p>
|
||
<p className="font-body text-xs text-[#666] mt-0.5">Receive weekly broadcast industry news</p>
|
||
</div>
|
||
<Link
|
||
href="/home-page"
|
||
className="font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
|
||
Manage →
|
||
</Link>
|
||
</div>
|
||
<div className="flex items-center justify-between py-3 border-b border-[#222]">
|
||
<div>
|
||
<p className="font-body text-sm font-bold text-[#e0e0e0]">Reading List</p>
|
||
<p className="font-body text-xs text-[#666] mt-0.5">View your saved articles</p>
|
||
</div>
|
||
<Link
|
||
href="/reading-list"
|
||
className="font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
|
||
View List →
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Weekly Digest Toggle */}
|
||
<div className="flex items-center justify-between py-3 border-b border-[#222]">
|
||
<div className="flex-1 pr-4">
|
||
<p className="font-body text-sm font-bold text-[#e0e0e0]">Weekly Forum Digest</p>
|
||
<p className="font-body text-xs text-[#666] mt-0.5">
|
||
Get a weekly email summary of your thread replies, upvotes, and mentions
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
disabled={savingDigest}
|
||
onClick={() => handleToggleDigest(!weeklyDigest)}
|
||
aria-pressed={weeklyDigest}
|
||
aria-label="Toggle weekly digest email"
|
||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] disabled:opacity-50 ${
|
||
weeklyDigest ? "bg-[#F0A623] border-[#F0A623]" : "bg-[#3a322b] border-[#333]"
|
||
}`}>
|
||
<span
|
||
className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 ease-in-out mt-0.5 ${
|
||
weeklyDigest ? "translate-x-5" : "translate-x-0.5"
|
||
}`}
|
||
/>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between py-3">
|
||
<div>
|
||
<p className="font-body text-sm font-bold text-[#e0e0e0]">Member Since</p>
|
||
<p className="font-body text-xs text-[#666] mt-0.5">
|
||
{profile?.created_at
|
||
? new Date(profile.created_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
|
||
: "—"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</main>
|
||
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|