diff --git a/CLAUDE.md b/CLAUDE.md
index de667d4..a7089f2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -82,7 +82,7 @@ cd client && yarn build # tsc --noEmit + vite build → client/dist
`IntakeStatsPanel`, `ActivityStatsPanel`, `SpendingStatsPanel`)
- **Components:** `components/` (Header, CalorieLookup, MealSuggestions, NutrientsFieldset)
a `components/modals/` (MealModal, ActivityModal, ApplyWorkoutModal, ApplyMealModal —
- výběr z historie a receptů, SettingsModal, ImportModal)
+ výběr z historie a receptů, SettingsModal, ImportModal, SpinWheelModal — kolo štěstí)
- **Klientské enumy** patří do `enums.ts` jako TypeScript `enum` s mapou popisků,
ne jako inline union u komponenty (`DayTab`, `StatsTab`, `StatsRange`)
- **Context:** `context/auth.tsx` (JWT), `context/settings.tsx` (světlý/tmavý motiv)
@@ -343,6 +343,29 @@ ji při čtení doplní.
Recepty se nabízejí v našeptávání i ve výběru z historie (`ApplyMealModal`)
a jídlo stejného jména z historie se vedle receptu nenabízí podruhé.
+**Postup receptu je Markdown.** V aplikaci ho vykresluje `RecipeNote`
+(react-markdown, syrové HTML se nevykreslí), do PDF ho převádí `markdownPdf.ts`
+nad stejným parserem (mdast). Obě cesty používají `markdownBreaks.ts`, který
+z jednoduchého zalomení řádku dělá tvrdé — starší postupy jsou prostý text po
+řádcích a jinak by se slily do jednoho odstavce. **Když se změní zobrazení
+Markdownu v jedné cestě, je potřeba upravit i druhou.**
+
+**Export do PDF** (`recipePdf.ts`, pdfmake v prohlížeči, načte se líně až při
+exportu) jde u jednoho receptu (ikona na kartě receptu) i hromadně tlačítkem
+na stránce Jídla — exportují se recepty, které seznam po filtrech ukazuje, bez
+filtru všechny. Víc receptů dostane titulní stranu s klikacím obsahem, každý
+recept začíná na nové stránce. PDF nese suroviny, hodnoty porce, postup a fotky
+jídla (první přes šířku, ostatní menší); pdfmake umí jen JPEG a PNG, jiné
+formáty se vynechají. Server se exportu neúčastní.
+
+**Kolo štěstí** (`SpinWheelModal`, tlačítko v hlavičce) je převzaté z Luncheru.
+Losuje ze všech jídel (skupin ze seznamu jídel — i objednávaných), nebo jen
+z receptů, volitelně zúžených chutí a typem. U všech jídel se nejdřív vybere
+nebo vylosuje zdroj a točí se až z jeho jídel — všech jídel jsou stovky
+a kolo by s nimi bylo nepoužitelné. Padlý recept jde rovnou otevřít
+(`WheelRecipe`, jen ke čtení) a stáhnout jako PDF. Nic se neukládá — kolo jen
+rozhoduje. Konfety (`WheelConfetti`) mají vzhled jednoduchého stylu a Hell Girl.
+
Dřív pod stejným klíčem ležely i šablony hotových jídel (`kind: HOTOVE`, nebo
bez `kind`). Ty zanikly: `loadRecipes` je při čtení vynechá (poznají se podle
toho, že nemají suroviny) a první zápis receptu je z úložiště odklidí. **Jídla
diff --git a/client/package.json b/client/package.json
index af03ec3..3b48e52 100644
--- a/client/package.json
+++ b/client/package.json
@@ -10,15 +10,20 @@
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.0",
+ "@types/mdast": "4.0.4",
"@types/node": "^24.10.0",
+ "@types/pdfmake": "0.3.3",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"bootstrap": "^5.3.8",
+ "mdast-util-from-markdown": "2.0.3",
+ "pdfmake": "0.3.11",
"react": "^19.2.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0",
"react-jwt": "^1.3.0",
+ "react-markdown": "10.1.0",
"react-router": "^7.9.5",
"react-router-dom": "^7.9.5",
"react-toastify": "^11.0.5",
diff --git a/client/src/App.scss b/client/src/App.scss
index 5a60594..a1b43bf 100644
--- a/client/src/App.scss
+++ b/client/src/App.scss
@@ -268,6 +268,12 @@ body {
color: var(--ft-text-secondary);
font-size: 0.9rem;
}
+
+ .page-header-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
}
// ============================================
diff --git a/client/src/Utils.tsx b/client/src/Utils.tsx
index b02517b..569f17f 100644
--- a/client/src/Utils.tsx
+++ b/client/src/Utils.tsx
@@ -410,3 +410,9 @@ export const parseNutrientInput = (value: string): number | undefined => {
/** Převede gramy živiny na text pro editační pole. */
export const formatNutrientInput = (grams?: number): string =>
grams == null ? '' : String(grams).replace('.', ',');
+
+/** Počet se správným tvarem slova: 1 porce, 2–4 porce, 5 porcí. */
+export const formatCount = (count: number, one: string, few: string, many: string): string => {
+ if (count === 1) return `${count} ${one}`;
+ return `${count} ${count >= 2 && count <= 4 ? few : many}`;
+};
diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx
index 562ccb1..58da311 100644
--- a/client/src/components/Header.tsx
+++ b/client/src/components/Header.tsx
@@ -2,11 +2,12 @@ import { useState } from "react";
import { Navbar, Nav, NavDropdown } from "react-bootstrap";
import { Link, useLocation } from "react-router";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faSun, faMoon, faGear } from "@fortawesome/free-solid-svg-icons";
+import { faSun, faMoon, faGear, faDharmachakra } from "@fortawesome/free-solid-svg-icons";
import { useAuth } from "../context/auth";
import { useSettings, ThemePreference } from "../context/settings";
import { DEN_URL, JIDLA_URL, PROFIL_URL, STATISTIKY_URL, TRENINKY_URL } from "../routes";
import SettingsModal from "./modals/SettingsModal";
+import SpinWheelModal from "./modals/SpinWheelModal";
/** Položky hlavní navigace. */
const NAV_ITEMS = [
@@ -19,6 +20,7 @@ const NAV_ITEMS = [
export default function Header() {
const [settingsOpen, setSettingsOpen] = useState(false);
+ const [wheelOpen, setWheelOpen] = useState(false);
const auth = useAuth();
const settings = useSettings();
const location = useLocation();
@@ -49,6 +51,14 @@ export default function Header() {
{item.label}
))}
+