feat: podpora themes
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 33s
CI / Playwright E2E tests (push) Successful in 1m16s
CI / Build and push Docker image (push) Successful in 42s
CI / Notify (push) Successful in 1s

This commit is contained in:
2026-05-14 21:36:56 +02:00
parent a166634db8
commit 640c7ed41d
3 changed files with 104 additions and 3 deletions
+63
View File
@@ -87,6 +87,42 @@
--luncher-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4); --luncher-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4);
} }
// Modrý motiv světlý
[data-luncher-color="blue"][data-bs-theme="light"] {
--luncher-primary: #2563eb;
--luncher-primary-hover: #1d4ed8;
--luncher-primary-light: #dbeafe;
--luncher-action-icon: #2563eb;
--luncher-success: #2563eb;
}
// Modrý motiv tmavý
[data-luncher-color="blue"][data-bs-theme="dark"] {
--luncher-primary: #3b82f6;
--luncher-primary-hover: #60a5fa;
--luncher-primary-light: #172554;
--luncher-action-icon: #3b82f6;
--luncher-success: #3b82f6;
}
// Fialový motiv světlý
[data-luncher-color="purple"][data-bs-theme="light"] {
--luncher-primary: #7c3aed;
--luncher-primary-hover: #6d28d9;
--luncher-primary-light: #ede9fe;
--luncher-action-icon: #7c3aed;
--luncher-success: #7c3aed;
}
// Fialový motiv tmavý
[data-luncher-color="purple"][data-bs-theme="dark"] {
--luncher-primary: #a78bfa;
--luncher-primary-hover: #c4b5fd;
--luncher-primary-light: #2e1065;
--luncher-action-icon: #a78bfa;
--luncher-success: #a78bfa;
}
// ============================================ // ============================================
// BASE STYLES // BASE STYLES
// ============================================ // ============================================
@@ -226,6 +262,33 @@ body {
&:hover svg { &:hover svg {
transform: rotate(15deg); transform: rotate(15deg);
} }
// Přizpůsobení pro NavDropdown (palette ikona)
&.nav-item.dropdown {
.dropdown-toggle {
background: transparent;
border: none;
color: var(--luncher-navbar-text);
padding: 8px 12px;
font-size: 1.1rem;
display: flex;
align-items: center;
&::after {
display: none;
}
&:hover {
background: rgba(255, 255, 255, 0.1);
transform: scale(1.1);
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3);
}
}
}
} }
// ============================================ // ============================================
+13 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { Navbar, Nav, NavDropdown, Modal, Button } from "react-bootstrap"; import { Navbar, Nav, NavDropdown, Modal, Button } from "react-bootstrap";
import { useAuth } from "../context/auth"; import { useAuth } from "../context/auth";
import SettingsModal from "./modals/SettingsModal"; import SettingsModal from "./modals/SettingsModal";
import { useSettings, ThemePreference } from "../context/settings"; import { useSettings, ThemePreference, ColorTheme } from "../context/settings";
import FeaturesVotingModal from "./modals/FeaturesVotingModal"; import FeaturesVotingModal from "./modals/FeaturesVotingModal";
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal"; import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
import RefreshMenuModal from "./modals/RefreshMenuModal"; import RefreshMenuModal from "./modals/RefreshMenuModal";
@@ -13,7 +13,7 @@ import { useNavigate } from "react-router";
import { STATS_URL, OBJEDNANI_URL } from "../AppRoutes"; import { STATS_URL, OBJEDNANI_URL } from "../AppRoutes";
import { FeatureRequest, getVotes, updateVote, LunchChoices, getChangelogs } from "../../../types"; import { FeatureRequest, getVotes, updateVote, LunchChoices, getChangelogs } from "../../../types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons"; import { faSun, faMoon, faPalette } from "@fortawesome/free-solid-svg-icons";
import { formatDateString } from "../Utils"; import { formatDateString } from "../Utils";
const LAST_SEEN_CHANGELOG_KEY = "lastChangelogDate"; const LAST_SEEN_CHANGELOG_KEY = "lastChangelogDate";
@@ -196,10 +196,20 @@ export default function Header({ choices, dayIndex }: Props) {
className="theme-toggle" className="theme-toggle"
onClick={toggleTheme} onClick={toggleTheme}
title={effectiveTheme === 'dark' ? 'Přepnout na světlý režim' : 'Přepnout na tmavý režim'} title={effectiveTheme === 'dark' ? 'Přepnout na světlý režim' : 'Přepnout na tmavý režim'}
aria-label="Přepnout barevný motiv" aria-label="Přepnout světlý/tmavý režim"
> >
<FontAwesomeIcon icon={effectiveTheme === 'dark' ? faSun : faMoon} /> <FontAwesomeIcon icon={effectiveTheme === 'dark' ? faSun : faMoon} />
</button> </button>
<NavDropdown
align="end"
title={<FontAwesomeIcon icon={faPalette} />}
id="color-theme-dropdown"
className="theme-toggle"
>
<NavDropdown.Item active={settings?.colorTheme === 'green'} onClick={() => settings?.setColorTheme('green' as ColorTheme)}>🟢 Zelený</NavDropdown.Item>
<NavDropdown.Item active={settings?.colorTheme === 'blue'} onClick={() => settings?.setColorTheme('blue' as ColorTheme)}>🔵 Modrý</NavDropdown.Item>
<NavDropdown.Item active={settings?.colorTheme === 'purple'} onClick={() => settings?.setColorTheme('purple' as ColorTheme)}>🟣 Fialový</NavDropdown.Item>
</NavDropdown>
<NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown"> <NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown">
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item> <NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item>
<NavDropdown.Item onClick={() => setRefreshMenuModalOpen(true)}>Přenačtení menu</NavDropdown.Item> <NavDropdown.Item onClick={() => setRefreshMenuModalOpen(true)}>Přenačtení menu</NavDropdown.Item>
+28
View File
@@ -4,18 +4,22 @@ const BANK_ACCOUNT_NUMBER_KEY = 'bank_account_number';
const BANK_ACCOUNT_HOLDER_KEY = 'bank_account_holder_name'; const BANK_ACCOUNT_HOLDER_KEY = 'bank_account_holder_name';
const HIDE_SOUPS_KEY = 'hide_soups'; const HIDE_SOUPS_KEY = 'hide_soups';
const THEME_KEY = 'theme_preference'; const THEME_KEY = 'theme_preference';
const COLOR_THEME_KEY = 'color_theme';
export type ThemePreference = 'system' | 'light' | 'dark'; export type ThemePreference = 'system' | 'light' | 'dark';
export type ColorTheme = 'green' | 'blue' | 'purple';
export type SettingsContextProps = { export type SettingsContextProps = {
bankAccount?: string, bankAccount?: string,
holderName?: string, holderName?: string,
hideSoups?: boolean, hideSoups?: boolean,
themePreference: ThemePreference, themePreference: ThemePreference,
colorTheme: ColorTheme,
setBankAccountNumber: (accountNumber?: string) => void, setBankAccountNumber: (accountNumber?: string) => void,
setBankAccountHolderName: (holderName?: string) => void, setBankAccountHolderName: (holderName?: string) => void,
setHideSoupsOption: (hideSoups?: boolean) => void, setHideSoupsOption: (hideSoups?: boolean) => void,
setThemePreference: (theme: ThemePreference) => void, setThemePreference: (theme: ThemePreference) => void,
setColorTheme: (color: ColorTheme) => void,
} }
type ContextProps = { type ContextProps = {
@@ -45,11 +49,24 @@ function getInitialTheme(): ThemePreference {
return 'system'; return 'system';
} }
function getInitialColorTheme(): ColorTheme {
try {
const saved = localStorage.getItem(COLOR_THEME_KEY) as ColorTheme | null;
if (saved && ['green', 'blue', 'purple'].includes(saved)) {
return saved;
}
} catch (e) {
// localStorage nedostupný
}
return 'green';
}
function useProvideSettings(): SettingsContextProps { function useProvideSettings(): SettingsContextProps {
const [bankAccount, setBankAccount] = useState<string | undefined>(); const [bankAccount, setBankAccount] = useState<string | undefined>();
const [holderName, setHolderName] = useState<string | undefined>(); const [holderName, setHolderName] = useState<string | undefined>();
const [hideSoups, setHideSoups] = useState<boolean | undefined>(); const [hideSoups, setHideSoups] = useState<boolean | undefined>();
const [themePreference, setTheme] = useState<ThemePreference>(getInitialTheme); const [themePreference, setTheme] = useState<ThemePreference>(getInitialTheme);
const [colorTheme, setColor] = useState<ColorTheme>(getInitialColorTheme);
useEffect(() => { useEffect(() => {
const accountNumber = localStorage.getItem(BANK_ACCOUNT_NUMBER_KEY); const accountNumber = localStorage.getItem(BANK_ACCOUNT_NUMBER_KEY);
@@ -94,6 +111,11 @@ function useProvideSettings(): SettingsContextProps {
localStorage.setItem(THEME_KEY, themePreference); localStorage.setItem(THEME_KEY, themePreference);
}, [themePreference]); }, [themePreference]);
useEffect(() => {
localStorage.setItem(COLOR_THEME_KEY, colorTheme);
document.documentElement.setAttribute('data-luncher-color', colorTheme);
}, [colorTheme]);
useEffect(() => { useEffect(() => {
const applyTheme = (theme: 'light' | 'dark') => { const applyTheme = (theme: 'light' | 'dark') => {
document.documentElement.setAttribute('data-bs-theme', theme); document.documentElement.setAttribute('data-bs-theme', theme);
@@ -129,14 +151,20 @@ function useProvideSettings(): SettingsContextProps {
setTheme(theme); setTheme(theme);
} }
function setColorTheme(color: ColorTheme) {
setColor(color);
}
return { return {
bankAccount, bankAccount,
holderName, holderName,
hideSoups, hideSoups,
themePreference, themePreference,
colorTheme,
setBankAccountNumber, setBankAccountNumber,
setBankAccountHolderName, setBankAccountHolderName,
setHideSoupsOption, setHideSoupsOption,
setThemePreference, setThemePreference,
setColorTheme,
} }
} }