feat(social): add social handles to settings page
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
This commit is contained in:
parent
a87c0cc8a3
commit
e07c23aceb
3 changed files with 116 additions and 8 deletions
|
|
@ -16,6 +16,11 @@ class UserProfileUpdate(BaseModel):
|
||||||
display_name: Optional[str] = None
|
display_name: Optional[str] = None
|
||||||
avatar_bg_color: Optional[str] = None
|
avatar_bg_color: Optional[str] = None
|
||||||
avatar_text: Optional[str] = None
|
avatar_text: Optional[str] = None
|
||||||
|
# Social handles
|
||||||
|
bluesky_handle: Optional[str] = None
|
||||||
|
mastodon_handle: Optional[str] = None
|
||||||
|
instagram_handle: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
|
||||||
# Preset avatar colors - Jewel Tones (Primary Set)
|
# Preset avatar colors - Jewel Tones (Primary Set)
|
||||||
AVATAR_COLORS = [
|
AVATAR_COLORS = [
|
||||||
|
|
@ -46,7 +51,7 @@ def update_my_profile(
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
session: Session = Depends(get_session)
|
session: Session = Depends(get_session)
|
||||||
):
|
):
|
||||||
"""Update current user's bio, avatar, and primary profile"""
|
"""Update current user's bio, avatar, social handles, and primary profile"""
|
||||||
if update.bio is not None:
|
if update.bio is not None:
|
||||||
current_user.bio = update.bio
|
current_user.bio = update.bio
|
||||||
if update.avatar is not None:
|
if update.avatar is not None:
|
||||||
|
|
@ -61,6 +66,18 @@ def update_my_profile(
|
||||||
if len(update.avatar_text) <= 3 and re.match(r'^[A-Za-z0-9]*$', update.avatar_text):
|
if len(update.avatar_text) <= 3 and re.match(r'^[A-Za-z0-9]*$', update.avatar_text):
|
||||||
current_user.avatar_text = update.avatar_text if update.avatar_text else None
|
current_user.avatar_text = update.avatar_text if update.avatar_text else None
|
||||||
|
|
||||||
|
# Social handles (simple sanitization)
|
||||||
|
if update.bluesky_handle is not None:
|
||||||
|
current_user.bluesky_handle = update.bluesky_handle.strip() or None
|
||||||
|
if update.mastodon_handle is not None:
|
||||||
|
current_user.mastodon_handle = update.mastodon_handle.strip() or None
|
||||||
|
if update.instagram_handle is not None:
|
||||||
|
# Remove @ prefix if user includes it
|
||||||
|
handle = update.instagram_handle.strip().lstrip('@')
|
||||||
|
current_user.instagram_handle = handle or None
|
||||||
|
if update.location is not None:
|
||||||
|
current_user.location = update.location.strip() or None
|
||||||
|
|
||||||
if update.username or update.display_name:
|
if update.username or update.display_name:
|
||||||
# Find or create primary profile
|
# Find or create primary profile
|
||||||
query = select(Profile).where(Profile.user_id == current_user.id)
|
query = select(Profile).where(Profile.user_id == current_user.id)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ class UserRead(SQLModel):
|
||||||
bio: Optional[str] = None
|
bio: Optional[str] = None
|
||||||
username: Optional[str] = None
|
username: Optional[str] = None
|
||||||
joined_at: Optional[datetime] = None
|
joined_at: Optional[datetime] = None
|
||||||
|
# Social handles
|
||||||
|
bluesky_handle: Optional[str] = None
|
||||||
|
mastodon_handle: Optional[str] = None
|
||||||
|
instagram_handle: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
|
||||||
class Token(SQLModel):
|
class Token(SQLModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ export default function SettingsPage() {
|
||||||
// Profile state
|
// Profile state
|
||||||
const [bio, setBio] = useState("")
|
const [bio, setBio] = useState("")
|
||||||
const [username, setUsername] = useState("")
|
const [username, setUsername] = useState("")
|
||||||
|
const [location, setLocation] = useState("")
|
||||||
|
const [blueskyHandle, setBlueskyHandle] = useState("")
|
||||||
|
const [mastodonHandle, setMastodonHandle] = useState("")
|
||||||
|
const [instagramHandle, setInstagramHandle] = useState("")
|
||||||
const [profileSaving, setProfileSaving] = useState(false)
|
const [profileSaving, setProfileSaving] = useState(false)
|
||||||
const [profileSaved, setProfileSaved] = useState(false)
|
const [profileSaved, setProfileSaved] = useState(false)
|
||||||
|
|
||||||
|
|
@ -75,6 +79,10 @@ export default function SettingsPage() {
|
||||||
const extUser = user as any
|
const extUser = user as any
|
||||||
setBio(extUser.bio || "")
|
setBio(extUser.bio || "")
|
||||||
setUsername(extUser.email?.split('@')[0] || "")
|
setUsername(extUser.email?.split('@')[0] || "")
|
||||||
|
setLocation(extUser.location || "")
|
||||||
|
setBlueskyHandle(extUser.bluesky_handle || "")
|
||||||
|
setMastodonHandle(extUser.mastodon_handle || "")
|
||||||
|
setInstagramHandle(extUser.instagram_handle || "")
|
||||||
setAvatarBgColor(extUser.avatar_bg_color || "#0F4C81")
|
setAvatarBgColor(extUser.avatar_bg_color || "#0F4C81")
|
||||||
setAvatarText(extUser.avatar_text || "")
|
setAvatarText(extUser.avatar_text || "")
|
||||||
setPrivacySettings({
|
setPrivacySettings({
|
||||||
|
|
@ -96,7 +104,14 @@ export default function SettingsPage() {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": `Bearer ${token}`
|
"Authorization": `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ bio, username })
|
body: JSON.stringify({
|
||||||
|
bio,
|
||||||
|
username,
|
||||||
|
location,
|
||||||
|
bluesky_handle: blueskyHandle,
|
||||||
|
mastodon_handle: mastodonHandle,
|
||||||
|
instagram_handle: instagramHandle
|
||||||
|
})
|
||||||
})
|
})
|
||||||
setProfileSaved(true)
|
setProfileSaved(true)
|
||||||
setTimeout(() => setProfileSaved(false), 2000)
|
setTimeout(() => setProfileSaved(false), 2000)
|
||||||
|
|
@ -221,6 +236,14 @@ export default function SettingsPage() {
|
||||||
setBio={setBio}
|
setBio={setBio}
|
||||||
username={username}
|
username={username}
|
||||||
setUsername={setUsername}
|
setUsername={setUsername}
|
||||||
|
location={location}
|
||||||
|
setLocation={setLocation}
|
||||||
|
blueskyHandle={blueskyHandle}
|
||||||
|
setBlueskyHandle={setBlueskyHandle}
|
||||||
|
mastodonHandle={mastodonHandle}
|
||||||
|
setMastodonHandle={setMastodonHandle}
|
||||||
|
instagramHandle={instagramHandle}
|
||||||
|
setInstagramHandle={setInstagramHandle}
|
||||||
saving={profileSaving}
|
saving={profileSaving}
|
||||||
saved={profileSaved}
|
saved={profileSaved}
|
||||||
onSave={handleSaveProfile}
|
onSave={handleSaveProfile}
|
||||||
|
|
@ -285,6 +308,14 @@ export default function SettingsPage() {
|
||||||
setBio={setBio}
|
setBio={setBio}
|
||||||
username={username}
|
username={username}
|
||||||
setUsername={setUsername}
|
setUsername={setUsername}
|
||||||
|
location={location}
|
||||||
|
setLocation={setLocation}
|
||||||
|
blueskyHandle={blueskyHandle}
|
||||||
|
setBlueskyHandle={setBlueskyHandle}
|
||||||
|
mastodonHandle={mastodonHandle}
|
||||||
|
setMastodonHandle={setMastodonHandle}
|
||||||
|
instagramHandle={instagramHandle}
|
||||||
|
setInstagramHandle={setInstagramHandle}
|
||||||
saving={profileSaving}
|
saving={profileSaving}
|
||||||
saved={profileSaved}
|
saved={profileSaved}
|
||||||
onSave={handleSaveProfile}
|
onSave={handleSaveProfile}
|
||||||
|
|
@ -348,7 +379,14 @@ function SidebarLink({ icon: Icon, label, href, active }: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profile Section
|
// Profile Section
|
||||||
function ProfileSection({ bio, setBio, username, setUsername, saving, saved, onSave }: any) {
|
function ProfileSection({
|
||||||
|
bio, setBio, username, setUsername,
|
||||||
|
location, setLocation,
|
||||||
|
blueskyHandle, setBlueskyHandle,
|
||||||
|
mastodonHandle, setMastodonHandle,
|
||||||
|
instagramHandle, setInstagramHandle,
|
||||||
|
saving, saved, onSave
|
||||||
|
}: any) {
|
||||||
return (
|
return (
|
||||||
<Card id="profile">
|
<Card id="profile">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
@ -375,13 +413,16 @@ function ProfileSection({ bio, setBio, username, setUsername, saving, saved, onS
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label htmlFor="location">Location / Local Scene</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="location"
|
||||||
disabled
|
placeholder="e.g. Portland, OR"
|
||||||
value="(cannot be changed)"
|
value={location}
|
||||||
className="bg-muted"
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Your hometown or local music scene
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -399,6 +440,51 @@ function ProfileSection({ bio, setBio, username, setUsername, saving, saved, onS
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Social Handles Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-sm mb-1">Social Links</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">Connect your accounts (displayed on your public profile)</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="bluesky" className="flex items-center gap-2">
|
||||||
|
<span className="text-sky-500">🦋</span> BlueSky
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="bluesky"
|
||||||
|
placeholder="handle.bsky.social"
|
||||||
|
value={blueskyHandle}
|
||||||
|
onChange={(e) => setBlueskyHandle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="mastodon" className="flex items-center gap-2">
|
||||||
|
<span className="text-purple-500">🐘</span> Mastodon
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="mastodon"
|
||||||
|
placeholder="@user@instance.social"
|
||||||
|
value={mastodonHandle}
|
||||||
|
onChange={(e) => setMastodonHandle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="instagram" className="flex items-center gap-2">
|
||||||
|
<span className="text-pink-500">📷</span> Instagram
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="instagram"
|
||||||
|
placeholder="@username"
|
||||||
|
value={instagramHandle}
|
||||||
|
onChange={(e) => setInstagramHandle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button onClick={onSave} disabled={saving}>
|
<Button onClick={onSave} disabled={saving}>
|
||||||
{saving ? "Saving..." : saved ? "Saved ✓" : "Save Changes"}
|
{saving ? "Saving..." : saved ? "Saved ✓" : "Save Changes"}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue