Compare commits

...

10 Commits

Author SHA1 Message Date
Michal Hájek
aa00542a03 Výběr obědu kliknutím
Výběr obědu kliknutím

Oprava možnosti zadat klikáním více podniků současně

Výběr obědu kliknutím
2025-02-18 10:05:19 +01:00
ff650ec3b8 rm db.json
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-02-17 09:32:23 +01:00
f8aa293413 fix
Some checks are pending
ci/woodpecker/push/workflow Pipeline is running
2025-02-17 09:26:03 +01:00
cafcd0a467 Log username a email pri kazdem dotazu pouze pro neproduction env
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-02-17 09:19:28 +01:00
9e247eb2a1 Podpora sestavování přes Woodpecker CI
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-02-09 00:34:59 +01:00
469a6b9031 Oprava .gitignore 2025-02-08 23:28:20 +01:00
Michal Hájek
89dec1c194 Založení složky server/data, pokud neexistuje, do které je vytvořen soubor db.json 2025-02-02 19:46:20 +01:00
Michal Hájek
f3af64923c Přesun json databaze (souboru db.json) do složky data, související úpravy v Dockerfile 2025-02-02 16:09:07 +01:00
Michal Hájek
44b09a9d1a Začištění souborů .gitignore 2025-02-02 16:06:52 +01:00
Michal Hájek
c311cc2fd7 Oprava importů klienta do složky types, aby nebylo potřeba složku kokírovat 2025-02-02 16:01:21 +01:00
21 changed files with 135 additions and 57 deletions

22
.gitignore vendored
View File

@ -1,23 +1 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

56
.woodpecker/workflow.yaml Normal file
View File

@ -0,0 +1,56 @@
variables:
- &node_image 'node:18-alpine3.18'
- &branch 'master'
when:
- event: push
branch: *branch
steps:
- name: Install server dependencies
image: *node_image
commands:
- cd server
- yarn install --frozen-lockfile
- name: Install client dependencies
image: *node_image
commands:
- cd client
- yarn install --frozen-lockfile
- name: Build server
depends_on: [Install server dependencies]
image: *node_image
commands:
- cd server
- yarn build
- name: Build client
depends_on: [Install client dependencies]
image: *node_image
commands:
- cd client
- yarn build
- name: Build Docker image
depends_on: [Build server, Build client]
image: woodpeckerci/plugin-docker-buildx
settings:
dockerfile: Dockerfile-Woodpecker
platforms: linux/amd64
registry:
from_secret: REPO_URL
username:
from_secret: REPO_USERNAME
password:
from_secret: REPO_PASSWORD
repo:
from_secret: REPO_NAME
- name: Discord notification - build
image: appleboy/drone-discord
depends_on: [Build Docker image]
when:
- status: [success, failure]
settings:
webhook_id:
from_secret: DISCORD_WEBHOOK_ID
webhook_token:
from_secret: DISCORD_WEBHOOK_TOKEN
message: "{{#success build.status}}✅ Sestavení {{build.number}} proběhlo úspěšně.{{else}}❌ Sestavení {{build.number}} selhalo.{{/success}}\n\nPipeline: {{build.link}}\nPoslední commit: {{commit.message}}Autor: {{commit.author}}"

View File

@ -54,7 +54,6 @@ WORKDIR /app
# Vykopírování sestaveného serveru # Vykopírování sestaveného serveru
COPY --from=builder /build/server/node_modules ./server/node_modules COPY --from=builder /build/server/node_modules ./server/node_modules
COPY --from=builder /build/server/dist ./ COPY --from=builder /build/server/dist ./
COPY server/resources ./server/resources
# Vykopírování sestaveného klienta # Vykopírování sestaveného klienta
COPY --from=builder /build/client/dist ./public COPY --from=builder /build/client/dist ./public
@ -63,8 +62,10 @@ COPY --from=builder /build/client/dist ./public
COPY /server/.env.production ./server/src COPY /server/.env.production ./server/src
# Zkopírování konfigurace easter eggů # Zkopírování konfigurace easter eggů
# TODO tohle spadne když nebude existovat! RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi
COPY /server/.easter-eggs.json ./server/
# Export /data/db.json do složky /data
VOLUME ["/data"]
EXPOSE 3000 EXPOSE 3000

21
Dockerfile-Woodpecker Normal file
View File

@ -0,0 +1,21 @@
FROM node:18-alpine3.18
ENV LANG=cs_CZ.UTF-8
ENV NODE_ENV=production
WORKDIR /app
# Vykopírování sestaveného serveru
COPY ./server/node_modules ./server/node_modules
COPY ./server/dist ./
# TODO tohle není dobře, má to být součástí serveru
# COPY ./server/resources ./resources
# Vykopírování sestaveného klienta
COPY ./client/dist ./public
# Zkopírování konfigurace easter eggů
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi
EXPOSE 3000
CMD [ "node", "./server/src/index.js" ]

1
client/.gitignore vendored
View File

@ -1,3 +1,2 @@
build build
dist dist
src/types

View File

@ -31,9 +31,8 @@
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"scripts": { "scripts": {
"copy-types": "cp -r ../types ./src", "start": "yarn vite",
"start": "yarn copy-types && vite", "build": "yarn vite build"
"build": "yarn copy-types && vite build"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@ -8,13 +8,12 @@ import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
import Header from './components/Header'; import Header from './components/Header';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import PizzaOrderList from './components/PizzaOrderList'; import PizzaOrderList from './components/PizzaOrderList';
import SelectSearch, { SelectedOptionValue } from 'react-select-search'; import SelectSearch, {SelectedOptionValue, SelectSearchOption} from 'react-select-search';
import 'react-select-search/style.css'; import 'react-select-search/style.css';
import './App.scss'; import './App.scss';
import { SelectSearchOption } from 'react-select-search';
import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { useSettings } from './context/settings'; import { useSettings } from './context/settings';
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime, LocationKey } from './types'; import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime, LocationKey } from '../../types';
import Footer from './components/Footer'; import Footer from './components/Footer';
import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
import Loader from './components/Loader'; import Loader from './components/Loader';
@ -190,6 +189,13 @@ function App() {
} }
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]); }, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
const doAddClickFoodChoice = async (location: Locations, foodIndex: number) => {
const locationKey = Object.keys(Locations).find(key => Locations[key as keyof typeof Locations] === location) as LocationKey;
if (auth?.login) {
await errorHandler(() => addChoice(locationKey, foodIndex, dayIndex));
}
}
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => { const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const locationKey = event.target.value as LocationKey; const locationKey = event.target.value as LocationKey;
if (auth?.login) { if (auth?.login) {
@ -332,15 +338,15 @@ function App() {
} }
} }
const renderFoodTable = (name: string, menu: DayMenu) => { const renderFoodTable = (name: string, location: Locations, menu: DayMenu) => {
let content; let content;
if (menu?.closed) { if (menu?.closed) {
content = <h3>Zavřeno</h3> content = <h3>Zavřeno</h3>
} else if (menu?.food?.length > 0) { } else if (menu?.food?.length > 0) {
content = <Table striped bordered hover> content = <Table striped bordered hover>
<tbody> <tbody style={{ cursor: 'pointer' }}>
{menu.food.filter(f => (settings?.hideSoups ? !f.isSoup : true)).map((f: any, index: number) => {menu.food.filter(f => (settings?.hideSoups ? !f.isSoup : true)).map((f: any, index: number) =>
<tr key={index}> <tr key={index} onClick={() => doAddClickFoodChoice(location, index)}>
<td>{f.amount}</td> <td>{f.amount}</td>
<td>{f.name}</td> <td>{f.name}</td>
<td>{f.price}</td> <td>{f.price}</td>
@ -402,8 +408,8 @@ function App() {
<img src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> <img src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
Poslední změny: Poslední změny:
<ul> <ul>
<li>Přidání restaurace Zastávka u Michala</li> <li>Přidání restaurací Zastávka u Michala a Pivovarský šenk Šeříková</li>
<li>Přidání restaurace Pivovarský šenk Šeříková</li> <li>Možnost výběru restaurace a jídel kliknutím v tabulce</li>
</ul> </ul>
</Alert> </Alert>
{dayIndex != null && {dayIndex != null &&
@ -414,11 +420,11 @@ function App() {
</div> </div>
} }
<Row className='food-tables'> <Row className='food-tables'>
{food[Restaurants.SLADOVNICKA] && renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])} {food[Restaurants.SLADOVNICKA] && renderFoodTable('Sladovnická', Locations.SLADOVNICKA, food[Restaurants.SLADOVNICKA])}
{/* {food[Restaurants.UMOTLIKU] && renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])} */} {/* {food[Restaurants.UMOTLIKU] && renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])} */}
{food[Restaurants.TECHTOWER] && renderFoodTable('TechTower', food[Restaurants.TECHTOWER])} {food[Restaurants.TECHTOWER] && renderFoodTable('TechTower', Locations.TECHTOWER, food[Restaurants.TECHTOWER])}
{food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable('Zastávka u Michala', food[Restaurants.ZASTAVKAUMICHALA])} {food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable('Zastávka u Michala', Locations.ZASTAVKAUMICHALA, food[Restaurants.ZASTAVKAUMICHALA])}
{food[Restaurants.SENKSERIKOVA] && renderFoodTable('Pivovarský šenk Šeříková', food[Restaurants.SENKSERIKOVA])} {food[Restaurants.SENKSERIKOVA] && renderFoodTable('Pivovarský šenk Šeříková', Locations.SENKSERIKOVA, food[Restaurants.SENKSERIKOVA])}
</Row> </Row>
<div className='content-wrapper'> <div className='content-wrapper'>
<div className='content'> <div className='content'>

View File

@ -1,4 +1,4 @@
import { EasterEgg } from "../types"; import { EasterEgg } from "../../../types";
import { api } from "./Api"; import { api } from "./Api";
const EASTER_EGGS_API_PREFIX = '/api/easterEggs'; const EASTER_EGGS_API_PREFIX = '/api/easterEggs';

View File

@ -1,4 +1,4 @@
import { AddChoiceRequest, ChangeDepartureTimeRequest, LocationKey, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../types"; import { AddChoiceRequest, ChangeDepartureTimeRequest, LocationKey, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types";
import { api } from "./Api"; import { api } from "./Api";
const FOOD_API_PREFIX = '/api/food'; const FOOD_API_PREFIX = '/api/food';

View File

@ -1,4 +1,4 @@
import { AddPizzaRequest, FinishDeliveryRequest, PizzaOrder, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../types"; import { AddPizzaRequest, FinishDeliveryRequest, PizzaOrder, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types";
import { api } from "./Api"; import { api } from "./Api";
const PIZZADAY_API_PREFIX = '/api/pizzaDay'; const PIZZADAY_API_PREFIX = '/api/pizzaDay';

View File

@ -1,4 +1,4 @@
import { FeatureRequest, UpdateFeatureVoteRequest } from "../types"; import { FeatureRequest, UpdateFeatureVoteRequest } from "../../../types";
import { api } from "./Api"; import { api } from "./Api";
const VOTING_API_PREFIX = '/api/voting'; const VOTING_API_PREFIX = '/api/voting';

View File

@ -4,7 +4,7 @@ import { useAuth } from "../context/auth";
import SettingsModal from "./modals/SettingsModal"; import SettingsModal from "./modals/SettingsModal";
import { useSettings } from "../context/settings"; import { useSettings } from "../context/settings";
import FeaturesVotingModal from "./modals/FeaturesVotingModal"; import FeaturesVotingModal from "./modals/FeaturesVotingModal";
import { FeatureRequest } from "../types"; import { FeatureRequest } from "../../../types";
import { errorHandler } from "../api/Api"; import { errorHandler } from "../api/Api";
import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi"; import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi";
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal"; import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";

View File

@ -1,5 +1,5 @@
import { Table } from "react-bootstrap"; import { Table } from "react-bootstrap";
import { Order, PizzaDayState, PizzaOrder } from "../types"; import { Order, PizzaDayState, PizzaOrder } from "../../../types";
import PizzaOrderRow from "./PizzaOrderRow"; import PizzaOrderRow from "./PizzaOrderRow";
import { updatePizzaFee } from "../api/PizzaDayApi"; import { updatePizzaFee } from "../api/PizzaDayApi";

View File

@ -2,7 +2,7 @@ import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMoneyBill1, faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { faMoneyBill1, faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { useAuth } from "../context/auth"; import { useAuth } from "../context/auth";
import { Order, PizzaDayState, PizzaOrder } from "../types"; import { Order, PizzaDayState, PizzaOrder } from "../../../types";
import PizzaAdditionalFeeModal from "./modals/PizzaAdditionalFeeModal"; import PizzaAdditionalFeeModal from "./modals/PizzaAdditionalFeeModal";
type Props = { type Props = {

View File

@ -1,5 +1,5 @@
import { Modal, Button, Form } from "react-bootstrap" import { Modal, Button, Form } from "react-bootstrap"
import { FeatureRequest } from "../../types"; import { FeatureRequest } from "../../../../types";
type Props = { type Props = {
isOpen: boolean, isOpen: boolean,

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getEasterEgg } from "../api/EasterEggApi"; import { getEasterEgg } from "../api/EasterEggApi";
import { AuthContextProps } from "./auth"; import { AuthContextProps } from "./auth";
import { EasterEgg } from "../types"; import { EasterEgg } from "../../../types";
export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undefined, boolean] => { export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undefined, boolean] => {
const [result, setResult] = useState<EasterEgg | undefined>(); const [result, setResult] = useState<EasterEgg | undefined>();

4
server/.gitignore vendored
View File

@ -1,7 +1,5 @@
/node_modules
/dist /dist
data.json /resources/easterEggs
.env.production .env.production
.env.development .env.development
.easter-eggs.json .easter-eggs.json
/resources/easterEggs

View File

@ -100,7 +100,9 @@ app.use("/api/", (req, res, next) => {
const emailHeader = req.header('remote-email'); const emailHeader = req.header('remote-email');
if (userHeader !== undefined && nameHeader !== undefined) { if (userHeader !== undefined && nameHeader !== undefined) {
const remoteName = Buffer.from(nameHeader, 'latin1').toString(); const remoteName = Buffer.from(nameHeader, 'latin1').toString();
console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader); if (ENVIRONMENT !== "production"){
console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader);
}
} }
} }
if (!req.headers.authorization) { if (!req.headers.authorization) {

View File

@ -268,14 +268,19 @@ export async function removeChoice(login: string, trusted: boolean, locationKey:
} }
/** /**
* Odstraní kompletně volbu uživatele. * Odstraní kompletně volbu uživatele, vyjma ignoredLocationKey (pokud byla předána a existuje).
* *
* @param login login uživatele * @param login login uživatele
* @param date datum, ke kterému se volby vztahují
* @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje
*/ */
async function removeChoiceIfPresent(login: string, date: string) { async function removeChoiceIfPresent(login: string, date: string, ignoredLocationKey?: LocationKey) {
let data: DayData = await storage.getData(date); let data: DayData = await storage.getData(date);
for (const key of Object.keys(data.choices)) { for (const key of Object.keys(data.choices)) {
const locationKey = key as LocationKey; const locationKey = key as LocationKey;
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
continue;
}
if (data.choices[locationKey] && login in data.choices[locationKey]) { if (data.choices[locationKey] && login in data.choices[locationKey]) {
delete data.choices[locationKey][login]; delete data.choices[locationKey][login];
if (Object.keys(data.choices[locationKey]).length === 0) { if (Object.keys(data.choices[locationKey]).length === 0) {
@ -329,6 +334,9 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lo
// Pokud měníme pouze lokaci, mažeme případné předchozí // Pokud měníme pouze lokaci, mažeme případné předchozí
if (foodIndex == null) { if (foodIndex == null) {
data = await removeChoiceIfPresent(login, selectedDate); data = await removeChoiceIfPresent(login, selectedDate);
} else {
// Mažeme případné ostatní volby (měla by být maximálně jedna)
removeChoiceIfPresent(login, selectedDate, locationKey);
} }
// TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce // TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
if (!(data.choices[locationKey])) { if (!(data.choices[locationKey])) {

View File

@ -1,8 +1,17 @@
import JSONdb from 'simple-json-db'; import JSONdb from 'simple-json-db';
import { StorageInterface } from "./StorageInterface"; import { StorageInterface } from "./StorageInterface";
import * as fs from 'fs';
import * as path from 'path';
const db = new JSONdb('./data.json'); const dbPath = path.resolve(__dirname, '../../data/db.json');
const dbDir = path.dirname(dbPath);
// Zajistěte, že adresář existuje
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
const db = new JSONdb(dbPath);
/** /**
* Implementace úložiště používající JSON soubor. * Implementace úložiště používající JSON soubor.
*/ */

View File

@ -111,6 +111,7 @@ export type Food = {
} }
// TODO tohle je dost špatné pojmenování, vzhledem k tomu co to obsahuje // TODO tohle je dost špatné pojmenování, vzhledem k tomu co to obsahuje
// TODO pokud by se použilo ovládáni výběru obědu kliknutím, pak bych restaurace z tohoto výčtu vyhodil
export enum Locations { export enum Locations {
SLADOVNICKA = 'Sladovnická', SLADOVNICKA = 'Sladovnická',
// UMOTLIKU = 'U Motlíků', // UMOTLIKU = 'U Motlíků',