mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-11 22:40:32 +01:00
Update profile API to handle bio, website, and image fields; enhance profile UI with avatar, bio, and website inputs; fetch full profile data on component mount for better state management.
This commit is contained in:
@@ -73,6 +73,8 @@ const getProfile = async ({ request }: { request: Request }) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
image: user.image,
|
image: user.image,
|
||||||
|
bio: user.bio,
|
||||||
|
website: user.website,
|
||||||
stream: stream
|
stream: stream
|
||||||
? {
|
? {
|
||||||
id: stream.id,
|
id: stream.id,
|
||||||
@@ -110,7 +112,13 @@ const updateProfile = async ({ request }: { request: Request }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { name, username } = body as { name?: string; username?: string }
|
const { name, username, image, bio, website } = body as {
|
||||||
|
name?: string
|
||||||
|
username?: string
|
||||||
|
image?: string | null
|
||||||
|
bio?: string | null
|
||||||
|
website?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
const database = getDb(resolveDatabaseUrl(request))
|
const database = getDb(resolveDatabaseUrl(request))
|
||||||
|
|
||||||
@@ -142,9 +150,12 @@ const updateProfile = async ({ request }: { request: Request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
const updates: Record<string, string> = { updatedAt: new Date().toISOString() }
|
const updates: Record<string, string | null> = { updatedAt: new Date().toISOString() }
|
||||||
if (name !== undefined) updates.name = name
|
if (name !== undefined) updates.name = name
|
||||||
if (username !== undefined) updates.username = username
|
if (username !== undefined) updates.username = username
|
||||||
|
if (image !== undefined) updates.image = image
|
||||||
|
if (bio !== undefined) updates.bio = bio
|
||||||
|
if (website !== undefined) updates.website = website
|
||||||
|
|
||||||
await database
|
await database
|
||||||
.update(users)
|
.update(users)
|
||||||
|
|||||||
@@ -272,22 +272,55 @@ function PreferencesSection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProfileSection({
|
function ProfileSection({
|
||||||
profile,
|
profile: sessionProfile,
|
||||||
onLogout,
|
onLogout,
|
||||||
onChangeEmail,
|
onChangeEmail,
|
||||||
onChangePassword,
|
onChangePassword,
|
||||||
}: {
|
}: {
|
||||||
profile: { name?: string | null; email: string; username?: string | null } | null | undefined
|
profile: { name?: string | null; email: string; username?: string | null; image?: string | null; bio?: string | null; website?: string | null } | null | undefined
|
||||||
onLogout: () => Promise<void>
|
onLogout: () => Promise<void>
|
||||||
onChangeEmail: () => void
|
onChangeEmail: () => void
|
||||||
onChangePassword: () => void
|
onChangePassword: () => void
|
||||||
}) {
|
}) {
|
||||||
const [username, setUsername] = useState(profile?.username ?? "")
|
const [loading, setLoading] = useState(true)
|
||||||
const [name, setName] = useState(profile?.name ?? "")
|
const [profile, setProfile] = useState(sessionProfile)
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [name, setName] = useState("")
|
||||||
|
const [image, setImage] = useState("")
|
||||||
|
const [bio, setBio] = useState("")
|
||||||
|
const [website, setWebsite] = useState("")
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
|
||||||
|
// Fetch full profile from API on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/profile", { credentials: "include" })
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setProfile(data)
|
||||||
|
setUsername(data.username ?? "")
|
||||||
|
setName(data.name ?? "")
|
||||||
|
setImage(data.image ?? "")
|
||||||
|
setBio(data.bio ?? "")
|
||||||
|
setWebsite(data.website ?? "")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to session data
|
||||||
|
setUsername(sessionProfile?.username ?? "")
|
||||||
|
setName(sessionProfile?.name ?? "")
|
||||||
|
setImage(sessionProfile?.image ?? "")
|
||||||
|
setBio(sessionProfile?.bio ?? "")
|
||||||
|
setWebsite(sessionProfile?.website ?? "")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchProfile()
|
||||||
|
}, [sessionProfile])
|
||||||
|
|
||||||
const initials = useMemo(() => {
|
const initials = useMemo(() => {
|
||||||
if (!profile) return "G"
|
if (!profile) return "G"
|
||||||
return (
|
return (
|
||||||
@@ -297,6 +330,8 @@ function ProfileSection({
|
|||||||
)
|
)
|
||||||
}, [profile])
|
}, [profile])
|
||||||
|
|
||||||
|
const avatarUrl = image || `https://api.dicebear.com/7.x/initials/svg?seed=${username || profile?.email || "user"}`
|
||||||
|
|
||||||
const handleSaveProfile = async () => {
|
const handleSaveProfile = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -306,7 +341,13 @@ function ProfileSection({
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify({ name, username: username.toLowerCase() }),
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
username: username.toLowerCase(),
|
||||||
|
image: image || null,
|
||||||
|
bio: bio || null,
|
||||||
|
website: website || null,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -322,7 +363,27 @@ function ProfileSection({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanges = username !== (profile?.username ?? "") || name !== (profile?.name ?? "")
|
const hasChanges =
|
||||||
|
username !== (profile?.username ?? "") ||
|
||||||
|
name !== (profile?.name ?? "") ||
|
||||||
|
image !== (profile?.image ?? "") ||
|
||||||
|
bio !== (profile?.bio ?? "") ||
|
||||||
|
website !== (profile?.website ?? "")
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div id="profile" className="scroll-mt-24">
|
||||||
|
<SectionHeader
|
||||||
|
title="Profile"
|
||||||
|
description="Manage your account details and security."
|
||||||
|
/>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="h-32 bg-white/5 rounded-2xl animate-pulse" />
|
||||||
|
<div className="h-64 bg-white/5 rounded-2xl animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="profile" className="scroll-mt-24">
|
<div id="profile" className="scroll-mt-24">
|
||||||
@@ -355,6 +416,29 @@ function ProfileSection({
|
|||||||
|
|
||||||
<SettingCard title="Public Profile">
|
<SettingCard title="Public Profile">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Profile Picture */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Profile Picture</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt="Profile"
|
||||||
|
className="w-16 h-16 rounded-full bg-white/10 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={image}
|
||||||
|
onChange={(e) => setImage(e.target.value)}
|
||||||
|
placeholder="https://example.com/your-photo.jpg"
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500 text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-white/50 mt-1">
|
||||||
|
Enter a URL to your profile picture
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm text-white/70">Display Name</label>
|
<label className="text-sm text-white/70">Display Name</label>
|
||||||
<input
|
<input
|
||||||
@@ -381,6 +465,26 @@ function ProfileSection({
|
|||||||
This is your public stream URL. Only lowercase letters, numbers, hyphens, and underscores.
|
This is your public stream URL. Only lowercase letters, numbers, hyphens, and underscores.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Bio</label>
|
||||||
|
<textarea
|
||||||
|
value={bio}
|
||||||
|
onChange={(e) => setBio(e.target.value)}
|
||||||
|
placeholder="Tell people about yourself..."
|
||||||
|
rows={3}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Website</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={website}
|
||||||
|
onChange={(e) => setWebsite(e.target.value)}
|
||||||
|
placeholder="https://yourwebsite.com"
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{error && <p className="text-sm text-rose-400">{error}</p>}
|
{error && <p className="text-sm text-rose-400">{error}</p>}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
{saved && <span className="text-sm text-teal-400 flex items-center gap-1"><Check className="w-4 h-4" /> Saved</span>}
|
{saved && <span className="text-sm text-teal-400 flex items-center gap-1"><Check className="w-4 h-4" /> Saved</span>}
|
||||||
|
|||||||
Reference in New Issue
Block a user