From bbd7d5a98513a13562dba77a4b85a2f34a5542d5 Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Wed, 20 Aug 2025 09:44:49 -0500 Subject: [PATCH] feat: Added visual confirmation when saving settings and moved button to the top right --- .../src/components/config/DownloadsTab.tsx | 31 +++++--- .../src/components/config/FormattingTab.tsx | 33 ++++++--- .../src/components/config/GeneralTab.tsx | 38 +++++++--- .../src/components/config/ServerTab.tsx | 70 ++++++++++++++----- .../src/components/config/WatchTab.tsx | 31 +++++--- 5 files changed, 151 insertions(+), 52 deletions(-) diff --git a/spotizerr-ui/src/components/config/DownloadsTab.tsx b/spotizerr-ui/src/components/config/DownloadsTab.tsx index bb491b8..81eec47 100644 --- a/spotizerr-ui/src/components/config/DownloadsTab.tsx +++ b/spotizerr-ui/src/components/config/DownloadsTab.tsx @@ -72,6 +72,7 @@ const fetchCredentials = async (service: "spotify" | "deezer"): Promise(""); + const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); // Fetch watch config const { data: watchConfig } = useQuery({ @@ -97,10 +98,14 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) { mutationFn: saveDownloadConfig, onSuccess: () => { toast.success("Download settings saved successfully!"); + setSaveStatus("success"); + setTimeout(() => setSaveStatus("idle"), 3000); queryClient.invalidateQueries({ queryKey: ["config"] }); }, onError: (error) => { 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 (
+
+
+ {saveStatus === "success" && ( + Saved + )} + {saveStatus === "error" && ( + Save failed + )} + +
+
+ {/* Download Settings */}

Download Behavior

@@ -360,14 +383,6 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) { />
- -
); } diff --git a/spotizerr-ui/src/components/config/FormattingTab.tsx b/spotizerr-ui/src/components/config/FormattingTab.tsx index fcd05ef..57ea7a7 100644 --- a/spotizerr-ui/src/components/config/FormattingTab.tsx +++ b/spotizerr-ui/src/components/config/FormattingTab.tsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useRef, useState } from "react"; import { useForm, type SubmitHandler } from "react-hook-form"; import { authApiClient } from "../../lib/api-client"; import { toast } from "sonner"; @@ -79,15 +79,20 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) { const queryClient = useQueryClient(); const dirInputRef = useRef(null); const trackInputRef = useRef(null); + const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); const mutation = useMutation({ mutationFn: saveFormattingConfig, onSuccess: () => { toast.success("Formatting settings saved!"); + setSaveStatus("success"); + setTimeout(() => setSaveStatus("idle"), 3000); queryClient.invalidateQueries({ queryKey: ["config"] }); }, onError: (error) => { 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 (
+
+
+ {saveStatus === "success" && ( + Saved + )} + {saveStatus === "error" && ( + Save failed + )} + +
+
+

File Naming

@@ -185,14 +208,6 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
- -
); } diff --git a/spotizerr-ui/src/components/config/GeneralTab.tsx b/spotizerr-ui/src/components/config/GeneralTab.tsx index a0aa968..171cd55 100644 --- a/spotizerr-ui/src/components/config/GeneralTab.tsx +++ b/spotizerr-ui/src/components/config/GeneralTab.tsx @@ -3,7 +3,7 @@ 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"; +import { useEffect, useState } from "react"; // --- Type Definitions --- interface Credential { @@ -56,13 +56,21 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro } }, [config, reset]); + const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); + const mutation = useMutation({ mutationFn: saveGeneralConfig, onSuccess: () => { toast.success("General settings saved!"); + setSaveStatus("success"); + setTimeout(() => setSaveStatus("idle"), 3000); 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 = (data) => { @@ -74,6 +82,24 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro return (
+
+
+ {saveStatus === "success" && ( + Saved + )} + {saveStatus === "error" && ( + Save failed + )} + +
+
+

Service Defaults

@@ -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.

- - ); } diff --git a/spotizerr-ui/src/components/config/ServerTab.tsx b/spotizerr-ui/src/components/config/ServerTab.tsx index c71c19c..e0b288c 100644 --- a/spotizerr-ui/src/components/config/ServerTab.tsx +++ b/spotizerr-ui/src/components/config/ServerTab.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { authApiClient } from "../../lib/api-client"; import { toast } from "sonner"; @@ -46,14 +46,21 @@ function SpotifyApiForm() { const queryClient = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ["spotifyApiConfig"], queryFn: fetchSpotifyApiConfig }); const { register, handleSubmit, reset } = useForm(); + const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); const mutation = useMutation({ mutationFn: saveSpotifyApiConfig, onSuccess: () => { toast.success("Spotify API settings saved!"); + setSaveStatus("success"); + setTimeout(() => setSaveStatus("idle"), 3000); 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(() => { @@ -66,6 +73,24 @@ function SpotifyApiForm() { return (
+
+
+ {saveStatus === "success" && ( + Saved + )} + {saveStatus === "error" && ( + Save failed + )} + +
+
+
-
); } @@ -102,14 +120,21 @@ function WebhookForm() { const { data, isLoading } = useQuery({ queryKey: ["webhookConfig"], queryFn: fetchWebhookConfig }); const { register, handleSubmit, control, reset, watch } = useForm(); const currentUrl = watch("url"); + const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); const mutation = useMutation({ mutationFn: saveWebhookConfig, onSuccess: () => { // No toast needed since the function shows one + setSaveStatus("success"); + setTimeout(() => setSaveStatus("idle"), 3000); 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({ @@ -130,6 +155,24 @@ function WebhookForm() { return (
+
+
+ {saveStatus === "success" && ( + Saved + )} + {saveStatus === "error" && ( + Save failed + )} + +
+
+
- +
+
+

Watchlist Behavior

@@ -234,14 +257,6 @@ export function WatchTab() { ))}
- - ); }