feat: nový způsob zobrazování novinek
Some checks are pending
ci/woodpecker/push/workflow Pipeline is pending
Some checks are pending
ci/woodpecker/push/workflow Pipeline is pending
fix: oprava kopírování changelogů do Docker image fix: oprava kopírování changelogů do Docker image fix: oprava
This commit is contained in:
@@ -82,8 +82,11 @@ COPY --from=builder /build/client/dist ./public
|
|||||||
# Zkopírování produkčních .env serveru
|
# Zkopírování produkčních .env serveru
|
||||||
COPY /server/.env.production ./server
|
COPY /server/.env.production ./server
|
||||||
|
|
||||||
# Zkopírování konfigurace easter eggů
|
# Zkopírování changelogů (seznamu novinek)
|
||||||
RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi
|
COPY /server/changelogs ./server/changelogs
|
||||||
|
|
||||||
|
# Zkopírování konfigurace easter eggů a changelogů
|
||||||
|
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi
|
||||||
|
|
||||||
# Export /data/db.json do složky /data
|
# Export /data/db.json do složky /data
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ COPY ./server/dist ./
|
|||||||
# Vykopírování sestaveného klienta
|
# Vykopírování sestaveného klienta
|
||||||
COPY ./client/dist ./public
|
COPY ./client/dist ./public
|
||||||
|
|
||||||
# Zkopírování konfigurace easter eggů
|
# Zkopírování changelogů (seznamu novinek)
|
||||||
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi
|
COPY ./server/changelogs ./server/changelogs
|
||||||
|
|
||||||
|
# Zkopírování konfigurace easter eggů a changelogů
|
||||||
|
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi \
|
||||||
|
&& if [ -d ./server/changelogs ]; then cp -r ./server/changelogs ./server/changelogs; fi
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ import GenerateMockDataModal from "./modals/GenerateMockDataModal";
|
|||||||
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { STATS_URL } from "../AppRoutes";
|
import { STATS_URL } from "../AppRoutes";
|
||||||
import { FeatureRequest, getVotes, updateVote, LunchChoices } 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 } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { formatDateString } from "../Utils";
|
||||||
|
|
||||||
const CHANGELOG = [
|
const LAST_SEEN_CHANGELOG_KEY = "lastChangelogDate";
|
||||||
"Nový moderní design aplikace",
|
|
||||||
"Oprava parsování Sladovnické a TechTower",
|
|
||||||
"Možnost označit se jako objednávající u volby \"budu objednávat\"",
|
|
||||||
"Možnost generovat QR kódy pro platby (i mimo Pizza day)",
|
|
||||||
];
|
|
||||||
|
|
||||||
const IS_DEV = process.env.NODE_ENV === 'development';
|
const IS_DEV = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@@ -38,6 +34,7 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
||||||
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
|
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
|
||||||
const [changelogModalOpen, setChangelogModalOpen] = useState<boolean>(false);
|
const [changelogModalOpen, setChangelogModalOpen] = useState<boolean>(false);
|
||||||
|
const [changelogEntries, setChangelogEntries] = useState<Record<string, string[]>>({});
|
||||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false);
|
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false);
|
||||||
const [generateMockModalOpen, setGenerateMockModalOpen] = useState<boolean>(false);
|
const [generateMockModalOpen, setGenerateMockModalOpen] = useState<boolean>(false);
|
||||||
const [clearMockModalOpen, setClearMockModalOpen] = useState<boolean>(false);
|
const [clearMockModalOpen, setClearMockModalOpen] = useState<boolean>(false);
|
||||||
@@ -71,6 +68,19 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
}
|
}
|
||||||
}, [auth?.login]);
|
}, [auth?.login]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
const lastSeen = localStorage.getItem(LAST_SEEN_CHANGELOG_KEY) ?? undefined;
|
||||||
|
getChangelogs({ query: lastSeen ? { since: lastSeen } : {} }).then(response => {
|
||||||
|
const entries = response.data;
|
||||||
|
if (!entries || Object.keys(entries).length === 0) return;
|
||||||
|
setChangelogEntries(entries);
|
||||||
|
setChangelogModalOpen(true);
|
||||||
|
const newestDate = Object.keys(entries).sort((a, b) => b.localeCompare(a))[0];
|
||||||
|
localStorage.setItem(LAST_SEEN_CHANGELOG_KEY, newestDate);
|
||||||
|
});
|
||||||
|
}, [auth?.login]);
|
||||||
|
|
||||||
const closeSettingsModal = () => {
|
const closeSettingsModal = () => {
|
||||||
setSettingsModalOpen(false);
|
setSettingsModalOpen(false);
|
||||||
}
|
}
|
||||||
@@ -197,7 +207,17 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={() => setChangelogModalOpen(true)}>Novinky</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => {
|
||||||
|
getChangelogs().then(response => {
|
||||||
|
const entries = response.data ?? {};
|
||||||
|
setChangelogEntries(entries);
|
||||||
|
setChangelogModalOpen(true);
|
||||||
|
const dates = Object.keys(entries).sort((a, b) => b.localeCompare(a));
|
||||||
|
if (dates.length > 0) {
|
||||||
|
localStorage.setItem(LAST_SEEN_CHANGELOG_KEY, dates[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}>Novinky</NavDropdown.Item>
|
||||||
{IS_DEV && (
|
{IS_DEV && (
|
||||||
<>
|
<>
|
||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
@@ -237,16 +257,24 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Modal show={changelogModalOpen} onHide={() => setChangelogModalOpen(false)}>
|
<Modal show={changelogModalOpen} onHide={() => setChangelogModalOpen(false)} size="lg">
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title><h2>Novinky</h2></Modal.Title>
|
<Modal.Title><h2>Novinky</h2></Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<ul>
|
{Object.keys(changelogEntries).sort((a, b) => b.localeCompare(a)).map(date => (
|
||||||
{CHANGELOG.map((item, index) => (
|
<div key={date}>
|
||||||
<li key={index}>{item}</li>
|
<strong>{formatDateString(date)}</strong>
|
||||||
))}
|
<ul>
|
||||||
</ul>
|
{changelogEntries[date].map((item, index) => (
|
||||||
|
<li key={index}>{item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{Object.keys(changelogEntries).length === 0 && (
|
||||||
|
<p>Žádné novinky.</p>
|
||||||
|
)}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="secondary" onClick={() => setChangelogModalOpen(false)}>
|
<Button variant="secondary" onClick={() => setChangelogModalOpen(false)}>
|
||||||
|
|||||||
4
server/changelogs/2025-01-07.json
Normal file
4
server/changelogs/2025-01-07.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
"Zimní atmosféra",
|
||||||
|
"Skrytí podniku U Motlíků"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-01-15.json
Normal file
3
server/changelogs/2025-01-15.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Přidání restaurace Zastávka u Michala"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-01-29.json
Normal file
3
server/changelogs/2025-01-29.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Přidání restaurace Pivovarský šenk Šeříková"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-02-19.json
Normal file
3
server/changelogs/2025-02-19.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost výběru podniku/jídla kliknutím"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-02-27.json
Normal file
3
server/changelogs/2025-02-27.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Stránka se statistikami nejoblíbenějších voleb"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-03-05.json
Normal file
3
server/changelogs/2025-03-05.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Zobrazení počtu osob u každé volby"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-03-20.json
Normal file
3
server/changelogs/2025-03-20.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Migrace na generované OpenApi"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-03-21.json
Normal file
3
server/changelogs/2025-03-21.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Odebrání zimní atmosféry"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-08-01.json
Normal file
3
server/changelogs/2025-08-01.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost ručního přenačtení menu"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-10-06.json
Normal file
3
server/changelogs/2025-10-06.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Parsování a zobrazení alergenů"
|
||||||
|
]
|
||||||
4
server/changelogs/2025-10-11.json
Normal file
4
server/changelogs/2025-10-11.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
"Oddělení přenačtení menu do vlastního dialogu",
|
||||||
|
"Podzimní atmosféra"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-11-03.json
Normal file
3
server/changelogs/2025-11-03.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost převzetí poznámky ostatních uživatelů"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-01-09.json
Normal file
3
server/changelogs/2026-01-09.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Zimní atmosféra"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-01-13.json
Normal file
3
server/changelogs/2026-01-13.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost označit se jako objednávající u volby \"Budu objednávat\""
|
||||||
|
]
|
||||||
3
server/changelogs/2026-01-30.json
Normal file
3
server/changelogs/2026-01-30.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Podpora dark mode"
|
||||||
|
]
|
||||||
7
server/changelogs/2026-02-04.json
Normal file
7
server/changelogs/2026-02-04.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
"Redesign aplikace pomocí Claude Code",
|
||||||
|
"Zobrazení uplynulého týdne i o víkendu",
|
||||||
|
"Podpora Discord, ntfy a Teams notifikací (v Nastavení)",
|
||||||
|
"Trvalé zobrazení QR kódů do ručního zavření",
|
||||||
|
"Zobrazení nejvíce požadovaných funkcí (na stránce Statistiky)"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-02-10.json
Normal file
3
server/changelogs/2026-02-10.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Zobrazení sekce Pizza day pouze při volbě \"Pizza day\""
|
||||||
|
]
|
||||||
3
server/changelogs/2026-02-20.json
Normal file
3
server/changelogs/2026-02-20.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost generování obecných QR kódů pro platby i mimo Pizza day (v uživatelském menu)"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-03-04.json
Normal file
3
server/changelogs/2026-03-04.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Podpora push notifikací pro připomenutí výběru oběda (v nastavení)"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-03-05.json
Normal file
3
server/changelogs/2026-03-05.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Oprava detekce zastaralého menu"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-03-08.json
Normal file
3
server/changelogs/2026-03-08.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Automatické zobrazení dialogu s dosud nezobrazenými novinkami"
|
||||||
|
]
|
||||||
@@ -18,6 +18,7 @@ import statsRoutes from "./routes/statsRoutes";
|
|||||||
import notificationRoutes from "./routes/notificationRoutes";
|
import notificationRoutes from "./routes/notificationRoutes";
|
||||||
import qrRoutes from "./routes/qrRoutes";
|
import qrRoutes from "./routes/qrRoutes";
|
||||||
import devRoutes from "./routes/devRoutes";
|
import devRoutes from "./routes/devRoutes";
|
||||||
|
import changelogRoutes from "./routes/changelogRoutes";
|
||||||
|
|
||||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||||
@@ -165,6 +166,7 @@ app.use("/api/stats", statsRoutes);
|
|||||||
app.use("/api/notifications", notificationRoutes);
|
app.use("/api/notifications", notificationRoutes);
|
||||||
app.use("/api/qr", qrRoutes);
|
app.use("/api/qr", qrRoutes);
|
||||||
app.use("/api/dev", devRoutes);
|
app.use("/api/dev", devRoutes);
|
||||||
|
app.use("/api/changelogs", changelogRoutes);
|
||||||
|
|
||||||
app.use('/stats', express.static('public'));
|
app.use('/stats', express.static('public'));
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
|
|||||||
50
server/src/routes/changelogRoutes.ts
Normal file
50
server/src/routes/changelogRoutes.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const CHANGELOGS_DIR = path.resolve(__dirname, "../../changelogs");
|
||||||
|
|
||||||
|
// In-memory cache: datum → seznam změn
|
||||||
|
const cache: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
function loadAllChangelogs(): Record<string, string[]> {
|
||||||
|
let files: string[];
|
||||||
|
try {
|
||||||
|
files = fs.readdirSync(CHANGELOGS_DIR).filter(f => f.endsWith(".json"));
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const date = file.replace(".json", "");
|
||||||
|
if (!cache[date]) {
|
||||||
|
const content = fs.readFileSync(path.join(CHANGELOGS_DIR, file), "utf-8");
|
||||||
|
cache[date] = JSON.parse(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/", (req: Request, res: Response) => {
|
||||||
|
const all = loadAllChangelogs();
|
||||||
|
const since = typeof req.query.since === "string" ? req.query.since : undefined;
|
||||||
|
|
||||||
|
// Seřazení od nejnovějšího po nejstarší
|
||||||
|
const sortedDates = Object.keys(all).sort((a, b) => b.localeCompare(a));
|
||||||
|
|
||||||
|
const filteredDates = since
|
||||||
|
? sortedDates.filter(date => date > since)
|
||||||
|
: sortedDates;
|
||||||
|
|
||||||
|
const result: Record<string, string[]> = {};
|
||||||
|
for (const date of filteredDates) {
|
||||||
|
result[date] = all[date];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -77,6 +77,10 @@ paths:
|
|||||||
/voting/stats:
|
/voting/stats:
|
||||||
$ref: "./paths/voting/getVotingStats.yml"
|
$ref: "./paths/voting/getVotingStats.yml"
|
||||||
|
|
||||||
|
# Changelog (/api/changelogs)
|
||||||
|
/changelogs:
|
||||||
|
$ref: "./paths/changelogs/getChangelogs.yml"
|
||||||
|
|
||||||
# DEV endpointy (/api/dev)
|
# DEV endpointy (/api/dev)
|
||||||
/dev/generate:
|
/dev/generate:
|
||||||
$ref: "./paths/dev/generate.yml"
|
$ref: "./paths/dev/generate.yml"
|
||||||
|
|||||||
21
types/paths/changelogs/getChangelogs.yml
Normal file
21
types/paths/changelogs/getChangelogs.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
get:
|
||||||
|
operationId: getChangelogs
|
||||||
|
summary: Vrátí seznam změn (changelog). Pokud není předáno datum, vrátí všechny změny. Pokud je předáno datum, vrátí pouze změny po tomto datu.
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: since
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Datum (formát YYYY-MM-DD) od kterého se mají vrátit změny (exkluzivně). Pokud není předáno, vrátí se všechny změny.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Slovník kde klíčem je datum (YYYY-MM-DD) a hodnotou seznam změn k danému datu. Seřazeno od nejnovějšího po nejstarší.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
Reference in New Issue
Block a user