import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { api } from "../api/client"; interface MemberRead { id: string; display_name: string; email: string; avatar_url: string | null; nc_username: string | null; nc_url: string | null; nc_configured: boolean; } const getMe = () => api.get("/auth/me"); const updateSettings = (data: { display_name?: string; nc_url?: string; nc_username?: string; nc_password?: string; avatar_url?: string; }) => api.patch("/auth/me/settings", data); const inputStyle: React.CSSProperties = { width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 14, boxSizing: "border-box", }; function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { const qc = useQueryClient(); const [displayName, setDisplayName] = useState(me.display_name ?? ""); 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 [uploading, setUploading] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); const saveMutation = useMutation({ mutationFn: () => updateSettings({ display_name: displayName || undefined, nc_url: ncUrl || undefined, nc_username: ncUsername || undefined, nc_password: ncPassword || undefined, avatar_url: avatarUrl || undefined, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["me"] }); setSaved(true); setNcPassword(""); setError(null); setTimeout(() => setSaved(false), 3000); }, onError: (err) => setError(err instanceof Error ? err.message : "Save failed"), }); const labelStyle: React.CSSProperties = { display: "block", color: "var(--text-muted)", fontSize: 11, marginBottom: 6, }; return ( <>

PROFILE

{avatarUrl && ( Profile )}
setDisplayName(e.target.value)} style={inputStyle} />

{me.email}

NEXTCLOUD CONNECTION

Configure your personal Nextcloud credentials. When set, all file operations (band folders, song uploads, scans) will use these credentials.

{me.nc_configured ? "Connected" : "Not configured"}
setNcUrl(e.target.value)} placeholder="https://cloud.example.com" style={inputStyle} /> setNcUsername(e.target.value)} style={inputStyle} /> setNcPassword(e.target.value)} placeholder={me.nc_configured ? "•••••••• (leave blank to keep existing)" : ""} style={inputStyle} />

Use an app password from Nextcloud Settings → Security for better security.

AVATAR

{ const file = e.target.files?.[0]; if (file) { setUploading(true); try { const formData = new FormData(); formData.append('file', file); const response = await api.post('/auth/me/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); setAvatarUrl(response.avatar_url || ''); qc.invalidateQueries({ queryKey: ['me'] }); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to upload avatar'); } finally { setUploading(false); } } }} style={{ display: "none" }} id="avatar-upload" />
{avatarUrl && (
Preview { console.error("Failed to load avatar:", avatarUrl); // Set to default avatar on error setAvatarUrl(`https://api.dicebear.com/v6/identicon/svg?seed=${me.id}&backgroundType=gradientLinear&size=128`); }} />
)}
{error &&

{error}

} {saved &&

Settings saved.

}
); } export function SettingsPage() { const navigate = useNavigate(); const { data: me, isLoading } = useQuery({ queryKey: ["me"], queryFn: getMe }); return (

Settings

{isLoading &&

Loading...

} {me && navigate("/")} />}
); }