feat: band NC folder config, fix watcher event filter, add light/dark theme
- Add PATCH /bands/{id} endpoint for admins to update nc_folder_path
- Add band NC scan folder UI panel with inline edit
- Fix watcher: use activity type field (not human-readable subject) for upload detection
- Reorder watcher filters: audio extension check first, then band path, then type
- Add dark/light theme toggle using GitHub Primer-inspired CSS custom properties
- All inline styles migrated to CSS variables for theme-awareness
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,17 @@ const updateSettings = (data: {
|
||||
nc_password?: string;
|
||||
}) => api.patch<MemberRead>("/auth/me/settings", data);
|
||||
|
||||
// Rendered only after `me` is loaded — initializes form state directly from props.
|
||||
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 ?? "");
|
||||
@@ -48,35 +58,39 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
||||
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 (
|
||||
<>
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<h2 style={{ fontSize: 13, color: "#5A6480", fontFamily: "monospace", letterSpacing: 1, marginBottom: 16 }}>PROFILE</h2>
|
||||
<label style={{ display: "block", color: "#5A6480", fontSize: 11, marginBottom: 6 }}>DISPLAY NAME</label>
|
||||
<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: "#38496A", fontSize: 11, margin: "4px 0 0" }}>{me.email}</p>
|
||||
<p style={{ color: "var(--text-subtle)", fontSize: 11, margin: "4px 0 0" }}>{me.email}</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<h2 style={{ fontSize: 13, color: "#5A6480", fontFamily: "monospace", letterSpacing: 1, marginBottom: 8 }}>NEXTCLOUD CONNECTION</h2>
|
||||
<p style={{ color: "#38496A", fontSize: 12, marginBottom: 16 }}>
|
||||
<h2 style={{ fontSize: 13, color: "var(--text-muted)", fontFamily: "monospace", letterSpacing: 1, marginBottom: 8 }}>NEXTCLOUD CONNECTION</h2>
|
||||
<p style={{ color: "var(--text-subtle)", fontSize: 12, marginBottom: 16 }}>
|
||||
Configure your personal Nextcloud credentials. When set, all file operations (band folders, song uploads, scans) will use these credentials.
|
||||
</p>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 16 }}>
|
||||
<span style={{ display: "inline-block", width: 8, height: 8, borderRadius: "50%", background: me.nc_configured ? "#38C9A8" : "#5A6480" }} />
|
||||
<span style={{ fontSize: 12, color: me.nc_configured ? "#38C9A8" : "#5A6480" }}>
|
||||
<span style={{ display: "inline-block", width: 8, height: 8, borderRadius: "50%", background: me.nc_configured ? "var(--teal)" : "var(--text-muted)" }} />
|
||||
<span style={{ fontSize: 12, color: me.nc_configured ? "var(--teal)" : "var(--text-muted)" }}>
|
||||
{me.nc_configured ? "Connected" : "Not configured"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label style={{ display: "block", color: "#5A6480", fontSize: 11, marginBottom: 6 }}>NEXTCLOUD URL</label>
|
||||
<label style={labelStyle}>NEXTCLOUD URL</label>
|
||||
<input value={ncUrl} onChange={(e) => setNcUrl(e.target.value)} placeholder="https://cloud.example.com" style={inputStyle} />
|
||||
|
||||
<label style={{ display: "block", color: "#5A6480", fontSize: 11, marginBottom: 6, marginTop: 12 }}>USERNAME</label>
|
||||
<label style={{ ...labelStyle, marginTop: 12 }}>USERNAME</label>
|
||||
<input value={ncUsername} onChange={(e) => setNcUsername(e.target.value)} style={inputStyle} />
|
||||
|
||||
<label style={{ display: "block", color: "#5A6480", fontSize: 11, marginBottom: 6, marginTop: 12 }}>PASSWORD / APP PASSWORD</label>
|
||||
<label style={{ ...labelStyle, marginTop: 12 }}>PASSWORD / APP PASSWORD</label>
|
||||
<input
|
||||
type="password"
|
||||
value={ncPassword}
|
||||
@@ -84,25 +98,25 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
||||
placeholder={me.nc_configured ? "•••••••• (leave blank to keep existing)" : ""}
|
||||
style={inputStyle}
|
||||
/>
|
||||
<p style={{ color: "#38496A", fontSize: 11, margin: "4px 0 0" }}>
|
||||
<p style={{ color: "var(--text-subtle)", fontSize: 11, margin: "4px 0 0" }}>
|
||||
Use an app password from Nextcloud Settings → Security for better security.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{error && <p style={{ color: "#E85878", fontSize: 13, marginBottom: 12 }}>{error}</p>}
|
||||
{saved && <p style={{ color: "#38C9A8", fontSize: 13, marginBottom: 12 }}>Settings saved.</p>}
|
||||
{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>}
|
||||
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
style={{ background: "#F0A840", border: "none", borderRadius: 6, color: "#080A0E", cursor: "pointer", padding: "10px 24px", fontWeight: 600, fontSize: 14 }}
|
||||
style={{ background: "var(--accent)", border: "none", borderRadius: 6, color: "var(--accent-fg)", cursor: "pointer", padding: "10px 24px", fontWeight: 600, fontSize: 14 }}
|
||||
>
|
||||
{saveMutation.isPending ? "Saving…" : "Save Settings"}
|
||||
</button>
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{ background: "none", border: "1px solid #1C2235", borderRadius: 6, color: "#5A6480", cursor: "pointer", padding: "10px 18px", fontSize: 14 }}
|
||||
style={{ background: "none", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text-muted)", cursor: "pointer", padding: "10px 18px", fontSize: 14 }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -116,32 +130,21 @@ export function SettingsPage() {
|
||||
const { data: me, isLoading } = useQuery({ queryKey: ["me"], queryFn: getMe });
|
||||
|
||||
return (
|
||||
<div style={{ background: "#080A0E", minHeight: "100vh", color: "#E2E6F0", padding: 24 }}>
|
||||
<div style={{ background: "var(--bg)", minHeight: "100vh", color: "var(--text)", padding: 24 }}>
|
||||
<div style={{ maxWidth: 540, margin: "0 auto" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 32 }}>
|
||||
<button
|
||||
onClick={() => navigate("/")}
|
||||
style={{ background: "none", border: "none", color: "#5A6480", cursor: "pointer", fontSize: 13, padding: 0 }}
|
||||
style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 13, padding: 0 }}
|
||||
>
|
||||
← All Bands
|
||||
</button>
|
||||
<h1 style={{ color: "#F0A840", fontFamily: "monospace", margin: 0, fontSize: 20 }}>Settings</h1>
|
||||
<h1 style={{ color: "var(--accent)", fontFamily: "monospace", margin: 0, fontSize: 20 }}>Settings</h1>
|
||||
</div>
|
||||
|
||||
{isLoading && <p style={{ color: "#5A6480" }}>Loading...</p>}
|
||||
{isLoading && <p style={{ color: "var(--text-muted)" }}>Loading...</p>}
|
||||
{me && <SettingsForm me={me} onBack={() => navigate("/")} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
padding: "8px 12px",
|
||||
background: "#131720",
|
||||
border: "1px solid #1C2235",
|
||||
borderRadius: 6,
|
||||
color: "#E2E6F0",
|
||||
fontSize: 14,
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user