feat: Added visual confirmation when saving settings and moved button to the top right

This commit is contained in:
Xoconoch
2025-08-20 09:44:49 -05:00
parent 5b261e45f3
commit bbd7d5a985
5 changed files with 151 additions and 52 deletions

View File

@@ -72,6 +72,7 @@ const fetchCredentials = async (service: "spotify" | "deezer"): Promise<Credenti
export function DownloadsTab({ config, isLoading }: DownloadsTabProps) { export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [validationError, setValidationError] = useState<string>(""); const [validationError, setValidationError] = useState<string>("");
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
// Fetch watch config // Fetch watch config
const { data: watchConfig } = useQuery({ const { data: watchConfig } = useQuery({
@@ -97,10 +98,14 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
mutationFn: saveDownloadConfig, mutationFn: saveDownloadConfig,
onSuccess: () => { onSuccess: () => {
toast.success("Download settings saved successfully!"); toast.success("Download settings saved successfully!");
setSaveStatus("success");
setTimeout(() => setSaveStatus("idle"), 3000);
queryClient.invalidateQueries({ queryKey: ["config"] }); queryClient.invalidateQueries({ queryKey: ["config"] });
}, },
onError: (error) => { onError: (error) => {
toast.error(`Failed to save settings: ${error.message}`); toast.error(`Failed to save settings: ${error.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
}, },
}); });
@@ -173,6 +178,24 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="flex items-center justify-end mb-4">
<div className="flex items-center gap-3">
{saveStatus === "success" && (
<span className="text-success text-sm">Saved</span>
)}
{saveStatus === "error" && (
<span className="text-error text-sm">Save failed</span>
)}
<button
type="submit"
disabled={mutation.isPending || !!validationError}
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 Download Settings"}
</button>
</div>
</div>
{/* Download Settings */} {/* Download Settings */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Download Behavior</h3> <h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Download Behavior</h3>
@@ -360,14 +383,6 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
/> />
</div> </div>
</div> </div>
<button
type="submit"
disabled={mutation.isPending || !!validationError}
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 Download Settings"}
</button>
</form> </form>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useRef } from "react"; import { useRef, useState } from "react";
import { useForm, type SubmitHandler } from "react-hook-form"; import { useForm, type SubmitHandler } from "react-hook-form";
import { authApiClient } from "../../lib/api-client"; import { authApiClient } from "../../lib/api-client";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -79,15 +79,20 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const dirInputRef = useRef<HTMLInputElement | null>(null); const dirInputRef = useRef<HTMLInputElement | null>(null);
const trackInputRef = useRef<HTMLInputElement | null>(null); const trackInputRef = useRef<HTMLInputElement | null>(null);
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const mutation = useMutation({ const mutation = useMutation({
mutationFn: saveFormattingConfig, mutationFn: saveFormattingConfig,
onSuccess: () => { onSuccess: () => {
toast.success("Formatting settings saved!"); toast.success("Formatting settings saved!");
setSaveStatus("success");
setTimeout(() => setSaveStatus("idle"), 3000);
queryClient.invalidateQueries({ queryKey: ["config"] }); queryClient.invalidateQueries({ queryKey: ["config"] });
}, },
onError: (error) => { onError: (error) => {
toast.error(`Failed to save settings: ${error.message}`); toast.error(`Failed to save settings: ${error.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
}, },
}); });
@@ -120,6 +125,24 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="flex items-center justify-end mb-4">
<div className="flex items-center gap-3">
{saveStatus === "success" && (
<span className="text-success text-sm">Saved</span>
)}
{saveStatus === "error" && (
<span className="text-error text-sm">Save failed</span>
)}
<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 Formatting Settings"}
</button>
</div>
</div>
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">File Naming</h3> <h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">File Naming</h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -185,14 +208,6 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
<input id="spotifyMetadataToggle" type="checkbox" {...register("spotifyMetadata")} className="h-6 w-6 rounded" /> <input id="spotifyMetadataToggle" type="checkbox" {...register("spotifyMetadata")} className="h-6 w-6 rounded" />
</div> </div>
</div> </div>
<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 Formatting Settings"}
</button>
</form> </form>
); );
} }

View File

@@ -3,7 +3,7 @@ import { authApiClient } from "../../lib/api-client";
import { toast } from "sonner"; import { toast } from "sonner";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useSettings } from "../../contexts/settings-context"; import { useSettings } from "../../contexts/settings-context";
import { useEffect } from "react"; import { useEffect, useState } from "react";
// --- Type Definitions --- // --- Type Definitions ---
interface Credential { interface Credential {
@@ -56,13 +56,21 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
} }
}, [config, reset]); }, [config, reset]);
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const mutation = useMutation({ const mutation = useMutation({
mutationFn: saveGeneralConfig, mutationFn: saveGeneralConfig,
onSuccess: () => { onSuccess: () => {
toast.success("General settings saved!"); toast.success("General settings saved!");
setSaveStatus("success");
setTimeout(() => setSaveStatus("idle"), 3000);
queryClient.invalidateQueries({ queryKey: ["config"] }); queryClient.invalidateQueries({ queryKey: ["config"] });
}, },
onError: (e: Error) => toast.error(`Failed to save: ${e.message}`), onError: (e: Error) => {
toast.error(`Failed to save: ${e.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
}); });
const onSubmit: SubmitHandler<GeneralSettings> = (data) => { const onSubmit: SubmitHandler<GeneralSettings> = (data) => {
@@ -74,6 +82,24 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="flex items-center justify-end mb-4">
<div className="flex items-center gap-3">
{saveStatus === "success" && (
<span className="text-success text-sm">Saved</span>
)}
{saveStatus === "error" && (
<span className="text-error text-sm">Save failed</span>
)}
<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"> <div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Service Defaults</h3> <h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Service Defaults</h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -140,14 +166,6 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
The explicit content filter is controlled by an environment variable and cannot be changed here. The explicit content filter is controlled by an environment variable and cannot be changed here.
</p> </p>
</div> </div>
<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>
</form> </form>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { authApiClient } from "../../lib/api-client"; import { authApiClient } from "../../lib/api-client";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -46,14 +46,21 @@ function SpotifyApiForm() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data, isLoading } = useQuery({ queryKey: ["spotifyApiConfig"], queryFn: fetchSpotifyApiConfig }); const { data, isLoading } = useQuery({ queryKey: ["spotifyApiConfig"], queryFn: fetchSpotifyApiConfig });
const { register, handleSubmit, reset } = useForm<SpotifyApiSettings>(); const { register, handleSubmit, reset } = useForm<SpotifyApiSettings>();
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const mutation = useMutation({ const mutation = useMutation({
mutationFn: saveSpotifyApiConfig, mutationFn: saveSpotifyApiConfig,
onSuccess: () => { onSuccess: () => {
toast.success("Spotify API settings saved!"); toast.success("Spotify API settings saved!");
setSaveStatus("success");
setTimeout(() => setSaveStatus("idle"), 3000);
queryClient.invalidateQueries({ queryKey: ["spotifyApiConfig"] }); queryClient.invalidateQueries({ queryKey: ["spotifyApiConfig"] });
}, },
onError: (e) => toast.error(`Failed to save: ${e.message}`), onError: (e) => {
toast.error(`Failed to save: ${e.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
}); });
useEffect(() => { useEffect(() => {
@@ -66,6 +73,24 @@ function SpotifyApiForm() {
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="flex items-center justify-end mb-2">
<div className="flex items-center gap-3">
{saveStatus === "success" && (
<span className="text-success text-sm">Saved</span>
)}
{saveStatus === "error" && (
<span className="text-error text-sm">Save failed</span>
)}
<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 Spotify API"}
</button>
</div>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="client_id" className="text-content-primary dark:text-content-primary-dark">Client ID</label> <label htmlFor="client_id" className="text-content-primary dark:text-content-primary-dark">Client ID</label>
<input <input
@@ -86,13 +111,6 @@ function SpotifyApiForm() {
placeholder="Optional" placeholder="Optional"
/> />
</div> </div>
<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 Spotify API"}
</button>
</form> </form>
); );
} }
@@ -102,14 +120,21 @@ function WebhookForm() {
const { data, isLoading } = useQuery({ queryKey: ["webhookConfig"], queryFn: fetchWebhookConfig }); const { data, isLoading } = useQuery({ queryKey: ["webhookConfig"], queryFn: fetchWebhookConfig });
const { register, handleSubmit, control, reset, watch } = useForm<WebhookSettings>(); const { register, handleSubmit, control, reset, watch } = useForm<WebhookSettings>();
const currentUrl = watch("url"); const currentUrl = watch("url");
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const mutation = useMutation({ const mutation = useMutation({
mutationFn: saveWebhookConfig, mutationFn: saveWebhookConfig,
onSuccess: () => { onSuccess: () => {
// No toast needed since the function shows one // No toast needed since the function shows one
setSaveStatus("success");
setTimeout(() => setSaveStatus("idle"), 3000);
queryClient.invalidateQueries({ queryKey: ["webhookConfig"] }); queryClient.invalidateQueries({ queryKey: ["webhookConfig"] });
}, },
onError: (e) => toast.error(`Failed to save: ${e.message}`), onError: (e) => {
toast.error(`Failed to save: ${e.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
}); });
const testMutation = useMutation({ const testMutation = useMutation({
@@ -130,6 +155,24 @@ function WebhookForm() {
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="flex items-center justify-end mb-2">
<div className="flex items-center gap-3">
{saveStatus === "success" && (
<span className="text-success text-sm">Saved</span>
)}
{saveStatus === "error" && (
<span className="text-error text-sm">Save failed</span>
)}
<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 Webhook"}
</button>
</div>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="webhookUrl" className="text-content-primary dark:text-content-primary-dark">Webhook URL</label> <label htmlFor="webhookUrl" className="text-content-primary dark:text-content-primary-dark">Webhook URL</label>
<input <input
@@ -168,13 +211,6 @@ function WebhookForm() {
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<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 Webhook"}
</button>
<button <button
type="button" type="button"
onClick={() => testMutation.mutate(currentUrl)} onClick={() => testMutation.mutate(currentUrl)}

View File

@@ -57,6 +57,7 @@ const saveWatchConfig = async (data: Partial<WatchSettings>) => {
export function WatchTab() { export function WatchTab() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [validationError, setValidationError] = useState<string>(""); const [validationError, setValidationError] = useState<string>("");
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const { data: config, isLoading } = useQuery({ const { data: config, isLoading } = useQuery({
queryKey: ["watchConfig"], queryKey: ["watchConfig"],
@@ -87,10 +88,14 @@ export function WatchTab() {
mutationFn: saveWatchConfig, mutationFn: saveWatchConfig,
onSuccess: () => { onSuccess: () => {
toast.success("Watch settings saved successfully!"); toast.success("Watch settings saved successfully!");
setSaveStatus("success");
setTimeout(() => setSaveStatus("idle"), 3000);
queryClient.invalidateQueries({ queryKey: ["watchConfig"] }); queryClient.invalidateQueries({ queryKey: ["watchConfig"] });
}, },
onError: (error) => { onError: (error) => {
toast.error(`Failed to save settings: ${error.message}`); toast.error(`Failed to save settings: ${error.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
}, },
}); });
@@ -155,6 +160,24 @@ export function WatchTab() {
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="flex items-center justify-end mb-4">
<div className="flex items-center gap-3">
{saveStatus === "success" && (
<span className="text-success text-sm">Saved</span>
)}
{saveStatus === "error" && (
<span className="text-error text-sm">Save failed</span>
)}
<button
type="submit"
disabled={mutation.isPending || !!validationError}
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 Watch Settings"}
</button>
</div>
</div>
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Watchlist Behavior</h3> <h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Watchlist Behavior</h3>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -234,14 +257,6 @@ export function WatchTab() {
))} ))}
</div> </div>
</div> </div>
<button
type="submit"
disabled={mutation.isPending || !!validationError}
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 Watch Settings"}
</button>
</form> </form>
); );
} }