Merge pull request #301 from gitmotion/enhancement/implement-toaster

Implement Toaster - Issue #300, #227
This commit is contained in:
Spotizerr
2025-08-22 11:25:14 -06:00
committed by GitHub
8 changed files with 155 additions and 140 deletions

View File

@@ -53,7 +53,7 @@ const CONVERSION_FORMATS: Record<string, string[]> = {
// --- API Functions ---
const saveDownloadConfig = async (data: Partial<DownloadSettings>) => {
const payload: any = { ...data };
const payload: Partial<DownloadSettings> = { ...data };
const { data: response } = await authApiClient.client.post("/config", payload);
return response;
};
@@ -72,7 +72,6 @@ const fetchCredentials = async (service: "spotify" | "deezer"): Promise<Credenti
export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
const queryClient = useQueryClient();
const [validationError, setValidationError] = useState<string>("");
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
// Fetch watch config
const { data: watchConfig } = useQuery({
@@ -89,7 +88,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
});
const { data: deezerCredentials } = useQuery({
queryKey: ["credentials", "deezer"],
queryKey: ["credentials", "deezer"],
queryFn: () => fetchCredentials("deezer"),
staleTime: 30000,
});
@@ -98,14 +97,11 @@ 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) => {
console.error("Failed to save settings", error.message);
toast.error(`Failed to save settings: ${error.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
});
@@ -126,12 +122,12 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
// Validation effect for watch + download method requirement
useEffect(() => {
let error = "";
// Check watch requirements
if (watchConfig?.enabled && !realTime && !fallback) {
error = "When watch is enabled, either Real-time downloading or Download Fallback (or both) must be enabled.";
}
// Check fallback account requirements
if (fallback && (!spotifyCredentials?.length || !deezerCredentials?.length)) {
const missingServices: string[] = [];
@@ -139,7 +135,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
if (!deezerCredentials?.length) missingServices.push("Deezer");
error = `Download Fallback requires accounts to be configured for both services. Missing: ${missingServices.join(", ")}. Configure accounts in the Accounts tab.`;
}
setValidationError(error);
}, [watchConfig?.enabled, realTime, fallback, spotifyCredentials?.length, deezerCredentials?.length]);
@@ -180,12 +176,6 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
<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}
@@ -248,7 +238,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
<p className="text-sm text-content-muted dark:text-content-muted-dark">
When enabled, downloads will be organized in user-specific subdirectories (downloads/username/...)
</p>
{/* Watch validation info */}
{watchConfig?.enabled && (
<div className="p-3 bg-info/10 border border-info/20 rounded-lg">
@@ -260,7 +250,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
</p>
</div>
)}
{/* Fallback account requirements info */}
{fallback && (!spotifyCredentials?.length || !deezerCredentials?.length) && (
<div className="p-3 bg-warning/10 border border-warning/20 rounded-lg">
@@ -272,7 +262,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
</p>
</div>
)}
{/* Validation error display */}
{validationError && (
<div className="p-3 bg-error/10 border border-error/20 rounded-lg">

View File

@@ -1,4 +1,4 @@
import { useRef, useState } from "react";
import { useRef } from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { authApiClient } from "../../lib/api-client";
import { toast } from "sonner";
@@ -80,20 +80,16 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
const queryClient = useQueryClient();
const dirInputRef = useRef<HTMLInputElement | null>(null);
const trackInputRef = useRef<HTMLInputElement | null>(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) => {
console.error("Failed to save formatting settings:", error.message);
toast.error(`Failed to save settings: ${error.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
});
@@ -131,12 +127,6 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
<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}

View File

@@ -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, useState } from "react";
import { useEffect } from "react";
// --- Type Definitions ---
interface Credential {
@@ -56,20 +56,15 @@ 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) => {
console.error("Failed to save general settings:", e.message);
toast.error(`Failed to save: ${e.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
});
@@ -84,12 +79,6 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
<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}
@@ -103,14 +92,18 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
<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>
<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>
<option value="deezer" disabled>
Deezer (not yet...)
</option>
</select>
</div>
</div>
@@ -118,7 +111,9 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
<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>
<label htmlFor="spotifyAccount" className="text-content-primary dark:text-content-primary-dark">
Active Spotify Account
</label>
<select
id="spotifyAccount"
{...register("spotify")}
@@ -136,7 +131,9 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
<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>
<label htmlFor="deezerAccount" className="text-content-primary dark:text-content-primary-dark">
Active Deezer Account
</label>
<select
id="deezerAccount"
{...register("deezer")}
@@ -159,7 +156,9 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
<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>
<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">

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
import { authApiClient } from "../../lib/api-client";
import { toast } from "sonner";
@@ -46,20 +46,16 @@ function SpotifyApiForm() {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({ queryKey: ["spotifyApiConfig"], queryFn: fetchSpotifyApiConfig });
const { register, handleSubmit, reset } = useForm<SpotifyApiSettings>();
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) => {
console.error("Failed to save Spotify API settings:", e.message);
toast.error(`Failed to save: ${e.message}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
});
@@ -75,12 +71,6 @@ function SpotifyApiForm() {
<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}
@@ -120,20 +110,16 @@ function WebhookForm() {
const { data, isLoading } = useQuery({ queryKey: ["webhookConfig"], queryFn: fetchWebhookConfig });
const { register, handleSubmit, control, reset, watch } = useForm<WebhookSettings>();
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}`);
setSaveStatus("error");
setTimeout(() => setSaveStatus("idle"), 3000);
},
});
@@ -157,12 +143,6 @@ function WebhookForm() {
<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}

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

View File

@@ -0,0 +1,47 @@
import React, { useEffect, useState } from "react";
import { Toaster as SonnerToaster } from "sonner";
import { getEffectiveTheme } from "@/lib/theme";
// Centralized Toaster wrapper so we can control defaults + theme.
// Tailwind dark mode relies on .dark on <html>. Sonner auto-detects, but we can also
// explicitly set className variants for better contrast. (as needed/commented out below)
export const Toaster: React.FC = () => {
const [theme, setTheme] = useState<"light" | "dark" | "system">(getEffectiveTheme());
useEffect(() => {
const update = () => setTheme(getEffectiveTheme());
window.addEventListener("app-theme-changed", update);
window.addEventListener("storage", (e) => {
if (e.key === "theme") update();
});
return () => {
window.removeEventListener("app-theme-changed", update);
};
}, []);
return (
<SonnerToaster
position="top-center"
theme={theme}
richColors
toastOptions={{
duration: 3000,
classNames: {
// toast:
// "bg-white dark:bg-surface-secondary-dark text-content-primary dark:text-content-primary-dark border border-line dark:border-border-dark shadow-md",
title: "font-medium",
description: "text-content-secondary dark:text-content-secondary-dark",
// success: "bg-success/10 dark:bg-success/20 text-success border-success/40",
// error: "bg-error/10 dark:bg-error/20 text-error border-error/40",
// warning: "bg-warning/10 dark:bg-warning/20 text-warning border-warning/40",
// info: "bg-info/10 dark:bg-info/20 text-info border-info/40",
closeButton:
"text-content-muted dark:text-content-muted-dark hover:text-content-primary dark:hover:text-content-primary-dark",
actionButton: "bg-primary text-white hover:bg-primary-hover",
},
}}
/>
);
};
export default Toaster;

View File

@@ -1,71 +1,80 @@
// Theme management functions
export function getTheme(): 'light' | 'dark' | 'system' {
return (localStorage.getItem('theme') as 'light' | 'dark' | 'system') || 'system';
export function getTheme(): "light" | "dark" | "system" {
return (localStorage.getItem("theme") as "light" | "dark" | "system") || "system";
}
export function setTheme(theme: 'light' | 'dark' | 'system') {
localStorage.setItem('theme', theme);
export function setTheme(theme: "light" | "dark" | "system") {
localStorage.setItem("theme", theme);
applyTheme(theme);
dispatchThemeChange();
}
export function toggleTheme() {
const currentTheme = getTheme();
let nextTheme: 'light' | 'dark' | 'system';
let nextTheme: "light" | "dark" | "system";
switch (currentTheme) {
case 'light':
nextTheme = 'dark';
case "light":
nextTheme = "dark";
break;
case 'dark':
nextTheme = 'system';
case "dark":
nextTheme = "system";
break;
default:
nextTheme = 'light';
nextTheme = "light";
break;
}
setTheme(nextTheme);
return nextTheme;
}
function applyTheme(theme: 'light' | 'dark' | 'system') {
function applyTheme(theme: "light" | "dark" | "system") {
const root = document.documentElement;
if (theme === 'system') {
if (theme === "system") {
// Use system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
} else if (theme === 'dark') {
root.classList.add('dark');
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (prefersDark) root.classList.add("dark");
else root.classList.remove("dark");
} else if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove('dark');
root.classList.remove("dark");
}
}
function dispatchThemeChange() {
window.dispatchEvent(new CustomEvent("app-theme-changed"));
}
export function getEffectiveTheme(): "light" | "dark" {
const stored = getTheme();
if (stored === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return stored;
}
// Dark mode detection and setup
export function setupDarkMode() {
// First, ensure we start with a clean slate
document.documentElement.classList.remove('dark');
document.documentElement.classList.remove("dark");
const savedTheme = getTheme();
applyTheme(savedTheme);
dispatchThemeChange();
// Listen for system theme changes (only when using system theme)
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
// Only respond to system changes when we're in system mode
if (getTheme() === 'system') {
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
if (getTheme() === "system") {
if (e.matches) document.documentElement.classList.add("dark");
else document.documentElement.classList.remove("dark");
dispatchThemeChange();
}
};
mediaQuery.addEventListener('change', handleSystemThemeChange);
}
mediaQuery.addEventListener("change", handleSystemThemeChange);
}

View File

@@ -4,6 +4,7 @@ import { RouterProvider } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { router } from "./router";
import { AuthProvider } from "./contexts/AuthProvider";
import { Toaster } from "./components/ui/Toaster";
import { setupDarkMode } from "./lib/theme";
import "./index.css";
@@ -23,6 +24,7 @@ const queryClient = new QueryClient({
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<Toaster />
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>