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
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:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user