Merge pull request #301 from gitmotion/enhancement/implement-toaster
Implement Toaster - Issue #300, #227
This commit is contained in:
@@ -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({
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"],
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
@@ -206,9 +196,7 @@ export function WatchTab() {
|
||||
{/* 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>
|
||||
@@ -218,9 +206,7 @@ export function WatchTab() {
|
||||
{/* 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>
|
||||
@@ -235,7 +221,9 @@ export function WatchTab() {
|
||||
)}
|
||||
|
||||
<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 (1–50).</p>
|
||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
|
||||
Batch size per watch cycle (1–50).
|
||||
</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
|
||||
|
||||
47
spotizerr-ui/src/components/ui/Toaster.tsx
Normal file
47
spotizerr-ui/src/components/ui/Toaster.tsx
Normal 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;
|
||||
@@ -1,26 +1,27 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -28,44 +29,52 @@ export function toggleTheme() {
|
||||
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');
|
||||
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");
|
||||
}
|
||||
} else if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user