feat(toaster): Implement Toaster and remove inline save messages - #300, #227

Remove save messages
This commit is contained in:
gitmotion
2025-08-21 22:16:39 -07:00
parent fa8025bf43
commit 95a0daabe3
8 changed files with 155 additions and 140 deletions

View File

@@ -58,7 +58,6 @@ const saveWatchConfig = async (data: Partial<WatchSettings>) => {
export function WatchTab() {
const queryClient = useQueryClient();
const [validationError, setValidationError] = useState<string>("");
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const { data: config, isLoading } = useQuery({
queryKey: ["watchConfig"],
@@ -81,7 +80,7 @@ export function WatchTab() {
const { data: deezerCredentials } = useQuery({
queryKey: ["credentials", "deezer"],
queryFn: () => fetchCredentials("deezer"),
queryFn: () => fetchCredentials("deezer"),
staleTime: 30000,
});
@@ -89,15 +88,12 @@ export function WatchTab() {
mutationFn: saveWatchConfig,
onSuccess: () => {
toast.success("Watch settings saved successfully!");
setSaveStatus("success");
setTimeout(() => setSaveStatus("idle"), 3000);
queryClient.invalidateQueries({ queryKey: ["watchConfig"] });
},
onError: (error: any) => {
const message = error?.response?.data?.error || error?.message || "Unknown error";
toast.error(`Failed to save settings: ${message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
console.error("Failed to save watch settings:", message);
},
});
@@ -115,12 +111,12 @@ export function WatchTab() {
// Validation effect for watch + download method requirement
useEffect(() => {
let error = "";
// Check if watch can be enabled (need download methods)
if (watchEnabled && downloadConfig && !downloadConfig.realTime && !downloadConfig.fallback) {
error = "To enable watch, either Real-time downloading or Download Fallback must be enabled in Download Settings.";
}
// Check fallback account requirements if watch is enabled and fallback is being used
if (watchEnabled && downloadConfig?.fallback && (!spotifyCredentials?.length || !deezerCredentials?.length)) {
const missingServices: string[] = [];
@@ -134,7 +130,7 @@ export function WatchTab() {
if (!error && (Number.isNaN(mir) || mir < 1 || mir > 50)) {
error = "Max items per run must be between 1 and 50.";
}
setValidationError(error);
}, [watchEnabled, downloadConfig?.realTime, downloadConfig?.fallback, spotifyCredentials?.length, deezerCredentials?.length, maxItemsPerRunValue]);
@@ -180,12 +176,6 @@ export function WatchTab() {
<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}
@@ -202,40 +192,38 @@ export function WatchTab() {
<label htmlFor="watchEnabledToggle" className="text-content-primary dark:text-content-primary-dark">Enable Watchlist</label>
<input id="watchEnabledToggle" type="checkbox" {...register("enabled")} className="h-6 w-6 rounded" />
</div>
{/* Download requirements info */}
{downloadConfig && (!downloadConfig.realTime && !downloadConfig.fallback) && (
<div className="p-3 bg-warning/10 border border-warning/20 rounded-lg">
<p className="text-sm text-warning font-medium mb-1">
Download methods required
</p>
<p className="text-sm text-warning font-medium mb-1">Download methods required</p>
<p className="text-xs text-content-muted dark:text-content-muted-dark">
To use watch functionality, enable either Real-time downloading or Download Fallback in the Downloads tab.
</p>
</div>
)}
{/* Fallback account requirements info */}
{downloadConfig?.fallback && (!spotifyCredentials?.length || !deezerCredentials?.length) && (
<div className="p-3 bg-warning/10 border border-warning/20 rounded-lg">
<p className="text-sm text-warning font-medium mb-1">
Fallback accounts required
</p>
<p className="text-sm text-warning font-medium mb-1">Fallback accounts required</p>
<p className="text-xs text-content-muted dark:text-content-muted-dark">
Download Fallback is enabled but requires accounts for both Spotify and Deezer. Configure accounts in the Accounts tab.
</p>
</div>
)}
{/* Validation error display */}
{validationError && (
<div className="p-3 bg-error/10 border border-error/20 rounded-lg">
<p className="text-sm text-error font-medium">{validationError}</p>
</div>
)}
<div className="flex flex-col gap-2">
<label htmlFor="watchPollIntervalSeconds" className="text-content-primary dark:text-content-primary-dark">Watch Poll Interval (seconds)</label>
<label htmlFor="watchPollIntervalSeconds" className="text-content-primary dark:text-content-primary-dark">
Watch Poll Interval (seconds)
</label>
<input
id="watchPollIntervalSeconds"
type="number"
@@ -243,11 +231,15 @@ export function WatchTab() {
{...register("watchPollIntervalSeconds")}
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"
/>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">How often to check for new items in watchlist.</p>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
How often to check for new items in watchlist.
</p>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="maxItemsPerRun" className="text-content-primary dark:text-content-primary-dark">Max Items Per Run</label>
<label htmlFor="maxItemsPerRun" className="text-content-primary dark:text-content-primary-dark">
Max Items Per Run
</label>
<input
id="maxItemsPerRun"
type="number"
@@ -256,13 +248,19 @@ export function WatchTab() {
{...register("maxItemsPerRun")}
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"
/>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Batch size per watch cycle (150).</p>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
Batch size per watch cycle (150).
</p>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Artist Album Groups</h3>
<p className="text-sm text-content-muted dark:text-content-muted-dark">Select which album groups to monitor for watched artists.</p>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">
Artist Album Groups
</h3>
<p className="text-sm text-content-muted dark:text-content-muted-dark">
Select which album groups to monitor for watched artists.
</p>
<div className="grid grid-cols-2 gap-4 pt-2">
{ALBUM_GROUPS.map((group) => (
<Controller