feat: implement user avatars with DiceBear integration
- Add avatar_url field to MemberSettingsUpdate schema - Create AvatarService for generating default avatars using DiceBear - Update auth service to generate avatars on user registration - Add avatar upload UI to settings page - Update settings endpoint to handle avatar URL updates - Display current avatar in settings with upload/generate options Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -7,6 +7,7 @@ interface MemberRead {
|
||||
id: string;
|
||||
display_name: string;
|
||||
email: string;
|
||||
avatar_url: string | null;
|
||||
nc_username: string | null;
|
||||
nc_url: string | null;
|
||||
nc_configured: boolean;
|
||||
@@ -37,6 +38,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
||||
const [ncUrl, setNcUrl] = useState(me.nc_url ?? "");
|
||||
const [ncUsername, setNcUsername] = useState(me.nc_username ?? "");
|
||||
const [ncPassword, setNcPassword] = useState("");
|
||||
const [avatarUrl, setAvatarUrl] = useState(me.avatar_url ?? "");
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -47,6 +49,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
||||
nc_url: ncUrl || undefined,
|
||||
nc_username: ncUsername || undefined,
|
||||
nc_password: ncPassword || undefined,
|
||||
avatar_url: avatarUrl || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["me"] });
|
||||
@@ -66,9 +69,16 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
||||
<>
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<h2 style={{ fontSize: 13, color: "var(--text-muted)", fontFamily: "monospace", letterSpacing: 1, marginBottom: 16 }}>PROFILE</h2>
|
||||
<label style={labelStyle}>DISPLAY NAME</label>
|
||||
<input value={displayName} onChange={(e) => setDisplayName(e.target.value)} style={inputStyle} />
|
||||
<p style={{ color: "var(--text-subtle)", fontSize: 11, margin: "4px 0 0" }}>{me.email}</p>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 16 }}>
|
||||
{avatarUrl && (
|
||||
<img src={avatarUrl} alt="Profile" style={{ width: 64, height: 64, borderRadius: "50%", objectFit: "cover" }} />
|
||||
)}
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={labelStyle}>DISPLAY NAME</label>
|
||||
<input value={displayName} onChange={(e) => setDisplayName(e.target.value)} style={inputStyle} />
|
||||
<p style={{ color: "var(--text-subtle)", fontSize: 11, margin: "4px 0 0" }}>{me.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
@@ -103,6 +113,78 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<h2 style={{ fontSize: 13, color: "var(--text-muted)", fontFamily: "monospace", letterSpacing: 1, marginBottom: 16 }}>AVATAR</h2>
|
||||
<div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 16 }}>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// In a real app, you would upload the file to a server
|
||||
// and get a URL back. For now, we'll use a placeholder.
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
// This is a simplified approach - in production you'd upload to server
|
||||
setAvatarUrl(event.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}}
|
||||
style={{ display: "none" }}
|
||||
id="avatar-upload"
|
||||
/>
|
||||
<label htmlFor="avatar-upload" style={{
|
||||
background: "var(--accent)",
|
||||
border: "none",
|
||||
borderRadius: 6,
|
||||
color: "var(--accent-fg)",
|
||||
cursor: "pointer",
|
||||
padding: "8px 16px",
|
||||
fontWeight: 600,
|
||||
fontSize: 14
|
||||
}}>
|
||||
Upload Avatar
|
||||
</label>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Generate a new random avatar
|
||||
const randomSeed = Math.random().toString(36).substring(2, 15);
|
||||
setAvatarUrl(`https://api.dicebear.com/v6/identicon/svg?seed=${randomSeed}&backgroundType=gradientLinear&size=128`);
|
||||
}}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 6,
|
||||
color: "var(--text)",
|
||||
cursor: "pointer",
|
||||
padding: "8px 16px",
|
||||
fontSize: 14
|
||||
}}
|
||||
>
|
||||
Generate Random
|
||||
</button>
|
||||
</div>
|
||||
{avatarUrl && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<img src={avatarUrl} alt="Preview" style={{ width: 48, height: 48, borderRadius: "50%", objectFit: "cover" }} />
|
||||
<button
|
||||
onClick={() => setAvatarUrl("")}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--danger)",
|
||||
cursor: "pointer",
|
||||
fontSize: 12
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{error && <p style={{ color: "var(--danger)", fontSize: 13, marginBottom: 12 }}>{error}</p>}
|
||||
{saved && <p style={{ color: "var(--teal)", fontSize: 13, marginBottom: 12 }}>Settings saved.</p>}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user