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.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
@ -21,3 +21,6 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
__pycache__
|
||||
venv
|
12
README.md
@ -1,7 +1,15 @@
|
||||
# Pizza Day
|
||||
# Luncher
|
||||
|
||||
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 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",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
@ -15,6 +16,7 @@
|
||||
"react-bootstrap": "^2.7.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
@ -42,4 +44,4 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -1,21 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Moderní webová aplikace pro lepší správu obědových preferencí" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
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/
|
||||
-->
|
||||
<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.
|
||||
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.
|
||||
@ -24,12 +22,13 @@
|
||||
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`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
<title>Luncher</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
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 create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</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 {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
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 {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
@ -10,4 +17,4 @@ body {
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
}
|
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"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"client/src"
|
||||
]
|
||||
}
|
||||
}
|
@ -1691,6 +1691,11 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
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"
|
||||
@ -3594,7 +3599,7 @@ debug@2.6.9, debug@^2.6.0:
|
||||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
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"
|
||||
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:
|
||||
version "5.12.0"
|
||||
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"
|
||||
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:
|
||||
version "0.3.24"
|
||||
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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "5.0.8"
|
||||
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
|
3
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",
|
||||
"main": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "ts-node src/index.ts"
|
||||
"start": "ts-node src/index.ts",
|
||||
"build": "tsc -p ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/request-promise": "^4.1.48",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.2"
|
||||
@ -17,6 +21,7 @@
|
||||
"express": "^4.18.2",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"simple-json-db": "^2.0.0",
|
||||
"socket.io": "^4.6.1"
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import fs from 'fs';
|
||||
|
||||
type PizzaSize = {
|
||||
size: string,
|
||||
pizzaPrice: string,
|
||||
pizzaPrice: number,
|
||||
boxPrice: number,
|
||||
price: number
|
||||
}
|
||||
@ -21,12 +21,15 @@ type Pizza = {
|
||||
const baseUrl = 'https://www.pizzachefie.cz';
|
||||
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) => {
|
||||
return `${baseUrl}/${pizzaUrl}`;
|
||||
}
|
||||
|
||||
// Ceny krabic dle velikosti
|
||||
const boxPrices = {
|
||||
const boxPrices: { [key: string]: number } = {
|
||||
"30cm": 13,
|
||||
"35cm": 15,
|
||||
"40cm": 18,
|
||||
@ -55,19 +58,20 @@ const downloadPizzy = async () => {
|
||||
// Název
|
||||
const name = $('.produkt > h2', pizzaHtml).first().text()
|
||||
// Přísady
|
||||
const ingredients = []
|
||||
const ingredients: string[] = []
|
||||
const ingredientsHtml = $('.prisady > li', pizzaHtml);
|
||||
ingredientsHtml.each((i, elm) => {
|
||||
ingredients.push($(elm).text());
|
||||
})
|
||||
// Velikosti
|
||||
const sizes = [];
|
||||
const sizes: PizzaSize[] = [];
|
||||
const a = $('.varianty > li > a', pizzaHtml);
|
||||
a.each((i, elm) => {
|
||||
const size = $('span', elm).text();
|
||||
const priceKc = $(elm).text().split(size).pop().trim();
|
||||
const price = Number.parseInt(priceKc.split(" Kč")[0]);
|
||||
sizes.push({ size: size, pizzaPrice: price, boxPrice: boxPrices[size], price: price + boxPrices[size] });
|
||||
// TODO nedoděláno
|
||||
// const size = $('span', elm).text();
|
||||
// const priceKc = $(elm).text().split(size).pop().trim();
|
||||
// const price = Number.parseInt(priceKc.split(" Kč")[0]);
|
||||
// sizes.push({ size: size, pizzaPrice: price, boxPrice: boxPrices[size], price: price + boxPrices[size] });
|
||||
})
|
||||
result.push({
|
||||
name: name,
|
||||
@ -101,6 +105,15 @@ export const fetchPizzy = async () => {
|
||||
console.log(`Zapsán ${dataPath}`);
|
||||
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 { Server } from "socket.io";
|
||||
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 server = require("http").createServer(app);
|
||||
@ -13,12 +15,25 @@ const io = new Server(server, {
|
||||
|
||||
// Body-parser middleware for parsing JSON
|
||||
app.use(bodyParser.json());
|
||||
// app.use(express.json());
|
||||
|
||||
const cors = require('cors');
|
||||
app.use(cors({
|
||||
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) => {
|
||||
fetchPizzy().then(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) => {
|
||||
const { username, message } = req.body;
|
||||
|
||||
@ -49,6 +88,7 @@ io.on("connection", (socket) => {
|
||||
io.emit("message", message);
|
||||
});
|
||||
|
||||
// TODO smazat
|
||||
socket.on("jduKafe", ({ username, timeString }) => {
|
||||
console.log(`Received message: ${username}`);
|
||||
socket.broadcast.emit("jduKafe", `${timeString}: ${username} -> jdu Kafe`);
|
||||
@ -60,7 +100,8 @@ io.on("connection", (socket) => {
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
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": {
|
||||
"target": "ES2016",
|
||||
"module": "CommonJS",
|
||||
"jsx": "react",
|
||||
"esModuleInterop": 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';
|