Files
spotizerr-dev/spotizerr-ui/src/components/config/GeneralTab.tsx
2025-08-22 10:16:03 -07:00

171 lines
6.5 KiB
TypeScript

import { useForm, type SubmitHandler } from "react-hook-form";
import { authApiClient } from "../../lib/api-client";
import { toast } from "sonner";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useSettings } from "../../contexts/settings-context";
import { useEffect } from "react";
// --- Type Definitions ---
interface Credential {
name: string;
}
interface GeneralSettings {
service: "spotify" | "deezer";
spotify: string;
deezer: string;
}
interface GeneralTabProps {
config: GeneralSettings;
isLoading: boolean;
}
// --- API Functions ---
const fetchCredentials = async (service: "spotify" | "deezer"): Promise<Credential[]> => {
const { data } = await authApiClient.client.get<string[]>(`/credentials/${service}`);
return data.map((name) => ({ name }));
};
const saveGeneralConfig = async (data: Partial<GeneralSettings>) => {
const { data: response } = await authApiClient.client.post("/config", data);
return response;
};
// --- Component ---
export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabProps) {
const queryClient = useQueryClient();
const { settings: globalSettings, isLoading: settingsLoading } = useSettings();
const { data: spotifyAccounts, isLoading: spotifyLoading } = useQuery({
queryKey: ["credentials", "spotify"],
queryFn: () => fetchCredentials("spotify"),
});
const { data: deezerAccounts, isLoading: deezerLoading } = useQuery({
queryKey: ["credentials", "deezer"],
queryFn: () => fetchCredentials("deezer"),
});
const { register, handleSubmit, reset } = useForm<GeneralSettings>({
defaultValues: config,
});
useEffect(() => {
if (config) {
reset(config);
}
}, [config, reset]);
const mutation = useMutation({
mutationFn: saveGeneralConfig,
onSuccess: () => {
toast.success("General settings saved!");
queryClient.invalidateQueries({ queryKey: ["config"] });
},
onError: (e: Error) => {
console.error("Failed to save general settings:", e.message);
toast.error(`Failed to save: ${e.message}`);
},
});
const onSubmit: SubmitHandler<GeneralSettings> = (data) => {
mutation.mutate(data);
};
const isLoading = isConfigLoading || spotifyLoading || deezerLoading || settingsLoading;
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading general settings...</p>;
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="flex items-center justify-end mb-4">
<div className="flex items-center gap-3">
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save General Settings"}
</button>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Service Defaults</h3>
<div className="flex flex-col gap-2">
<label htmlFor="service" className="text-content-primary dark:text-content-primary-dark">
Default Service
</label>
<select
id="service"
{...register("service")}
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="spotify">Spotify</option>
<option value="deezer" disabled>
Deezer (not yet...)
</option>
</select>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Spotify Settings</h3>
<div className="flex flex-col gap-2">
<label htmlFor="spotifyAccount" className="text-content-primary dark:text-content-primary-dark">
Active Spotify Account
</label>
<select
id="spotifyAccount"
{...register("spotify")}
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
{spotifyAccounts?.map((acc) => (
<option key={acc.name} value={acc.name}>
{acc.name}
</option>
))}
</select>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Deezer Settings</h3>
<div className="flex flex-col gap-2">
<label htmlFor="deezerAccount" className="text-content-primary dark:text-content-primary-dark">
Active Deezer Account
</label>
<select
id="deezerAccount"
{...register("deezer")}
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
{deezerAccounts?.map((acc) => (
<option key={acc.name} value={acc.name}>
{acc.name}
</option>
))}
</select>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Content Filters</h3>
<div className="form-item--row">
<label className="text-content-primary dark:text-content-primary-dark">Filter Explicit Content</label>
<div className="flex items-center gap-2">
<span className={`font-semibold ${globalSettings?.explicitFilter ? "text-success" : "text-error"}`}>
{globalSettings?.explicitFilter ? "Enabled" : "Disabled"}
</span>
<span className="text-xs bg-surface-accent dark:bg-surface-accent-dark text-content-primary dark:text-content-primary-dark px-2 py-1 rounded-full">
ENV
</span>
</div>
</div>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
The explicit content filter is controlled by an environment variable and cannot be changed here.
</p>
</div>
</form>
);
}