Prvotní nástřel fungující aplikace
5
.gitignore
vendored
@ -1,7 +1,7 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
@ -21,3 +21,6 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
__pycache__
|
||||||
|
venv
|
12
README.md
@ -1,7 +1,15 @@
|
|||||||
# Pizza Day
|
# Luncher
|
||||||
|
|
||||||
Zatím to nemá dokumentaci.
|
Zatím to nemá dokumentaci.
|
||||||
Klient je v tomto adresáři, server v adresáři /server, obojí lze spustit pomocí:
|
Server je v adresáři /server, client v adresáři /client obojí lze spustit pomocí:
|
||||||
|
|
||||||
### `yarn`
|
### `yarn`
|
||||||
### `yarn start`
|
### `yarn start`
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- Popsat Food API, nginx
|
||||||
|
- Popsat spuštění pro vývoj
|
||||||
|
- Vyndat URL na Food API do .env
|
||||||
|
- Neselhat při nedostupnosti nebo chybě z Food API
|
||||||
|
- Dokončit docker-compose pro kompletní funkčnost
|
||||||
|
- Implementovat pizza day
|
3
client/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
build
|
1
client/.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
PUBLIC_URL=http://192.168.1.106:3005
|
1
client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
build
|
23
client/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
FROM node:alpine AS builder
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
COPY yarn.lock .
|
||||||
|
COPY tsconfig.json .
|
||||||
|
COPY .env.production .
|
||||||
|
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
COPY ./src ./src
|
||||||
|
COPY ./public ./public
|
||||||
|
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
FROM node:alpine
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /build .
|
||||||
|
|
||||||
|
RUN yarn global add serve && yarn
|
||||||
|
CMD ["serve", "-s", "."]
|
2
client/build.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t luncher-client .
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "luncher",
|
"name": "luncher-client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
@ -15,6 +16,7 @@
|
|||||||
"react-bootstrap": "^2.7.2",
|
"react-bootstrap": "^2.7.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"socket.io-client": "^4.6.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -1,21 +1,19 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
<head>
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta name="theme-color" content="#000000" />
|
||||||
name="description"
|
<meta name="description" content="Moderní webová aplikace pro lepší správu obědových preferencí" />
|
||||||
content="Web site created using create-react-app"
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
/>
|
<!--
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
-->
|
-->
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
<!--
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
@ -24,12 +22,13 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>Luncher</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<body>
|
||||||
<div id="root"></div>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<!--
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
@ -39,5 +38,6 @@
|
|||||||
To begin the development, run `npm start` or `yarn start`.
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
-->
|
-->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
51
client/src/Api.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// type Pizza = {
|
||||||
|
// name: string;
|
||||||
|
// // TODO ingredience
|
||||||
|
// sizes: [
|
||||||
|
// size: number,
|
||||||
|
// price: number,
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
|
||||||
|
import { getBaseUrl } from "./Utils";
|
||||||
|
|
||||||
|
async function request<TResponse>(
|
||||||
|
url: string,
|
||||||
|
config: RequestInit = {}
|
||||||
|
): Promise<TResponse> {
|
||||||
|
return fetch(getBaseUrl() + url, config).then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json() as TResponse;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
get: <TResponse>(url: string) => request<TResponse>(url),
|
||||||
|
post: <TBody extends BodyInit, TResponse>(url: string, body: TBody) => request<TResponse>(url, { method: 'POST', body, headers: { 'Content-Type': 'application/json' } }),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getData = async () => {
|
||||||
|
return await api.get<any>('/api/data');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFood = async () => {
|
||||||
|
return await api.get<any>('/api/food');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPizzy = async () => {
|
||||||
|
return await api.get<any>('/api/pizza');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPizzaDay = async () => {
|
||||||
|
return await api.post<any, any>('/api/createPizzaDay', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deletePizzaDay = async () => {
|
||||||
|
return await api.post<any, any>('/api/deletePizzaDay', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateChoice = async (name: string, choice: number | null) => {
|
||||||
|
return await api.post<any, any>('/api/updateChoice', JSON.stringify({ name, choice }));
|
||||||
|
}
|
@ -32,7 +32,29 @@
|
|||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-tables {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
160
client/src/App.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket';
|
||||||
|
import { getData, getFood, updateChoice } from './Api';
|
||||||
|
import { useAuth } from './context/auth';
|
||||||
|
import Login from './Login';
|
||||||
|
import { Locations, ClientData } from './Types';
|
||||||
|
import './App.css';
|
||||||
|
import { Alert, Form, Table } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const EVENT_CONNECT = "connect"
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||||
|
const [data, setData] = useState<ClientData>();
|
||||||
|
const [food, setFood] = useState<any>();
|
||||||
|
// const [pizzy, setPizzy] = useState();
|
||||||
|
const socket = useContext(SocketContext);
|
||||||
|
|
||||||
|
// Prvotní načtení aktuálního stavu
|
||||||
|
useEffect(() => {
|
||||||
|
// getPizzy().then(pizzy => {
|
||||||
|
// setPizzy(pizzy);
|
||||||
|
// });
|
||||||
|
getData().then(data => {
|
||||||
|
setData(data);
|
||||||
|
})
|
||||||
|
getFood().then(food => {
|
||||||
|
setFood(food);
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Registrace socket eventů
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on(EVENT_CONNECT, () => {
|
||||||
|
// console.log("Connected!");
|
||||||
|
setIsConnected(true);
|
||||||
|
});
|
||||||
|
socket.on(EVENT_DISCONNECT, () => {
|
||||||
|
// console.log("Disconnected!");
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
||||||
|
// const data: any = JSON.parse(payload);
|
||||||
|
// console.log("Přijata nová data ze socketu", newData);
|
||||||
|
setData(newData);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off(EVENT_CONNECT);
|
||||||
|
socket.off(EVENT_DISCONNECT);
|
||||||
|
socket.off(EVENT_MESSAGE);
|
||||||
|
}
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const changeChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const index = Object.values(Locations).indexOf(event.target.value as unknown as Locations);
|
||||||
|
if (auth?.login) {
|
||||||
|
await updateChoice(auth.login, index > -1 ? index : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFoodTable = (name, food) => {
|
||||||
|
return <div className='food-table'>
|
||||||
|
<h3>{name}</h3>
|
||||||
|
<Table striped bordered hover>
|
||||||
|
<tbody>
|
||||||
|
{food.map((f: any, index: number) =>
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{f.amount}</td>
|
||||||
|
<td>{f.name}</td>
|
||||||
|
<td>{f.price}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth || !auth.login) {
|
||||||
|
return <Login />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || !isConnected || !food) {
|
||||||
|
return <div>Načítám data...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// const pizzaDayExists = data?.state > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='wrapper'>
|
||||||
|
<Alert variant={'primary'}>
|
||||||
|
Tvé zobrazované jméno je {auth.login}. Změnu můžeš provést v local storage prohlížeče.<br />
|
||||||
|
<small>Pro gamer move: Změň si své jméno na cizí. Můžeš pak libovolně měnit jejich volbu.</small>
|
||||||
|
</Alert>
|
||||||
|
<h1 className='title'>Dnes je {data.date}</h1>
|
||||||
|
<div className='food-tables'>
|
||||||
|
{renderFoodTable('Sladovnická', food.sladovnicka)}
|
||||||
|
{renderFoodTable('U Motlíků', food["uMotliku:"])}
|
||||||
|
{renderFoodTable('TechTower', food.techTower)}
|
||||||
|
</div>
|
||||||
|
<div className='content-wrapper'>
|
||||||
|
<div className='content'>
|
||||||
|
<p>Jak to dnes vidíš s obědem?</p>
|
||||||
|
<Form.Select onChange={changeChoice}>
|
||||||
|
<option></option>
|
||||||
|
<option value={Locations.SLADOVNICKA}>Sladovnická</option>
|
||||||
|
<option value={Locations.UMOTLIKU}>U Motlíků</option>
|
||||||
|
<option value={Locations.TECHTOWER}>TechTower</option>
|
||||||
|
<option value={Locations.SPSE}>SPŠE</option>
|
||||||
|
<option value={Locations.VLASTNI}>Mám vlastní</option>
|
||||||
|
<option value={Locations.OBJEDNAVAM}>Budu objednávat</option>
|
||||||
|
<option value={Locations.NEOBEDVAM}>Nebudu obědvat</option>
|
||||||
|
</Form.Select>
|
||||||
|
<p style={{ fontSize: "12px", marginTop: "5px" }}>
|
||||||
|
Aktuálně je možné vybrat pouze jednu variantu. Vyber prázdnou položku pro odstranění.
|
||||||
|
</p>
|
||||||
|
{Object.keys(data.choices).length > 0 ?
|
||||||
|
<Table striped bordered hover className='results-table mt-5'>
|
||||||
|
<tbody>
|
||||||
|
{Object.keys(data.choices).map((key: string, index: number) =>
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{Object.values(Locations)[Number(key)]}</td>
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
{data.choices[Number(key)].map((p: string, index: number) => <li key={index}>{p}</li>)}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
: <div className='mt-5'><i>Zatím nikdo nehlasoval...</i></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* {!pizzaDayExists &&
|
||||||
|
<div>
|
||||||
|
<p>Pro dnešní den není aktuálně založen Pizza day.</p>
|
||||||
|
<Button onClick={async () => {
|
||||||
|
await createPizzaDay();
|
||||||
|
}}>Založit Pizza day</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{pizzaDayExists && <div>
|
||||||
|
<Button className='danger' onClick={async () => {
|
||||||
|
await deletePizzaDay();
|
||||||
|
}}>Smazat Pizza day</Button>
|
||||||
|
<OrderList orders={data.orders} />
|
||||||
|
</div>} */}
|
||||||
|
{/* <Button onClick={async () => {
|
||||||
|
const pizzy = await getPizzy();
|
||||||
|
console.log("Výsledek", pizzy);
|
||||||
|
}}>Získat pizzy</Button> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
13
client/src/Login.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.login {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
28
client/src/Login.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
import { useAuth } from './context/auth';
|
||||||
|
import './Login.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formulář pro prvotní zadání přihlašovacího jména.
|
||||||
|
*/
|
||||||
|
export default function Login() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const [loginName, setLoginName] = useState<string>('');
|
||||||
|
|
||||||
|
if (!auth || !auth.login) {
|
||||||
|
return <div className='login'>
|
||||||
|
<div className='login-inner'>
|
||||||
|
<p style={{ fontSize: "12px", marginTop: "10px" }}>Zobrazované jméno by mělo být ideálně vaše jméno a příjmení, nebo přezdívka, pod kterou vás kolegové dokážou snadno identifikovat. Jméno lze kdykoliv upravit/smazat v local storage prohlížeče.<br />PS: Enter nefunguje</p>
|
||||||
|
Zobrazované jméno: <input style={{ marginTop: "10px" }} onChange={(e) => setLoginName(e.target.value)} type='text' />
|
||||||
|
<Button onClick={() => {
|
||||||
|
if (loginName?.length > 0) {
|
||||||
|
auth?.setLogin(loginName);
|
||||||
|
}
|
||||||
|
}} style={{ marginTop: "20px" }}>Uložit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
// TODO nějaký loader
|
||||||
|
return <div>TODO</div>;
|
||||||
|
}
|
33
client/src/Types.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// TODO všechno v tomto souboru jsou duplicity se serverem, ale aktuálně nevím jaký je nejlepší způsob jejich sdílení
|
||||||
|
|
||||||
|
/** Jedna konkrétní pizza */
|
||||||
|
export interface Pizza {
|
||||||
|
name: string, // název pizzy
|
||||||
|
size: number, // velikost pizzy v cm
|
||||||
|
price: number, // cena pizzy v Kč, včetně krabice
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
customer: string, // název člověka
|
||||||
|
pizzaList: Pizza[], // seznam objednaných pizz
|
||||||
|
totalPrice: number, // celková cena všech objednaných pizz a krabic
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Choices {
|
||||||
|
[location: string]: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientData {
|
||||||
|
date: string, // dnešní datum pro zobrazení
|
||||||
|
choices: Choices, // seznam voleb
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locations {
|
||||||
|
SLADOVNICKA = 'Sladovnická',
|
||||||
|
UMOTLIKU = 'U Motlíků',
|
||||||
|
TECHTOWER = 'TechTower',
|
||||||
|
SPSE = 'SPŠE',
|
||||||
|
VLASTNI = 'Mám vlastní',
|
||||||
|
OBJEDNAVAM = 'Objednávám',
|
||||||
|
NEOBEDVAM = 'Neobědvám',
|
||||||
|
}
|
38
client/src/Utils.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Vrátí kořenovou URL serveru na základě aktuálního prostředí (vývojovou či produkční).
|
||||||
|
*
|
||||||
|
* @returns kořenová URL serveru
|
||||||
|
*/
|
||||||
|
export const getBaseUrl = (): string => {
|
||||||
|
if (process.env.PUBLIC_URL) {
|
||||||
|
return process.env.PUBLIC_URL;
|
||||||
|
}
|
||||||
|
return 'http://localhost:3001';
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOGIN_KEY = "login";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uloží login do local storage prohlížeče.
|
||||||
|
*
|
||||||
|
* @param login login
|
||||||
|
*/
|
||||||
|
export const storeLogin = (login: string) => {
|
||||||
|
localStorage.setItem(LOGIN_KEY, login);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrátí login z local storage, pokud tam je.
|
||||||
|
*
|
||||||
|
* @returns login nebo null
|
||||||
|
*/
|
||||||
|
export const getLogin = (): string | null => {
|
||||||
|
return localStorage.getItem(LOGIN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Odstraní login z local storage, pokud tam je.
|
||||||
|
*/
|
||||||
|
export const deleteLogin = () => {
|
||||||
|
localStorage.removeItem(LOGIN_KEY);
|
||||||
|
}
|
21
client/src/components/OrderList.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Table } from "react-bootstrap";
|
||||||
|
import { Order } from "../Types";
|
||||||
|
|
||||||
|
export default function OrderList({ orders }: { orders: Order[] }) {
|
||||||
|
return <Table striped bordered hover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pizza</th>
|
||||||
|
<th>Jméno</th>
|
||||||
|
<th>Cena (Kč)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map(order => <tr>
|
||||||
|
<td>{order.pizzaList[0].name}, {order.pizzaList[0].size}</td>
|
||||||
|
<td>{order.customer}</td>
|
||||||
|
<td>{order.totalPrice}</td>
|
||||||
|
</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
}
|
58
client/src/context/auth.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React, { ReactNode, useContext, useState } from "react"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
const LOGIN_KEY = 'login';
|
||||||
|
|
||||||
|
export type AuthContextProps = {
|
||||||
|
login?: string,
|
||||||
|
setLogin: (name: string) => void,
|
||||||
|
clearLogin: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const authContext = React.createContext<AuthContextProps | null>(null);
|
||||||
|
|
||||||
|
export function ProvideAuth(props: ContextProps) {
|
||||||
|
const auth = useProvideAuth();
|
||||||
|
return <authContext.Provider value={auth}>{props.children}</authContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
return useContext(authContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useProvideAuth(): AuthContextProps {
|
||||||
|
const [loginName, setLoginName] = useState<string | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const login = localStorage.getItem(LOGIN_KEY);
|
||||||
|
if (login) {
|
||||||
|
setLogin(login);
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginName) {
|
||||||
|
localStorage.setItem(LOGIN_KEY, loginName)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(LOGIN_KEY);
|
||||||
|
}
|
||||||
|
}, [loginName]);
|
||||||
|
|
||||||
|
function setLogin(login: string) {
|
||||||
|
setLoginName(login);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogin() {
|
||||||
|
setLoginName(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
login: loginName,
|
||||||
|
setLogin,
|
||||||
|
clearLogin
|
||||||
|
}
|
||||||
|
}
|
17
client/src/context/socket.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import socketio from "socket.io-client";
|
||||||
|
import { getBaseUrl } from "../Utils";
|
||||||
|
|
||||||
|
// Záměrně omezeno jen na websocket, aby se případně odhalilo chybné nastavení proxy serveru
|
||||||
|
export const socket = socketio.connect(getBaseUrl(), { transports: ["websocket"] });
|
||||||
|
export const SocketContext = React.createContext();
|
||||||
|
|
||||||
|
// Konstanty websocket eventů, musí odpovídat těm na serveru!
|
||||||
|
export const EVENT_CONNECT = 'connect';
|
||||||
|
export const EVENT_DISCONNECT = 'disconnect';
|
||||||
|
export const EVENT_MESSAGE = 'message';
|
||||||
|
// export const EVENT_CONFIG = 'config';
|
||||||
|
// export const EVENT_TOASTER = 'toaster';
|
||||||
|
// export const EVENT_VOTING = 'voting';
|
||||||
|
// export const EVENT_VOTE_CONFIG = 'voteSettings';
|
||||||
|
// export const EVENT_ADMIN = 'admin';
|
@ -1,3 +1,10 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
19
client/src/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import { SocketContext, socket } from './context/socket';
|
||||||
|
import { ProvideAuth } from './context/auth';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById('root') as HTMLElement
|
||||||
|
);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ProvideAuth>
|
||||||
|
<SocketContext.Provider value={socket}>
|
||||||
|
<App />
|
||||||
|
</SocketContext.Provider>
|
||||||
|
</ProvideAuth>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@ -21,6 +21,6 @@
|
|||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"client/src"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1691,6 +1691,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/commons" "^1.7.0"
|
"@sinonjs/commons" "^1.7.0"
|
||||||
|
|
||||||
|
"@socket.io/component-emitter@~3.1.0":
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
||||||
|
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
|
||||||
|
|
||||||
"@surma/rollup-plugin-off-main-thread@^2.2.3":
|
"@surma/rollup-plugin-off-main-thread@^2.2.3":
|
||||||
version "2.2.3"
|
version "2.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
|
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
|
||||||
@ -3594,7 +3599,7 @@ debug@2.6.9, debug@^2.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
|
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||||
@ -3915,6 +3920,22 @@ encodeurl@~1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||||
|
|
||||||
|
engine.io-client@~6.4.0:
|
||||||
|
version "6.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91"
|
||||||
|
integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==
|
||||||
|
dependencies:
|
||||||
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
|
debug "~4.3.1"
|
||||||
|
engine.io-parser "~5.0.3"
|
||||||
|
ws "~8.11.0"
|
||||||
|
xmlhttprequest-ssl "~2.0.0"
|
||||||
|
|
||||||
|
engine.io-parser@~5.0.3:
|
||||||
|
version "5.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45"
|
||||||
|
integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==
|
||||||
|
|
||||||
enhanced-resolve@^5.10.0:
|
enhanced-resolve@^5.10.0:
|
||||||
version "5.12.0"
|
version "5.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
|
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
|
||||||
@ -8190,6 +8211,24 @@ slash@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
||||||
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
||||||
|
|
||||||
|
socket.io-client@^4.6.1:
|
||||||
|
version "4.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab"
|
||||||
|
integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==
|
||||||
|
dependencies:
|
||||||
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
|
debug "~4.3.2"
|
||||||
|
engine.io-client "~6.4.0"
|
||||||
|
socket.io-parser "~4.2.1"
|
||||||
|
|
||||||
|
socket.io-parser@~4.2.1:
|
||||||
|
version "4.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206"
|
||||||
|
integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==
|
||||||
|
dependencies:
|
||||||
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
|
debug "~4.3.1"
|
||||||
|
|
||||||
sockjs@^0.3.24:
|
sockjs@^0.3.24:
|
||||||
version "0.3.24"
|
version "0.3.24"
|
||||||
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
|
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
|
||||||
@ -9442,6 +9481,11 @@ ws@^8.13.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||||
|
|
||||||
|
ws@~8.11.0:
|
||||||
|
version "8.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
|
||||||
|
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
|
||||||
|
|
||||||
xml-name-validator@^3.0.0:
|
xml-name-validator@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||||
@ -9452,6 +9496,11 @@ xmlchars@^2.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||||
|
|
||||||
|
xmlhttprequest-ssl@~2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
|
||||||
|
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
29
docker-compose.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
food_api:
|
||||||
|
build:
|
||||||
|
context: ./food_api
|
||||||
|
# ports:
|
||||||
|
# - "3002:80"
|
||||||
|
server:
|
||||||
|
depends_on:
|
||||||
|
- food_api
|
||||||
|
build:
|
||||||
|
context: ./server
|
||||||
|
# ports:
|
||||||
|
# - "3001:3001"
|
||||||
|
client:
|
||||||
|
build:
|
||||||
|
context: ./client
|
||||||
|
# ports:
|
||||||
|
# - "3000:3000"
|
||||||
|
nginx:
|
||||||
|
depends_on:
|
||||||
|
- server
|
||||||
|
- client
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: ./nginx
|
||||||
|
ports:
|
||||||
|
- 3005:80
|
10
food_api/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.9
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
|
||||||
|
|
||||||
|
COPY ./food_service.py /app
|
||||||
|
COPY ./food_api.py /app
|
||||||
|
|
||||||
|
CMD ["uvicorn", "food_api:app", "--host", "0.0.0.0", "--port", "3002"]
|
43
food_api/README.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# TODO
|
||||||
|
Následující informace jsou neaktuální. Už nemáme Flask, místo WSGI jedeme přes ASGI apod. Místo tohoto dokumentu využijte nadřazený README.md.
|
||||||
|
|
||||||
|
# POMPSZČPS
|
||||||
|
POMPSZČPS, neboli Parser Obědových Menu Plzeňských Stravovacích Zařízení v Části Plzeň-Slovany, je Python aplikace poskytující na jednom místě aktuální obědové menu pro několik stravovacích zařízení v městské části Plzeň 2-Slovany. Aktuálně podporuje následující podniky:
|
||||||
|
- [Pivnice Sladovnická](https://sladovnicka.unasplzenchutna.cz)
|
||||||
|
- [Restaurace U Motlíků](https://www.umotliku.cz)
|
||||||
|
- [Restaurace TechTower](https://www.equifarm.cz/restaurace-techtower)
|
||||||
|
|
||||||
|
Pro tyto podniky umožňuje získání aktuálního obědového menu, a to buďto barevným výpisem do konzole (přímým spuštěním `food_service.py`) nebo v podobě [WSGI](https://cs.wikipedia.org/wiki/Web_Server_Gateway_Interface) endpointu (`wsgi.py`), který vrací zmíněná menu jako strukturovaný JSON objekt pro další použití v jiných aplikacích.
|
||||||
|
|
||||||
|
## Závislosti
|
||||||
|
- [Python 3.x](https://www.python.org)
|
||||||
|
|
||||||
|
Pro použití jako konzolová aplikace
|
||||||
|
- [beautifulsoup4](https://pypi.org/project/beautifulsoup4)
|
||||||
|
|
||||||
|
Pro použití jako API endpoint
|
||||||
|
- [beautifulsoup4](https://pypi.org/project/beautifulsoup4)
|
||||||
|
- [Flask](https://pypi.org/project/Flask)
|
||||||
|
- [gunicorn](https://pypi.org/project/gunicorn)
|
||||||
|
|
||||||
|
## Použití
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
(Unix): source venv/bin/activate
|
||||||
|
(Windows): venv\Scripts\activate.bat
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
- Jako konzolová aplikace: `python food_service.py`
|
||||||
|
- Vypíše přehledně pod sebe menu všech aktuálně integrovaných podniků
|
||||||
|
- Jako JSON API endpoint
|
||||||
|
- TODO
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- Umožnit zadat a zobrazit menu pro jiné dny
|
||||||
|
- umožnit zadání datumem nebo názvem dne v týdnu
|
||||||
|
- validace - žádná sobota, neděle
|
||||||
|
- validace - datum musí být tento týden
|
||||||
|
- minimálně pro Motlíky to znamená úpravu URL a parseru
|
||||||
|
- Otestovat rozchození - vytvoření venv, instalace requirements, spuštění jako konzole
|
||||||
|
- Umožnit konfiguračně určit pro které podniky se bude menu získávat a zobrazovat (vyberu si jen ty, které mě zajímají)
|
||||||
|
- Umožnit konfiguračně nastavit výrazy pro detekci polévky
|
20
food_api/food_api.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from food_service import getMenuSladovnicka, getMenuTechTower, getMenuUMotliku
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root():
|
||||||
|
return {
|
||||||
|
'sladovnicka': getMenuSladovnicka(),
|
||||||
|
'uMotliku:': getMenuUMotliku(),
|
||||||
|
'techTower': getMenuTechTower()
|
||||||
|
}
|
252
food_api/food_service.py
Executable file
@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import tempfile
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import urllib.request
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
URL_SLADOVNICKA = "https://sladovnicka.unasplzenchutna.cz/cz/denni-nabidka"
|
||||||
|
URL_MOTLICI = "https://www.umotliku.cz"
|
||||||
|
URL_TECHTOWER = "https://www.equifarm.cz/restaurace-techtower"
|
||||||
|
|
||||||
|
DAY_NAMES = ['pondělí', 'úterý', 'středa',
|
||||||
|
'čtvrtek', 'pátek', 'sobota', 'neděle']
|
||||||
|
|
||||||
|
# Fráze v názvech jídel, které naznačují že se jedná o polévku
|
||||||
|
SOUP_NAMES = ['polévka', 'česnečka', 'česnekový krém', 'cibulačka', 'vývar']
|
||||||
|
|
||||||
|
|
||||||
|
class bcolors:
|
||||||
|
HEADER = '\033[95m'
|
||||||
|
OKBLUE = '\033[94m'
|
||||||
|
OKCYAN = '\033[96m'
|
||||||
|
OKGREEN = '\033[92m'
|
||||||
|
WARNING = '\033[93m'
|
||||||
|
FAIL = '\033[91m'
|
||||||
|
ENDC = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
|
||||||
|
|
||||||
|
class Food:
|
||||||
|
name = None
|
||||||
|
amount = None
|
||||||
|
price = None
|
||||||
|
is_soup = False
|
||||||
|
|
||||||
|
def __init__(self, name, amount, price, is_soup=False) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.amount = amount
|
||||||
|
self.price = price
|
||||||
|
self.is_soup = is_soup
|
||||||
|
|
||||||
|
|
||||||
|
def getOrDownloadHtml(prefix: str, url: str):
|
||||||
|
'''Vrátí HTML pro daný prefix pro aktuální den.
|
||||||
|
Pokud v tempu neexistuje, provede jeho stažení z předané URL a uložení.'''
|
||||||
|
filename = prefix + "_" + date.today().strftime("%Y_%m_%d") + ".html"
|
||||||
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
urllib.request.urlretrieve(url, filepath)
|
||||||
|
file = open(filepath, "r")
|
||||||
|
contents = file.read()
|
||||||
|
file.close()
|
||||||
|
return contents
|
||||||
|
|
||||||
|
|
||||||
|
def isNameOfDay(text: str):
|
||||||
|
'''Vrátí True, pokud předaný text představuje název dne v týdnu (např. "pondělí")'''
|
||||||
|
return text.strip().lower() in DAY_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
def getDayNameOfDate(date: datetime.datetime):
|
||||||
|
'''Vrátí název dne v týdnu - např. pondělí, úterý, ...'''
|
||||||
|
return DAY_NAMES[date.weekday()]
|
||||||
|
|
||||||
|
|
||||||
|
def getStartOfWeekDate():
|
||||||
|
'''Vrátí datetime představující pondělí v aktuálním týdnu.'''
|
||||||
|
today = datetime.datetime.now()
|
||||||
|
return today - timedelta(days=today.weekday())
|
||||||
|
|
||||||
|
|
||||||
|
def isTextSoupName(text: str):
|
||||||
|
'''Vrátí True, pokud se předaný text jeví jako název polévky.
|
||||||
|
Používá se tam, kde nemáme lepší způsob detekce (TechTower).'''
|
||||||
|
for name in SOUP_NAMES:
|
||||||
|
if name in text.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def printMenu(name: str, foodList: List[Food]):
|
||||||
|
'''Vytiskne jídelní lístek na obrazovku.'''
|
||||||
|
print(f"{bcolors.OKGREEN}{name}{bcolors.ENDC}\n---------------------------------------------------------------------------------")
|
||||||
|
maxLength = 0
|
||||||
|
for jidlo in foodList:
|
||||||
|
if len(jidlo.name) > maxLength:
|
||||||
|
maxLength = len(jidlo.name)
|
||||||
|
for jidlo in foodList:
|
||||||
|
barva = bcolors.HEADER if jidlo.is_soup else bcolors.WARNING
|
||||||
|
print(f"{barva}{jidlo.amount}\t{jidlo.name.ljust(maxLength)}\t{bcolors.ENDC}{bcolors.OKCYAN}{jidlo.price}{bcolors.ENDC}")
|
||||||
|
print('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def getMenuSladovnicka() -> List[Food]:
|
||||||
|
html = getOrDownloadHtml('sladovnicka', URL_SLADOVNICKA)
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
div = soup.select_one("div.tab-pane.fade.in.active")
|
||||||
|
datumDen = div.find("h2").text
|
||||||
|
split = datumDen.split(".")
|
||||||
|
denMesic = split[0] + "." + split[1] + "."
|
||||||
|
# nazevDen = split[2]
|
||||||
|
dnesniDatum = date.today().strftime("%-d.%-m.")
|
||||||
|
if denMesic != dnesniDatum:
|
||||||
|
print('Chyba: neočekávané datum na stránce Sladovnické (' +
|
||||||
|
denMesic + '), očekáváno ' + dnesniDatum, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
tables = div.find_all("table", {"class": "simple"})
|
||||||
|
if len(tables) != 2:
|
||||||
|
print('Chyba: neočekávaný počet tabulek na stránce Sladovnické (' +
|
||||||
|
str(len(tables)) + '), očekávány 2', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
foodList: List[Food] = []
|
||||||
|
|
||||||
|
polevkaValues = tables[0].find_all("td")
|
||||||
|
amount = polevkaValues[0].text.strip()
|
||||||
|
name = polevkaValues[1].text.strip()
|
||||||
|
price = polevkaValues[2].text.strip()
|
||||||
|
foodList.append(Food(name, amount, price, True))
|
||||||
|
|
||||||
|
foodTables = tables[1].find_all("tr")
|
||||||
|
for food in foodTables:
|
||||||
|
rows = food.find_all("td")
|
||||||
|
if (len(rows) != 3):
|
||||||
|
print("Neočekávaný počet řádek hlavního jídla Sladovnické (" +
|
||||||
|
str(len(rows)) + ", očekávány 3, přeskakuji...")
|
||||||
|
continue
|
||||||
|
amount = rows[0].text.strip()
|
||||||
|
name = rows[1].text.strip()
|
||||||
|
price = rows[2].text.strip()
|
||||||
|
foodList.append(Food(name, amount, price))
|
||||||
|
return foodList
|
||||||
|
|
||||||
|
|
||||||
|
def getMenuUMotliku() -> List[Food]:
|
||||||
|
html = getOrDownloadHtml('u_motliku', URL_MOTLICI)
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
table = soup.find("table", {"class": "Xtable-striped"})
|
||||||
|
rows = table.find_all("tr")
|
||||||
|
if len(rows) < 4:
|
||||||
|
print('Chyba: neočekávaný celkový počet řádek tabulky (' +
|
||||||
|
str(len(rows)) + '), očekáváno 4 a více', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
foodList: List[Food] = []
|
||||||
|
|
||||||
|
if rows[0].td.text.strip() == 'Polévka':
|
||||||
|
tds = rows[1].find_all("td")
|
||||||
|
if len(tds) != 3:
|
||||||
|
print('Chyba: neočekávaný počet <td> elementů v řádce polévky (' +
|
||||||
|
str(len(tds)) + '), očekáváno 3', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
amount = tds[0].text.strip()
|
||||||
|
name = tds[1].text.strip()
|
||||||
|
price = tds[2].text.strip().replace(',-', '')
|
||||||
|
foodList.append(Food(name, amount, price, True))
|
||||||
|
rows = rows[2:]
|
||||||
|
|
||||||
|
if rows[0].td.text.strip() == 'Hlavní jídlo':
|
||||||
|
for i in range(1, len(rows)):
|
||||||
|
tds = rows[i].find_all("td")
|
||||||
|
if len(tds) != 3:
|
||||||
|
print("Neočekávaný počet <td> elementů (" + str(len(tds)
|
||||||
|
) + ") pro hlavní jídlo " + str(i) + ", přeskakuji")
|
||||||
|
continue
|
||||||
|
amount = tds[0].text.strip()
|
||||||
|
name = tds[1].text.strip()
|
||||||
|
price = tds[2].text.strip().replace(',-', '')
|
||||||
|
foodList.append(Food(name, amount, price))
|
||||||
|
return foodList
|
||||||
|
|
||||||
|
|
||||||
|
def getMenuTechTower() -> List[Food]:
|
||||||
|
html = getOrDownloadHtml('techtower', URL_TECHTOWER)
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
fonts = soup.find_all("font", {"class": ["wsw-41"]})
|
||||||
|
font = None
|
||||||
|
for f in fonts:
|
||||||
|
if (f.text.strip().startswith("Obědy")):
|
||||||
|
font = f
|
||||||
|
if font is None:
|
||||||
|
print('Chyba: nenalezen <font> pro obědy v HTML Techtower.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
siblings = font.parent.parent.find_next_siblings("p")
|
||||||
|
# dayNumber = date.today().strftime("%w")
|
||||||
|
currentDayName = getDayNameOfDate(datetime.datetime.now())
|
||||||
|
foodList = []
|
||||||
|
doParse = False
|
||||||
|
for i in range(0, len(siblings)):
|
||||||
|
text = siblings[i].text.strip().replace('\t', '').replace('\n', ' ')
|
||||||
|
if isNameOfDay(text):
|
||||||
|
if text == currentDayName:
|
||||||
|
# Našli jsme dnešní den, odtud začínáme parsovat jídla
|
||||||
|
doParse = True
|
||||||
|
elif doParse == True:
|
||||||
|
# Už parsujeme jídla, ale narazili jsme na následující den - končíme
|
||||||
|
break
|
||||||
|
elif doParse:
|
||||||
|
if len(text.strip()) == 0:
|
||||||
|
# Prázdná řádka - končíme (je za pátečním menu TechTower)
|
||||||
|
break
|
||||||
|
price = '? Kč'
|
||||||
|
if text.endswith('Kč'):
|
||||||
|
split = text.rsplit(' ', 2)
|
||||||
|
price = " ".join(split[1:])
|
||||||
|
text = split[0]
|
||||||
|
foodList.append(Food(text, '-', price, isTextSoupName(text)))
|
||||||
|
return foodList
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
input = sys.argv[1].lower()
|
||||||
|
selectedDate = None
|
||||||
|
if input[0].isalpha():
|
||||||
|
matches = []
|
||||||
|
for day in DAY_NAMES:
|
||||||
|
if day.startswith(input):
|
||||||
|
matches.append(day)
|
||||||
|
if len(matches) == 1:
|
||||||
|
print("Match - den v týdnu - " + matches[0])
|
||||||
|
selectedDate = getStartOfWeekDate(
|
||||||
|
) + timedelta(DAY_NAMES.index(matches[0]))
|
||||||
|
elif len(matches) == 0:
|
||||||
|
# TODO zkusit v, z (včera, zítra)
|
||||||
|
if 'zítra'.startswith(input):
|
||||||
|
print("Match - zítra")
|
||||||
|
selectedDate = datetime.datetime.now() + timedelta(days=1)
|
||||||
|
elif 'včera'.startswith(input):
|
||||||
|
print("Match - včera")
|
||||||
|
selectedDate = datetime.datetime.now() + timedelta(days=-1)
|
||||||
|
elif 'dneska'.startswith(input):
|
||||||
|
print("Match - dnes")
|
||||||
|
selectedDate = datetime.datetime.now()
|
||||||
|
else:
|
||||||
|
print('Nejasný parametr "' + input +
|
||||||
|
'" - může znamenat jednu z možností: ' + ', '.join(matches), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# TODO implementovat zadání datem
|
||||||
|
print('Zadání datem není aktuálně implementováno', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print("Datum: " + selectedDate.strftime('%d.%m.%Y'))
|
||||||
|
print("Den: " + getDayNameOfDate(selectedDate))
|
||||||
|
# printMenu('Sladovnická', getMenuSladovnicka())
|
||||||
|
# printMenu('U Motlíků', getMenuUMotliku())
|
||||||
|
# printMenu('TechTower', getMenuTechTower())
|
3
food_api/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
beautifulsoup4==4.12.2
|
||||||
|
fastapi==0.95.2
|
||||||
|
uvicorn==0.22.0
|
8
food_api/run_dev.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
cd $dir
|
||||||
|
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
uvicorn food_api:app --port 3002 --reload
|
2
nginx/Dockerfile
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
FROM nginx
|
||||||
|
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
47
nginx/default.conf
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
upstream client {
|
||||||
|
server client:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream server {
|
||||||
|
server server:3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream food_api {
|
||||||
|
server food_api:3002;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://client;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static {
|
||||||
|
proxy_pass http://client;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /sockjs-node {
|
||||||
|
proxy_pass http://client;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /socket.io {
|
||||||
|
proxy_pass http://server;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/food {
|
||||||
|
rewrite /api/food(.*) /$1 break;
|
||||||
|
proxy_pass http://food_api;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
# rewrite /api/(.*) /$1 break;
|
||||||
|
proxy_pass http://server;
|
||||||
|
}
|
||||||
|
}
|
4
run_dev.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
./food_api/run_dev.sh &
|
||||||
|
cd server && yarn install && yarn start &
|
||||||
|
cd client && yarn install && yarn start &
|
||||||
|
wait
|
1
server/.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
FOOD_API_URL=http://nginx/api/food
|
1
server/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
/dist
|
15
server/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM node:alpine
|
||||||
|
|
||||||
|
ENV LANG cs_CZ.UTF-8
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json /app
|
||||||
|
COPY yarn.lock /app
|
||||||
|
COPY .env.production /app
|
||||||
|
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
ADD ./dist /app
|
||||||
|
|
||||||
|
CMD [ "node", "/app/index.js" ]
|
3
server/build.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
yarn install --frozen-lockfile && yarn build
|
||||||
|
docker build -t luncher-server .
|
10
server/data.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"2023-05-30": {
|
||||||
|
"date": "30.05.2023 (úterý)",
|
||||||
|
"choices": {}
|
||||||
|
},
|
||||||
|
"2023-06-01": {
|
||||||
|
"date": "01.06.2023 (čtvrtek)",
|
||||||
|
"choices": {}
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,14 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ts-node src/index.ts"
|
"start": "ts-node src/index.ts",
|
||||||
|
"build": "tsc -p ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/node": "^20.2.5",
|
||||||
"@types/request-promise": "^4.1.48",
|
"@types/request-promise": "^4.1.48",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.2"
|
"typescript": "^5.0.2"
|
||||||
@ -17,6 +21,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise": "^4.2.6",
|
"request-promise": "^4.2.6",
|
||||||
|
"simple-json-db": "^2.0.0",
|
||||||
"socket.io": "^4.6.1"
|
"socket.io": "^4.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,7 +6,7 @@ import fs from 'fs';
|
|||||||
|
|
||||||
type PizzaSize = {
|
type PizzaSize = {
|
||||||
size: string,
|
size: string,
|
||||||
pizzaPrice: string,
|
pizzaPrice: number,
|
||||||
boxPrice: number,
|
boxPrice: number,
|
||||||
price: number
|
price: number
|
||||||
}
|
}
|
||||||
@ -21,12 +21,15 @@ type Pizza = {
|
|||||||
const baseUrl = 'https://www.pizzachefie.cz';
|
const baseUrl = 'https://www.pizzachefie.cz';
|
||||||
const pizzyUrl = `${baseUrl}/pizzy.html?pobocka=plzen`;
|
const pizzyUrl = `${baseUrl}/pizzy.html?pobocka=plzen`;
|
||||||
|
|
||||||
|
// URL na Food API - získání jídelních lístků restaurací
|
||||||
|
const foodUrl = process.env.FOOD_API_URL || 'http://localhost:3002';
|
||||||
|
|
||||||
const buildPizzaUrl = (pizzaUrl: string) => {
|
const buildPizzaUrl = (pizzaUrl: string) => {
|
||||||
return `${baseUrl}/${pizzaUrl}`;
|
return `${baseUrl}/${pizzaUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ceny krabic dle velikosti
|
// Ceny krabic dle velikosti
|
||||||
const boxPrices = {
|
const boxPrices: { [key: string]: number } = {
|
||||||
"30cm": 13,
|
"30cm": 13,
|
||||||
"35cm": 15,
|
"35cm": 15,
|
||||||
"40cm": 18,
|
"40cm": 18,
|
||||||
@ -55,19 +58,20 @@ const downloadPizzy = async () => {
|
|||||||
// Název
|
// Název
|
||||||
const name = $('.produkt > h2', pizzaHtml).first().text()
|
const name = $('.produkt > h2', pizzaHtml).first().text()
|
||||||
// Přísady
|
// Přísady
|
||||||
const ingredients = []
|
const ingredients: string[] = []
|
||||||
const ingredientsHtml = $('.prisady > li', pizzaHtml);
|
const ingredientsHtml = $('.prisady > li', pizzaHtml);
|
||||||
ingredientsHtml.each((i, elm) => {
|
ingredientsHtml.each((i, elm) => {
|
||||||
ingredients.push($(elm).text());
|
ingredients.push($(elm).text());
|
||||||
})
|
})
|
||||||
// Velikosti
|
// Velikosti
|
||||||
const sizes = [];
|
const sizes: PizzaSize[] = [];
|
||||||
const a = $('.varianty > li > a', pizzaHtml);
|
const a = $('.varianty > li > a', pizzaHtml);
|
||||||
a.each((i, elm) => {
|
a.each((i, elm) => {
|
||||||
const size = $('span', elm).text();
|
// TODO nedoděláno
|
||||||
const priceKc = $(elm).text().split(size).pop().trim();
|
// const size = $('span', elm).text();
|
||||||
const price = Number.parseInt(priceKc.split(" Kč")[0]);
|
// const priceKc = $(elm).text().split(size).pop().trim();
|
||||||
sizes.push({ size: size, pizzaPrice: price, boxPrice: boxPrices[size], price: price + boxPrices[size] });
|
// const price = Number.parseInt(priceKc.split(" Kč")[0]);
|
||||||
|
// sizes.push({ size: size, pizzaPrice: price, boxPrice: boxPrices[size], price: price + boxPrices[size] });
|
||||||
})
|
})
|
||||||
result.push({
|
result.push({
|
||||||
name: name,
|
name: name,
|
||||||
@ -101,6 +105,15 @@ export const fetchPizzy = async () => {
|
|||||||
console.log(`Zapsán ${dataPath}`);
|
console.log(`Zapsán ${dataPath}`);
|
||||||
return pizzy;
|
return pizzy;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO tohle sem absolutně nepatří! dát do vlastní servisky!
|
||||||
|
export const fetchFood = async () => {
|
||||||
|
try {
|
||||||
|
const json = await rp(foodUrl);
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Chyba při volání Food API", error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
3
server/src/database.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import JSONdb from 'simple-json-db';
|
||||||
|
|
||||||
|
export const db = new JSONdb('./data.json');
|
@ -1,7 +1,9 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import bodyParser from "body-parser";
|
import bodyParser from "body-parser";
|
||||||
import { fetchPizzy } from "./chefie";
|
import { fetchFood, fetchPizzy } from "./chefie";
|
||||||
|
import cors from 'cors';
|
||||||
|
import { getData, updateChoice } from "./service";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = require("http").createServer(app);
|
const server = require("http").createServer(app);
|
||||||
@ -13,12 +15,25 @@ const io = new Server(server, {
|
|||||||
|
|
||||||
// Body-parser middleware for parsing JSON
|
// Body-parser middleware for parsing JSON
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
// app.use(express.json());
|
||||||
|
|
||||||
const cors = require('cors');
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: '*'
|
origin: '*'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/** Vrátí data pro aktuální den. */
|
||||||
|
app.get("/api/data", (req, res) => {
|
||||||
|
res.status(200).json(getData());
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Vrátí obědové menu pro dostupné podniky. */
|
||||||
|
app.get("/api/food", (req, res) => {
|
||||||
|
fetchFood().then(food => {
|
||||||
|
res.status(200).json(food);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Vrátí seznam dostupných pizz. */
|
||||||
app.get("/api/pizza", (req, res) => {
|
app.get("/api/pizza", (req, res) => {
|
||||||
fetchPizzy().then(pizzaList => {
|
fetchPizzy().then(pizzaList => {
|
||||||
console.log("Výsledek", pizzaList);
|
console.log("Výsledek", pizzaList);
|
||||||
@ -26,6 +41,30 @@ app.get("/api/pizza", (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// /** Založí pizza day pro aktuální den, za předpokladu že dosud neexistuje. */
|
||||||
|
// app.post("/api/createPizzaDay", (req, res) => {
|
||||||
|
// const data = createPizzaDay();
|
||||||
|
// res.status(200).json(data);
|
||||||
|
// io.emit("message", data);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// /** Smaže pizza day pro aktuální den, za předpokladu že existuje. */
|
||||||
|
// app.post("/api/deletePizzaDay", (req, res) => {
|
||||||
|
// deletePizzaDay();
|
||||||
|
// io.emit("message", getData());
|
||||||
|
// });
|
||||||
|
|
||||||
|
app.post("/api/updateChoice", (req, res) => {
|
||||||
|
console.log("Změna výběru", req.body);
|
||||||
|
if (!req.body.hasOwnProperty('name')) {
|
||||||
|
res.status(400).json({});
|
||||||
|
}
|
||||||
|
const data = updateChoice(req.body.name, req.body.choice);
|
||||||
|
io.emit("message", data);
|
||||||
|
res.status(200).json(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO smazat
|
||||||
app.post("/api/zprava", (req, res) => {
|
app.post("/api/zprava", (req, res) => {
|
||||||
const { username, message } = req.body;
|
const { username, message } = req.body;
|
||||||
|
|
||||||
@ -49,6 +88,7 @@ io.on("connection", (socket) => {
|
|||||||
io.emit("message", message);
|
io.emit("message", message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO smazat
|
||||||
socket.on("jduKafe", ({ username, timeString }) => {
|
socket.on("jduKafe", ({ username, timeString }) => {
|
||||||
console.log(`Received message: ${username}`);
|
console.log(`Received message: ${username}`);
|
||||||
socket.broadcast.emit("jduKafe", `${timeString}: ${username} -> jdu Kafe`);
|
socket.broadcast.emit("jduKafe", `${timeString}: ${username} -> jdu Kafe`);
|
||||||
@ -60,7 +100,8 @@ io.on("connection", (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`Server listening on port ${PORT}`);
|
console.log(`Server listening on ${HOST}, port ${PORT}`);
|
||||||
});
|
});
|
105
server/src/service.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { ClientData, Locations } from "./types";
|
||||||
|
import { db } from "./database";
|
||||||
|
import { getTodayString } from "./utils";
|
||||||
|
import { getDate } from "./utils";
|
||||||
|
|
||||||
|
// /** Jedna konkrétní pizza */
|
||||||
|
// interface Pizza {
|
||||||
|
// name: string, // název pizzy
|
||||||
|
// size: number, // velikost pizzy v cm
|
||||||
|
// price: number, // cena pizzy v Kč, včetně krabice
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /** Objednávka jednoho člověka */
|
||||||
|
// interface Order {
|
||||||
|
// customer: string, // název člověka
|
||||||
|
// pizzaList: Pizza[], // seznam objednaných pizz
|
||||||
|
// totalPrice: number, // celková cena všech objednaných pizz a krabic
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /** Stav pizza dne. */
|
||||||
|
// enum State {
|
||||||
|
// NOT_CREATED, // Pizza day nebyl založen
|
||||||
|
// CREATED, // Pizza day je založen
|
||||||
|
// LOCKED // Objednávky uzamčeny
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /** Veškerá data pro zobrazení na klientovi */
|
||||||
|
// interface ClientData {
|
||||||
|
// date: string, // dnešní datum pro zobrazení
|
||||||
|
// state: State, // stav pizza dne
|
||||||
|
// orders?: Order[], // seznam objednávek, pokud není vyplněno, není založen pizza day
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** Vrátí "prázdná" (implicitní) data, pokud ještě nikdo nehlasoval. */
|
||||||
|
function getEmptyData(): ClientData {
|
||||||
|
return { date: getTodayString(), choices: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrátí veškerá klientská data pro aktuální den.
|
||||||
|
*/
|
||||||
|
export function getData(): ClientData {
|
||||||
|
const data = db.get(getDate()) || getEmptyData();
|
||||||
|
console.log("Vracím data pro dnešní den", data); // TODO smazat
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Vytvoří pizza day pro aktuální den a vrátí data pro klienta.
|
||||||
|
// */
|
||||||
|
// export function createPizzaDay(): ClientData {
|
||||||
|
// const today = getDate();
|
||||||
|
// if (db.has(today)) {
|
||||||
|
// throw Error("Pizza day pro dnešní den již existuje");
|
||||||
|
// }
|
||||||
|
// const data = { date: getTodayString(), state: State.CREATED, orders: [] };
|
||||||
|
// db.set(today, data);
|
||||||
|
// return data;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Smaže pizza day pro aktuální den.
|
||||||
|
// */
|
||||||
|
// export function deletePizzaDay() {
|
||||||
|
// const today = getDate();
|
||||||
|
// if (!db.has(today)) {
|
||||||
|
// throw Error("Pizza day pro dnešní den neexistuje");
|
||||||
|
// }
|
||||||
|
// db.delete(today);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function initIfNeeded() {
|
||||||
|
const today = getDate();
|
||||||
|
if (!db.has(today)) {
|
||||||
|
db.set(today, getEmptyData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeChoice(login: string, data: ClientData) {
|
||||||
|
for (let key of Object.keys(data.choices)) {
|
||||||
|
if (data.choices[key] && data.choices[key].includes(login)) {
|
||||||
|
const index = data.choices[key].indexOf(login);
|
||||||
|
data.choices[key].splice(index, 1);
|
||||||
|
if (data.choices[key].length == 0) {
|
||||||
|
delete data.choices[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateChoice(login: string, choice: Locations | null) {
|
||||||
|
initIfNeeded();
|
||||||
|
const today = getDate();
|
||||||
|
let data: ClientData = db.get(today);
|
||||||
|
data = removeChoice(login, data);
|
||||||
|
if (choice !== null) {
|
||||||
|
if (!data.choices?.[choice]) {
|
||||||
|
data.choices[choice] = [];
|
||||||
|
}
|
||||||
|
data.choices[choice].push(login);
|
||||||
|
}
|
||||||
|
db.set(today, data);
|
||||||
|
return data;
|
||||||
|
}
|
18
server/src/types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface Choices {
|
||||||
|
[location: string]: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientData {
|
||||||
|
date: string, // dnešní datum pro zobrazení
|
||||||
|
choices: Choices, // seznam voleb
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locations {
|
||||||
|
SLADOVNICKA = 'Sladovnická',
|
||||||
|
UMOTLIKU = 'U Motlíků',
|
||||||
|
TECHTOWER = 'TechTower',
|
||||||
|
SPSE = 'SPŠE',
|
||||||
|
VLASTNI = 'Mám vlastní',
|
||||||
|
OBJEDNAVAM = 'Objednávám',
|
||||||
|
NEOBEDVAM = 'Neobědvám',
|
||||||
|
}
|
17
server/src/utils.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export function getDate() {
|
||||||
|
const date = new Date();
|
||||||
|
let currentDay = String(date.getDate()).padStart(2, '0');
|
||||||
|
let currentMonth = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
let currentYear = date.getFullYear();
|
||||||
|
return `${currentYear}-${currentMonth}-${currentDay}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vrátí human-readable reprezentaci dnešního data pro zobrazení. */
|
||||||
|
export function getTodayString() {
|
||||||
|
const date = new Date();
|
||||||
|
let currentDay = String(date.getDate()).padStart(2, '0');
|
||||||
|
let currentMonth = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
let currentYear = date.getFullYear();
|
||||||
|
let currentDayOfWeek = date.toLocaleDateString("CZ-cs", { weekday: 'long' });
|
||||||
|
return `${currentDay}.${currentMonth}.${currentYear} (${currentDayOfWeek})`;
|
||||||
|
}
|
@ -2,7 +2,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2016",
|
"target": "ES2016",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
|
"jsx": "react",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
1798
server/yarn.lock
39
src/Api.ts
@ -1,39 +0,0 @@
|
|||||||
type Pizza = {
|
|
||||||
name: string;
|
|
||||||
// TODO ingredience
|
|
||||||
sizes: [
|
|
||||||
size: number,
|
|
||||||
price: number,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBaseUrl = (): string => {
|
|
||||||
if (process.env.PUBLIC_URL) {
|
|
||||||
return process.env.PUBLIC_URL;
|
|
||||||
}
|
|
||||||
return 'http://localhost:3001';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function request<TResponse>(
|
|
||||||
url: string,
|
|
||||||
config: RequestInit = {}
|
|
||||||
): Promise<TResponse> {
|
|
||||||
console.log("Calling fetch on", getBaseUrl() + url);
|
|
||||||
return fetch(getBaseUrl() + url, config).then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log("Response", response.status)
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
||||||
console.log("response", response);
|
|
||||||
return response.json() as TResponse;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = {
|
|
||||||
get: <TResponse>(url: string) => request<TResponse>(url),
|
|
||||||
post: <TBody extends BodyInit, TResponse>(url: string, body: TBody) => request<TResponse>(url, { method: 'POST', body }),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getPizzy = async () => {
|
|
||||||
return await api.get<any>('/api/pizza');
|
|
||||||
}
|
|
26
src/App.tsx
@ -1,26 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import './App.css';
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
|
||||||
import { Button } from 'react-bootstrap';
|
|
||||||
import { getPizzy } from './Api';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [pizzy, setPizzy] = useState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getPizzy().then(pizzy => {
|
|
||||||
setPizzy(pizzy);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Button onClick={async () => {
|
|
||||||
const pizzy = await getPizzy();
|
|
||||||
console.log("Výsledek", pizzy);
|
|
||||||
}}>Získat pizzy</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById('root') as HTMLElement
|
|
||||||
);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
@ -1,15 +0,0 @@
|
|||||||
import { ReportHandler } from 'web-vitals';
|
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|