diff --git a/client/app/components/LastPlays.tsx b/client/app/components/LastPlays.tsx index c1e1add..9463245 100644 --- a/client/app/components/LastPlays.tsx +++ b/client/app/components/LastPlays.tsx @@ -16,7 +16,6 @@ interface Props { export default function LastPlays(props: Props) { const { user } = useAppContext() - console.log(user) const { isPending, isError, data, error } = useQuery({ queryKey: ['last-listens', { limit: props.limit, diff --git a/client/app/components/themeSwitcher/ThemeSwitcher.tsx b/client/app/components/themeSwitcher/ThemeSwitcher.tsx index e051f50..fac24d4 100644 --- a/client/app/components/themeSwitcher/ThemeSwitcher.tsx +++ b/client/app/components/themeSwitcher/ThemeSwitcher.tsx @@ -1,25 +1,15 @@ // ThemeSwitcher.tsx import { useEffect } from 'react'; import { useTheme } from '../../hooks/useTheme'; -import { themes } from '~/providers/ThemeProvider'; +import themes from '~/styles/themes.css'; import ThemeOption from './ThemeOption'; export function ThemeSwitcher() { const { theme, setTheme } = useTheme(); - - useEffect(() => { - const saved = localStorage.getItem('theme'); - if (saved && saved !== theme) { - setTheme(saved); - } else if (!saved) { - localStorage.setItem('theme', theme) - } - }, []); - useEffect(() => { if (theme) { - localStorage.setItem('theme', theme) + setTheme(theme) } }, [theme]); diff --git a/client/app/providers/ThemeProvider.tsx b/client/app/providers/ThemeProvider.tsx index cbdbf72..3aea9a5 100644 --- a/client/app/providers/ThemeProvider.tsx +++ b/client/app/providers/ThemeProvider.tsx @@ -1,259 +1,80 @@ -import { createContext, useEffect, useState, type ReactNode } from 'react'; +import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react'; +import { themes, type Theme } from '~/styles/themes.css'; +import { themeVars } from '~/styles/vars.css'; -// a fair number of colors aren't actually used, but i'm keeping -// them so that I don't have to worry about colors when adding new ui elements -export type Theme = { - name: string, - bg: string - bgSecondary: string - bgTertiary: string - fg: string - fgSecondary: string - fgTertiary: string - primary: string - primaryDim: string - accent: string - accentDim: string - error: string - warning: string - info: string - success: string +interface ThemeContextValue { + theme: string; + setTheme: (theme: string) => void; + setCustomTheme: (theme: Theme) => void; } -export const themes: Theme[] = [ - { - name: "yuu", - bg: "#161312", - bgSecondary: "#272120", - bgTertiary: "#382F2E", - fg: "#faf5f4", - fgSecondary: "#CCC7C6", - fgTertiary: "#B0A3A1", - primary: "#ff826d", - primaryDim: "#CE6654", - accent: "#464DAE", - accentDim: "#393D74", - error: "#FF6247", - warning: "#FFC107", - success: "#3ECE5F", - info: "#41C4D8", - }, - { - name: "varia", - bg: "rgb(25, 25, 29)", - bgSecondary: "#222222", - bgTertiary: "#333333", - fg: "#eeeeee", - fgSecondary: "#aaaaaa", - fgTertiary: "#888888", - primary: "rgb(203, 110, 240)", - primaryDim: "#c28379", - accent: "#f0ad0a", - accentDim: "#d08d08", - error: "#f44336", - warning: "#ff9800", - success: "#4caf50", - info: "#2196f3", - }, - { - name: "midnight", - bg: "rgb(8, 15, 24)", - bgSecondary: "rgb(15, 27, 46)", - bgTertiary: "rgb(15, 41, 70)", - fg: "#dbdfe7", - fgSecondary: "#9ea3a8", - fgTertiary: "#74787c", - primary: "#1a97eb", - primaryDim: "#2680aa", - accent: "#f0ad0a", - accentDim: "#d08d08", - error: "#f44336", - warning: "#ff9800", - success: "#4caf50", - info: "#2196f3", - }, - { - name: "catppuccin", - bg: "#1e1e2e", - bgSecondary: "#181825", - bgTertiary: "#11111b", - fg: "#cdd6f4", - fgSecondary: "#a6adc8", - fgTertiary: "#9399b2", - primary: "#89b4fa", - primaryDim: "#739df0", - accent: "#f38ba8", - accentDim: "#d67b94", - error: "#f38ba8", - warning: "#f9e2af", - success: "#a6e3a1", - info: "#89dceb", - }, - { - name: "autumn", - bg: "rgb(44, 25, 18)", - bgSecondary: "rgb(70, 40, 18)", - bgTertiary: "#4b2f1c", - fg: "#fef9f3", - fgSecondary: "#dbc6b0", - fgTertiary: "#a3917a", - primary: "#d97706", - primaryDim: "#b45309", - accent: "#8c4c28", - accentDim: "#6b3b1f", - error: "#d1433f", - warning: "#e38b29", - success: "#6b8e23", - info: "#c084fc", - }, - { - name: "black", - bg: "#000000", - bgSecondary: "#1a1a1a", - bgTertiary: "#2a2a2a", - fg: "#dddddd", - fgSecondary: "#aaaaaa", - fgTertiary: "#888888", - primary: "#08c08c", - primaryDim: "#08c08c", - accent: "#f0ad0a", - accentDim: "#d08d08", - error: "#f44336", - warning: "#ff9800", - success: "#4caf50", - info: "#2196f3", - }, - { - name: "wine", - bg: "#23181E", - bgSecondary: "#2C1C25", - bgTertiary: "#422A37", - fg: "#FCE0B3", - fgSecondary: "#C7AC81", - fgTertiary: "#A78E64", - primary: "#EA8A64", - primaryDim: "#BD7255", - accent: "#FAE99B", - accentDim: "#C6B464", - error: "#fca5a5", - warning: "#fde68a", - success: "#bbf7d0", - info: "#bae6fd", - }, - { - name: "pearl", - bg: "#FFFFFF", - bgSecondary: "#EEEEEE", - bgTertiary: "#E0E0E0", - fg: "#333333", - fgSecondary: "#555555", - fgTertiary: "#777777", - primary: "#007BFF", - primaryDim: "#0056B3", - accent: "#28A745", - accentDim: "#1E7E34", - error: "#DC3545", - warning: "#FFC107", - success: "#28A745", - info: "#17A2B8", - }, - { - name: "asuka", - bg: "#3B1212", - bgSecondary: "#471B1B", - bgTertiary: "#020202", - fg: "#F1E9E6", - fgSecondary: "#CCB6AE", - fgTertiary: "#9F8176", - primary: "#F1E9E6", - primaryDim: "#CCB6AE", - accent: "#41CE41", - accentDim: "#3BA03B", - error: "#DC143C", - warning: "#FFD700", - success: "#32CD32", - info: "#1E90FF", - }, - { - name: "urim", - bg: "#101713", - bgSecondary: "#1B2921", - bgTertiary: "#273B30", - fg: "#D2E79E", - fgSecondary: "#B4DA55", - fgTertiary: "#7E9F2A", - primary: "#ead500", - primaryDim: "#C1B210", - accent: "#28A745", - accentDim: "#1E7E34", - error: "#EE5237", - warning: "#FFC107", - success: "#28A745", - info: "#17A2B8", - }, - { - name: "match", - bg: "#071014", - bgSecondary: "#0A181E", - bgTertiary: "#112A34", - fg: "#ebeaeb", - fgSecondary: "#BDBDBD", - fgTertiary: "#A2A2A2", - primary: "#fda827", - primaryDim: "#C78420", - accent: "#277CFD", - accentDim: "#1F60C1", - error: "#F14426", - warning: "#FFC107", - success: "#28A745", - info: "#17A2B8", - }, - { - name: "lemon", - bg: "#1a171a", - bgSecondary: "#2E272E", - bgTertiary: "#443844", - fg: "#E6E2DC", - fgSecondary: "#B2ACA1", - fgTertiary: "#968F82", - primary: "#f5c737", - primaryDim: "#C29D2F", - accent: "#277CFD", - accentDim: "#1F60C1", - error: "#F14426", - warning: "#FFC107", - success: "#28A745", - info: "#17A2B8", - }, -]; +const ThemeContext = createContext(undefined); -interface ThemeContextValue { - theme: string; - setTheme: (theme: string) => void; +function toKebabCase(str: string) { + return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase()); } -const ThemeContext = createContext(undefined); +function applyCustomThemeVars(theme: Theme) { + const root = document.documentElement; + for (const [key, value] of Object.entries(theme)) { + if (key === 'name') continue; + root.style.setProperty(`--color-${toKebabCase(key)}`, value); + } +} + +function clearCustomThemeVars() { + for (const cssVar of Object.values(themeVars)) { + document.documentElement.style.removeProperty(cssVar); + } +} export function ThemeProvider({ - theme: initialTheme, - children, + theme: initialTheme, + children, }: { - theme: string; - children: ReactNode; + theme: string; + children: ReactNode; }) { - const [theme, setTheme] = useState(initialTheme); + const [theme, setThemeName] = useState(initialTheme); - useEffect(() => { - if (theme) { - document.documentElement.setAttribute('data-theme', theme); + const setTheme = (theme: string) => { + setThemeName(theme) } - }, [theme]); - return ( - - {children} - - ); + const setCustomTheme = useCallback((customTheme: Theme) => { + localStorage.setItem('custom-theme', JSON.stringify(customTheme)); + applyCustomThemeVars(customTheme); + setTheme('custom'); + }, []); + + useEffect(() => { + const root = document.documentElement; + + root.setAttribute('data-theme', theme); + localStorage.setItem('theme', theme) + console.log(theme) + + if (theme === 'custom') { + const saved = localStorage.getItem('custom-theme'); + if (saved) { + try { + const parsed = JSON.parse(saved) as Theme; + applyCustomThemeVars(parsed); + } catch (err) { + console.error('Invalid custom theme in localStorage', err); + } + } + } else { + clearCustomThemeVars() + } + }, [theme]); + + + return ( + + {children} + + ); } -export { ThemeContext } \ No newline at end of file +export { ThemeContext }; diff --git a/client/app/routes/ThemeHelper.tsx b/client/app/routes/ThemeHelper.tsx index 7c65c6a..6452a08 100644 --- a/client/app/routes/ThemeHelper.tsx +++ b/client/app/routes/ThemeHelper.tsx @@ -7,8 +7,44 @@ import LastPlays from "~/components/LastPlays" import TopAlbums from "~/components/TopAlbums" import TopArtists from "~/components/TopArtists" import TopTracks from "~/components/TopTracks" +import { useTheme } from "~/hooks/useTheme" +import { themes, type Theme } from "~/styles/themes.css" export default function ThemeHelper() { + const initialTheme = { + name: "custom", + bg: "#1e1816", + bgSecondary: "#2f2623", + bgTertiary: "#453733", + fg: "#f8f3ec", + fgSecondary: "#d6ccc2", + fgTertiary: "#b4a89c", + primary: "#f5a97f", + primaryDim: "#d88b65", + accent: "#f9db6d", + accentDim: "#d9bc55", + error: "#e26c6a", + warning: "#f5b851", + success: "#8fc48f", + info: "#87b8dd", + } + + const [custom, setCustom] = useState(JSON.stringify(initialTheme, null, " ")) + const { setCustomTheme } = useTheme() + + const handleCustomTheme = () => { + console.log(custom) + try { + const theme = JSON.parse(custom) as Theme + if (theme.name !== "custom") { + throw new Error("theme name must be 'custom'") + } + console.log(theme) + setCustomTheme(theme) + } catch(err) { + console.log(err) + } + } const homeItems = 3 @@ -24,43 +60,49 @@ export default function ThemeHelper() { -
-
-

You're logged in as Example User

- {}}>Logout -
-
- - {}}>Submit -
-
- - - {}}>Submit +
+
+