From f9894143232d6bd577936b856594e1ba8ab8357c Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:38:18 -0800 Subject: [PATCH] feat: Redesign settings page with comprehensive sections, sidebar nav, and distinct avatar colors --- backend/routers/users.py | 41 +- frontend/app/settings/page.tsx | 714 +++++++++++++++--- .../components/profile/avatar-settings.tsx | 20 +- 3 files changed, 656 insertions(+), 119 deletions(-) diff --git a/backend/routers/users.py b/backend/routers/users.py index 1486e19..cc96303 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -17,29 +17,28 @@ class UserProfileUpdate(BaseModel): avatar_bg_color: Optional[str] = None avatar_text: Optional[str] = None -# Preset avatar colors +# Preset avatar colors - more distinct palette AVATAR_COLORS = [ - "#3B82F6", # Blue - "#EF4444", # Red - "#10B981", # Green - "#F59E0B", # Amber - "#8B5CF6", # Purple - "#EC4899", # Pink - "#6366F1", # Indigo - "#14B8A6", # Teal + "#1E3A8A", # Deep Blue + "#DC2626", # Bright Red + "#059669", # Emerald + "#D97706", # Amber/Orange + "#7C3AED", # Vibrant Purple + "#DB2777", # Magenta Pink + "#0F766E", # Teal + "#1F2937", # Dark Gray + "#B45309", # Burnt Orange + "#4338CA", # Indigo + "#0891B2", # Cyan + "#65A30D", # Lime Green ] class AvatarUpdate(BaseModel): bg_color: Optional[str] = None text: Optional[str] = None # 1-3 alphanumeric chars -@router.get("/{user_id}", response_model=UserRead) -def get_user_public(user_id: int, session: Session = Depends(get_session)): - """Get public user profile""" - user = session.get(User, user_id) - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user +# Note: Dynamic routes like /{user_id} are placed at the END of this file +# to avoid conflicts with static routes like /me and /avatar @router.patch("/me", response_model=UserRead) def update_my_profile( @@ -241,3 +240,13 @@ def get_user_groups( .limit(limit) ).all() return groups + +# --- Dynamic ID Routes (must be last to avoid conflicts with /me, /avatar) --- + +@router.get("/{user_id}", response_model=UserRead) +def get_user_public(user_id: int, session: Session = Depends(get_session)): + """Get public user profile""" + user = session.get(User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index ecb68a2..82c4319 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -7,28 +7,70 @@ import { Switch } from "@/components/ui/switch" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Button } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Separator } from "@/components/ui/separator" import { usePreferences } from "@/contexts/preferences-context" import { useAuth } from "@/contexts/auth-context" import { getApiUrl } from "@/lib/api-config" -import { AvatarSettings } from "@/components/profile/avatar-settings" +import { UserAvatar } from "@/components/ui/user-avatar" +import { + User, + Palette, + Bell, + Eye, + Shield, + Sparkles, + Check, + ArrowLeft +} from "lucide-react" +import Link from "next/link" + +// Avatar color palette - more distinct colors +const PRESET_COLORS = [ + { value: "#1E3A8A", name: "Deep Blue" }, + { value: "#DC2626", name: "Bright Red" }, + { value: "#059669", name: "Emerald" }, + { value: "#D97706", name: "Amber" }, + { value: "#7C3AED", name: "Violet" }, + { value: "#DB2777", name: "Magenta" }, + { value: "#0F766E", name: "Teal" }, + { value: "#1F2937", name: "Slate" }, + { value: "#B45309", name: "Burnt Orange" }, + { value: "#4338CA", name: "Indigo" }, + { value: "#0891B2", name: "Cyan" }, + { value: "#65A30D", name: "Lime" }, +] export default function SettingsPage() { const { preferences, updatePreferences, loading } = usePreferences() - const { user } = useAuth() + const { user, refreshUser } = useAuth() + + // Profile state const [bio, setBio] = useState("") - const [saving, setSaving] = useState(false) - const [saved, setSaved] = useState(false) + const [username, setUsername] = useState("") + const [profileSaving, setProfileSaving] = useState(false) + const [profileSaved, setProfileSaved] = useState(false) + + // Avatar state + const [avatarBgColor, setAvatarBgColor] = useState("#1E3A8A") + const [avatarText, setAvatarText] = useState("") + const [avatarSaving, setAvatarSaving] = useState(false) + const [avatarSaved, setAvatarSaved] = useState(false) + const [avatarError, setAvatarError] = useState("") useEffect(() => { - // Bio might be in extended user response - check dynamically - if (user && 'bio' in user && typeof (user as Record).bio === 'string') { - setBio((user as Record).bio as string) + if (user) { + const extUser = user as any + setBio(extUser.bio || "") + setUsername(extUser.email?.split('@')[0] || "") + setAvatarBgColor(extUser.avatar_bg_color || "#1E3A8A") + setAvatarText(extUser.avatar_text || "") } }, [user]) const handleSaveProfile = async () => { - setSaving(true) - setSaved(false) + setProfileSaving(true) + setProfileSaved(false) const token = localStorage.getItem("token") try { await fetch(`${getApiUrl()}/users/me`, { @@ -37,111 +79,593 @@ export default function SettingsPage() { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, - body: JSON.stringify({ bio }) + body: JSON.stringify({ bio, username }) }) - setSaved(true) - setTimeout(() => setSaved(false), 2000) + setProfileSaved(true) + setTimeout(() => setProfileSaved(false), 2000) } catch (e) { console.error(e) } finally { - setSaving(false) + setProfileSaving(false) + } + } + + const handleAvatarTextChange = (value: string) => { + const cleaned = value.replace(/[^A-Za-z0-9]/g, '').slice(0, 3).toUpperCase() + setAvatarText(cleaned) + setAvatarError("") + } + + const handleSaveAvatar = async () => { + setAvatarSaving(true) + setAvatarSaved(false) + setAvatarError("") + + try { + const token = localStorage.getItem("token") + const res = await fetch(`${getApiUrl()}/users/me/avatar`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + bg_color: avatarBgColor, + text: avatarText || null, + }), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.detail || "Failed to save") + } + + setAvatarSaved(true) + refreshUser?.() + setTimeout(() => setAvatarSaved(false), 2000) + } catch (e: any) { + setAvatarError(e.message || "Failed to save avatar") + } finally { + setAvatarSaving(false) } } if (loading) { - return
Loading settings...
+ return ( +
+
Loading settings...
+
+ ) + } + + if (!user) { + return ( +
+

Please Log In

+

You need to be logged in to access settings.

+ + + +
+ ) } return ( -
-

Settings

- - {/* Profile Section */} - - - Profile - - Tell other fans about yourself. - - - -
- -