Deduplikace typů a sloučení kontejnerů
- Zavedení yarn workspaces - Sloučení klienta a serveru do jednoho Docker kontejneru - Společný dockerfile, builder - Zbavení se nginx (není již potřeba)
This commit is contained in:
parent
0d0c5cb946
commit
3c0e8b2297
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Builder
|
||||||
|
FROM node:18-alpine3.18 as builder
|
||||||
|
|
||||||
|
ENV LANG cs_CZ.UTF-8
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
COPY yarn.lock .
|
||||||
|
|
||||||
|
COPY server/package.json ./server/
|
||||||
|
COPY client/package.json ./client/
|
||||||
|
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY server/tsconfig.json ./server/
|
||||||
|
COPY server/src ./server/src/
|
||||||
|
|
||||||
|
COPY client/tsconfig.json ./client/
|
||||||
|
COPY client/src ./client/src
|
||||||
|
COPY client/public ./client/public
|
||||||
|
|
||||||
|
COPY types ./types/
|
||||||
|
|
||||||
|
WORKDIR /build/server
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
WORKDIR /build/client
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Runner
|
||||||
|
FROM node:18-alpine3.18
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /build/node_modules ./node_modules
|
||||||
|
COPY --from=builder /build/server/dist ./
|
||||||
|
COPY --from=builder /build/client/build ./public
|
||||||
|
COPY /server/.env.production ./server/src
|
||||||
|
|
||||||
|
RUN echo "Test root" && ls -la
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD [ "node", "./server/src/index.js" ]
|
15
README.md
15
README.md
@ -1,13 +1,13 @@
|
|||||||
# Luncher
|
# Luncher
|
||||||
Aplikace pro profesionální management obědů.
|
Aplikace pro profesionální management obědů.
|
||||||
|
|
||||||
Aplikace sestává ze dvou (tří) modulů.
|
Aplikace sestává ze tří modulů.
|
||||||
|
- types
|
||||||
|
- společné TypeScript definice, pro objekty posílané mezi serverem a klientem
|
||||||
- server
|
- server
|
||||||
- backend psaný v [node.js](https://nodejs.dev)
|
- backend psaný v [node.js](https://nodejs.dev)
|
||||||
- client
|
- client
|
||||||
- frontend psaný v [React.js](https://react.dev)
|
- frontend psaný v [React.js](https://react.dev)
|
||||||
- [nginx](https://nginx.org)
|
|
||||||
- proxy pro snadné propojení Docker kontejnerů pod jednou URL
|
|
||||||
|
|
||||||
## Spuštění pro vývoj
|
## Spuštění pro vývoj
|
||||||
### Závislosti
|
### Závislosti
|
||||||
@ -17,7 +17,6 @@ Aplikace sestává ze dvou (tří) modulů.
|
|||||||
|
|
||||||
### Spuštění na *nix platformách
|
### Spuštění na *nix platformách
|
||||||
- Nainstalovat závislosti viz předchozí bod
|
- Nainstalovat závislosti viz předchozí bod
|
||||||
- Zkopírovat `client/.env.template` do `client/.env.development` a upravit dle potřeby
|
|
||||||
- Zkopírovat `server/.env.template` do `server/.env.development` a upravit dle potřeby
|
- Zkopírovat `server/.env.template` do `server/.env.development` a upravit dle potřeby
|
||||||
- Spustit `./run_dev.sh`. Na jiných platformách se lze inspirovat jeho obsahem, postup by měl být víceméně stejný.
|
- Spustit `./run_dev.sh`. Na jiných platformách se lze inspirovat jeho obsahem, postup by měl být víceméně stejný.
|
||||||
|
|
||||||
@ -26,9 +25,6 @@ Aplikace sestává ze dvou (tří) modulů.
|
|||||||
- [Docker](https://www.docker.com)
|
- [Docker](https://www.docker.com)
|
||||||
- [Docker Compose](https://docs.docker.com/compose)
|
- [Docker Compose](https://docs.docker.com/compose)
|
||||||
|
|
||||||
### Spuštění s nginx
|
|
||||||
- `docker compose up --build -d`
|
|
||||||
|
|
||||||
### Spuštení s traefik
|
### Spuštení s traefik
|
||||||
- `docker compose -f compose-traefik.yml up --build -d`
|
- `docker compose -f compose-traefik.yml up --build -d`
|
||||||
|
|
||||||
@ -53,8 +49,7 @@ Aplikace sestává ze dvou (tří) modulů.
|
|||||||
- [ ] Ceny krabic za pizzu jsou napevno v kódu - problém, pokud se někdy změní
|
- [ ] Ceny krabic za pizzu jsou napevno v kódu - problém, pokud se někdy změní
|
||||||
- [ ] Předvyplnění poslední vybrané hodnoty občas nefunguje, viz komentář
|
- [ ] Předvyplnění poslední vybrané hodnoty občas nefunguje, viz komentář
|
||||||
- [ ] Nasazení nové verze v Docker smaže veškerá data (protože data.json není venku)
|
- [ ] Nasazení nové verze v Docker smaže veškerá data (protože data.json není venku)
|
||||||
- [ ] Vylepšit dokumentaci projektu
|
- [x] Vylepšit dokumentaci projektu
|
||||||
- [ ] Popsat nginx
|
|
||||||
- [x] Popsat závislosti, co je nutné provést před vývojem a postup spuštění pro vývoj
|
- [x] Popsat závislosti, co je nutné provést před vývojem a postup spuštění pro vývoj
|
||||||
- [x] Popsat dostupné env
|
- [x] Popsat dostupné env
|
||||||
- [x] Přesunout autentizaci na server (JWT?)
|
- [x] Přesunout autentizaci na server (JWT?)
|
||||||
@ -67,7 +62,7 @@ Aplikace sestává ze dvou (tří) modulů.
|
|||||||
- [ ] Možnost náhledu na jiné dny v týdnu (např. pomocí šipek)
|
- [ ] Možnost náhledu na jiné dny v týdnu (např. pomocí šipek)
|
||||||
- [ ] Možnost zadat si oběd dopředu na následující dny v týdnu
|
- [ ] Možnost zadat si oběd dopředu na následující dny v týdnu
|
||||||
- [x] Zbavit se Food API, potřebnou funkcionalitu zahrnout do serveru
|
- [x] Zbavit se Food API, potřebnou funkcionalitu zahrnout do serveru
|
||||||
- [ ] Vyřešit API mezi serverem a klientem, aby nebyl v obou projektech duplicitní kód (viz types.ts a Types.tsx)
|
- [x] Vyřešit API mezi serverem a klientem, aby nebyl v obou projektech duplicitní kód (viz types.ts a Types.tsx)
|
||||||
- [ ] Mazat z databáze předchozí dny, aktuálně je to k ničemu
|
- [ ] Mazat z databáze předchozí dny, aktuálně je to k ničemu
|
||||||
- [ ] Vylepšit parsery restaurací
|
- [ ] Vylepšit parsery restaurací
|
||||||
- [ ] Sladovnická
|
- [ ] Sladovnická
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
**/node_modules
|
|
||||||
**/npm-debug.log
|
|
||||||
build
|
|
@ -1,3 +0,0 @@
|
|||||||
# Veřejná URL, na které bude dostupný klient (typicky přes proxy).
|
|
||||||
# Pro vývoj není potřeba, bude použita výchozí hodnota http://127.0.0.1:3001
|
|
||||||
# PUBLIC_URL=http://example:3001
|
|
@ -1,23 +0,0 @@
|
|||||||
FROM node:18-alpine3.18 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:18-alpine3.18
|
|
||||||
ENV NODE_ENV production
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /build .
|
|
||||||
EXPOSE 3000
|
|
||||||
RUN yarn global add serve && yarn
|
|
||||||
CMD ["serve", "-s", "."]
|
|
@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
docker build -t luncher-client .
|
|
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "luncher-client",
|
"name": "@luncher/client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"homepage": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
@ -29,8 +30,9 @@
|
|||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"copy-types": "cp -r ../types ./src",
|
||||||
"build": "react-scripts build",
|
"start": "yarn copy-types && react-scripts start",
|
||||||
|
"build": "yarn copy-types && react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { PizzaOrder } from "./Types";
|
import { PizzaOrder } from "./types";
|
||||||
import { getBaseUrl, getToken } from "./Utils";
|
import { getBaseUrl, getToken } from "./Utils";
|
||||||
|
|
||||||
async function request<TResponse>(
|
async function request<TResponse>(
|
||||||
url: string,
|
url: string,
|
||||||
config: RequestInit = {}
|
config: RequestInit = {}
|
||||||
): Promise<TResponse> {
|
): Promise<TResponse> {
|
||||||
if (!config.headers) {
|
config.headers = config?.headers ? new Headers(config.headers) : new Headers();
|
||||||
config.headers = {};
|
config.headers.set("Authorization", `Bearer ${getToken()}`);
|
||||||
}
|
|
||||||
config.headers["Authorization"] = `Bearer ${getToken()}`;
|
|
||||||
return fetch(getBaseUrl() + url, config).then(response => {
|
return fetch(getBaseUrl() + url, config).then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
@ -58,7 +56,7 @@ export const finishOrder = async () => {
|
|||||||
return await api.post<any, any>('/api/finishOrder', undefined);
|
return await api.post<any, any>('/api/finishOrder', undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const finishDelivery = async (bankAccount, bankAccountHolder) => {
|
export const finishDelivery = async (bankAccount?: string, bankAccountHolder?: string) => {
|
||||||
return await api.post<any, any>('/api/finishDelivery', JSON.stringify({ bankAccount, bankAccountHolder }));
|
return await api.post<any, any>('/api/finishDelivery', JSON.stringify({ bankAccount, bankAccountHolder }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,17 +4,17 @@ import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket
|
|||||||
import { addPizza, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getFood, getPizzy, getQrUrl, lockPizzaDay, removePizza, unlockPizzaDay, updateChoice, updateNote } from './Api';
|
import { addPizza, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getFood, getPizzy, getQrUrl, lockPizzaDay, removePizza, unlockPizzaDay, updateChoice, updateNote } from './Api';
|
||||||
import { useAuth } from './context/auth';
|
import { useAuth } from './context/auth';
|
||||||
import Login from './Login';
|
import Login from './Login';
|
||||||
import { Locations, ClientData, Pizza, PizzaOrder, State, Order, Food, Restaurants } from './Types';
|
|
||||||
import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
|
import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import PizzaOrderList from './components/PizzaOrderList';
|
import PizzaOrderList from './components/PizzaOrderList';
|
||||||
import SelectSearch from 'react-select-search';
|
import SelectSearch, { SelectedOptionValue } from 'react-select-search';
|
||||||
import 'react-select-search/style.css';
|
import 'react-select-search/style.css';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { SelectSearchOption } from 'react-select-search';
|
import { SelectSearchOption } from 'react-select-search';
|
||||||
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { useBank } from './context/bank';
|
import { useBank } from './context/bank';
|
||||||
|
import { ClientData, Restaurants, Food, Pizza, Order, Locations, PizzaOrder, PizzaDayState } from './types';
|
||||||
|
|
||||||
|
|
||||||
const EVENT_CONNECT = "connect"
|
const EVENT_CONNECT = "connect"
|
||||||
@ -77,7 +77,8 @@ function App() {
|
|||||||
if (data?.choices && choiceRef.current) {
|
if (data?.choices && choiceRef.current) {
|
||||||
for (let entry of Object.entries(data.choices)) {
|
for (let entry of Object.entries(data.choices)) {
|
||||||
if (entry[1].includes(auth.login)) {
|
if (entry[1].includes(auth.login)) {
|
||||||
choiceRef.current.value = Object.values(Locations)[entry[0]]
|
const value = entry[0] as any as number; // TODO tohle je absurdní
|
||||||
|
choiceRef.current.value = Object.values(Locations)[value];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,8 +125,11 @@ function App() {
|
|||||||
return suggestions;
|
return suggestions;
|
||||||
}, [pizzy]);
|
}, [pizzy]);
|
||||||
|
|
||||||
const handlePizzaChange = async (value) => {
|
const handlePizzaChange = async (value: SelectedOptionValue | SelectedOptionValue[]) => {
|
||||||
if (auth?.login && pizzy) {
|
if (auth?.login && pizzy) {
|
||||||
|
if (!(typeof value === 'string')) {
|
||||||
|
throw Error('Nepodporovaný typ hodnoty');
|
||||||
|
}
|
||||||
const s = value.split('|');
|
const s = value.split('|');
|
||||||
const pizzaIndex = Number.parseInt(s[0]);
|
const pizzaIndex = Number.parseInt(s[0]);
|
||||||
const pizzaSizeIndex = Number.parseInt(s[1]);
|
const pizzaSizeIndex = Number.parseInt(s[1]);
|
||||||
@ -169,7 +173,7 @@ function App() {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const renderFoodTable = (name, food) => {
|
const renderFoodTable = (name: string, food: Food[]) => {
|
||||||
return <Col md={12} lg={4}>
|
return <Col md={12} lg={4}>
|
||||||
<h3>{name}</h3>
|
<h3>{name}</h3>
|
||||||
<Table striped bordered hover>
|
<Table striped bordered hover>
|
||||||
@ -268,7 +272,7 @@ function App() {
|
|||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<h3>Pizza day</h3>
|
<h3>Pizza day</h3>
|
||||||
{
|
{
|
||||||
data.pizzaDay.state === State.CREATED &&
|
data.pizzaDay.state === PizzaDayState.CREATED &&
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
|
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
|
||||||
@ -288,7 +292,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
data.pizzaDay.state === State.LOCKED &&
|
data.pizzaDay.state === PizzaDayState.LOCKED &&
|
||||||
<div>
|
<div>
|
||||||
<p>Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}</p>
|
<p>Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}</p>
|
||||||
{data.pizzaDay.creator === auth.login &&
|
{data.pizzaDay.creator === auth.login &&
|
||||||
@ -307,7 +311,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
data.pizzaDay.state === State.ORDERED &&
|
data.pizzaDay.state === PizzaDayState.ORDERED &&
|
||||||
<div>
|
<div>
|
||||||
<p>Pizzy byly objednány uživatelem {data.pizzaDay.creator}</p>
|
<p>Pizzy byly objednány uživatelem {data.pizzaDay.creator}</p>
|
||||||
{data.pizzaDay.creator === auth.login &&
|
{data.pizzaDay.creator === auth.login &&
|
||||||
@ -323,13 +327,13 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
data.pizzaDay.state === State.DELIVERED &&
|
data.pizzaDay.state === PizzaDayState.DELIVERED &&
|
||||||
<div>
|
<div>
|
||||||
<p>Pizzy byly doručeny. Objednávku můžete uhradit pomocí QR kódu níže.</p>
|
<p>Pizzy byly doručeny. Objednávku můžete uhradit pomocí QR kódu níže.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{data.pizzaDay.state === State.CREATED &&
|
{data.pizzaDay.state === PizzaDayState.CREATED &&
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<SelectSearch
|
<SelectSearch
|
||||||
search={true}
|
search={true}
|
||||||
@ -352,7 +356,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} />
|
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} />
|
||||||
{
|
{
|
||||||
data.pizzaDay.state === State.DELIVERED && myOrder?.hasQr &&
|
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
|
||||||
<div className='qr-code'>
|
<div className='qr-code'>
|
||||||
<h3>QR platba</h3>
|
<h3>QR platba</h3>
|
||||||
<div>Částka: {myOrder.totalPrice} Kč</div>
|
<div>Částka: {myOrder.totalPrice} Kč</div>
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
// TODO všechno v tomto souboru jsou duplicity se serverem, ale aktuálně nevím jaký je nejlepší způsob jejich sdílení
|
|
||||||
|
|
||||||
export interface PizzaSize {
|
|
||||||
varId: number, // unikátní ID varianty pizzy
|
|
||||||
size: string, // velikost pizzy, např. "30cm"
|
|
||||||
pizzaPrice: number, // cena samotné pizzy
|
|
||||||
boxPrice: number, // cena krabice
|
|
||||||
price: number, // celková cena (pizza + krabice)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Jedna konkrétní pizza */
|
|
||||||
export interface Pizza {
|
|
||||||
name: string, // název pizzy
|
|
||||||
ingredients: string[], // seznam ingrediencí
|
|
||||||
sizes: PizzaSize[], // dostupné velikosti pizzy
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Objednávka jedné konkrétní pizzy */
|
|
||||||
export interface PizzaOrder {
|
|
||||||
varId: number, // unikátní ID varianty pizzy
|
|
||||||
name: string, // název pizzy
|
|
||||||
size: string, // velikost pizzy jako string (30cm)
|
|
||||||
price: number, // cena pizzy v Kč, včetně krabice
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Celková objednávka jednoho člověka */
|
|
||||||
export interface Order {
|
|
||||||
customer: string, // jméno objednatele
|
|
||||||
pizzaList: PizzaOrder[], // seznam objednaných pizz
|
|
||||||
totalPrice: number, // celková cena všech objednaných pizz a krabic
|
|
||||||
hasQr?: boolean, // zda je pro objednávku vygenerován QR kód pro platbu
|
|
||||||
note?: string, // volitelná poznámka uživatele k objednávce
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Choices {
|
|
||||||
[location: string]: string[],
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Údaje o Pizza day. */
|
|
||||||
export interface PizzaDay {
|
|
||||||
state: State,
|
|
||||||
creator: string,
|
|
||||||
orders: Order[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientData {
|
|
||||||
date: string, // dnešní datum pro zobrazení
|
|
||||||
isWeekend: boolean, // příznak zda je dnešní den víkend
|
|
||||||
choices: Choices, // seznam voleb
|
|
||||||
pizzaDay?: PizzaDay, // údaje o pizza day, pokud je pro dnešek založen
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Locations {
|
|
||||||
SLADOVNICKA = 'Sladovnická',
|
|
||||||
UMOTLIKU = 'U Motlíků',
|
|
||||||
TECHTOWER = 'TechTower',
|
|
||||||
SPSE = 'SPŠE',
|
|
||||||
PIZZA = 'Pizza day',
|
|
||||||
OBJEDNAVAM = 'Budu objednávat',
|
|
||||||
NEOBEDVAM = 'Mám vlastní/neobědvám',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Jídlo z obědového menu restaurace. */
|
|
||||||
export interface Food {
|
|
||||||
amount?: string, // množství standardní porce, např. 0,33l nebo 150g
|
|
||||||
name: string, // název/popis jídla
|
|
||||||
price: string, // cena ve formátu '135 Kč'
|
|
||||||
isSoup: boolean, // příznak, zda se jedná o polévku
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Výčtový typ pro restaurace, pro které umíme získat a parsovat obědové menu. */
|
|
||||||
export enum Restaurants {
|
|
||||||
SLADOVNICKA = 'sladovnicka',
|
|
||||||
UMOTLIKU = 'uMotliku',
|
|
||||||
TECHTOWER = 'techTower',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum State {
|
|
||||||
NOT_CREATED, // Pizza day nebyl založen
|
|
||||||
CREATED, // Pizza day je založen
|
|
||||||
LOCKED, // Objednávky uzamčeny
|
|
||||||
ORDERED, // Objednáno
|
|
||||||
DELIVERED, // Doručeno
|
|
||||||
}
|
|
@ -19,7 +19,7 @@ export default function Header() {
|
|||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidInteger = (str) => {
|
const isValidInteger = (str: string) => {
|
||||||
str = str.trim();
|
str = str.trim();
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return false;
|
return false;
|
||||||
@ -66,7 +66,7 @@ export default function Header() {
|
|||||||
if (sum % 11 !== 0) {
|
if (sum % 11 !== 0) {
|
||||||
throw Error("Číslo účtu je neplatné")
|
throw Error("Číslo účtu je neplatné")
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
alert(e.message)
|
alert(e.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Table } from "react-bootstrap";
|
import { Table } from "react-bootstrap";
|
||||||
import { Order, PizzaOrder, State } from "../Types";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
|
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { useAuth } from "../context/auth";
|
import { useAuth } from "../context/auth";
|
||||||
|
import { Order, PizzaDayState, PizzaOrder } from "../types";
|
||||||
|
|
||||||
export default function PizzaOrderList({ state, orders, onDelete }: { state: State, orders: Order[], onDelete: (pizzaOrder: PizzaOrder) => void }) {
|
export default function PizzaOrderList({ state, orders, onDelete }: { state: PizzaDayState, orders: Order[], onDelete: (pizzaOrder: PizzaOrder) => void }) {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|
||||||
if (!orders?.length) {
|
if (!orders?.length) {
|
||||||
@ -29,7 +29,7 @@ export default function PizzaOrderList({ state, orders, onDelete }: { state: Sta
|
|||||||
<td>{order.pizzaList.map<React.ReactNode>((pizzaOrder, index) =>
|
<td>{order.pizzaList.map<React.ReactNode>((pizzaOrder, index) =>
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
|
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
|
||||||
{auth?.login === order.customer && state === State.CREATED &&
|
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
|
||||||
<FontAwesomeIcon onClick={() => {
|
<FontAwesomeIcon onClick={() => {
|
||||||
onDelete(pizzaOrder);
|
onDelete(pizzaOrder);
|
||||||
}} title='Odstranit' className='trash-icon' icon={faTrashCan} />
|
}} title='Odstranit' className='trash-icon' icon={faTrashCan} />
|
||||||
|
@ -19,8 +19,5 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
}
|
||||||
"include": [
|
|
||||||
"client/src"
|
|
||||||
]
|
|
||||||
}
|
}
|
@ -1,18 +1,9 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
server:
|
luncher:
|
||||||
build:
|
|
||||||
context: ./server
|
|
||||||
client:
|
|
||||||
build:
|
|
||||||
context: ./client
|
|
||||||
nginx:
|
|
||||||
depends_on:
|
|
||||||
- server
|
|
||||||
- client
|
|
||||||
restart: always
|
restart: always
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./
|
||||||
ports:
|
ports:
|
||||||
- 3005:80
|
- 3001:3001
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
FROM nginx
|
|
||||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
|
@ -1,37 +0,0 @@
|
|||||||
upstream client {
|
|
||||||
server client:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream server {
|
|
||||||
server server:3001;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
proxy_pass http://server;
|
|
||||||
}
|
|
||||||
}
|
|
8
package.json
Normal file
8
package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"client",
|
||||||
|
"server",
|
||||||
|
"types"
|
||||||
|
]
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
export NODE_ENV=development
|
export NODE_ENV=development
|
||||||
cd server && yarn install && yarn start &
|
yarn install
|
||||||
cd client && yarn install && yarn start &
|
cd server && yarn start &
|
||||||
|
cd client && yarn start &
|
||||||
wait
|
wait
|
@ -1,18 +0,0 @@
|
|||||||
FROM node:18-alpine3.18
|
|
||||||
|
|
||||||
ENV LANG cs_CZ.UTF-8
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json .
|
|
||||||
COPY yarn.lock .
|
|
||||||
COPY .env.production .
|
|
||||||
COPY tsconfig.json .
|
|
||||||
COPY src ./src
|
|
||||||
|
|
||||||
RUN yarn install --frozen-lockfile
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
EXPOSE 3001
|
|
||||||
|
|
||||||
CMD [ "node", "/app/dist/index.js" ]
|
|
@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
yarn install --frozen-lockfile && yarn build
|
|
||||||
docker build -t luncher-server .
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "luncher-server",
|
"name": "@luncher/server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -8,11 +8,16 @@ import dotenv from 'dotenv';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
|
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
|
||||||
import { getQr } from "./qr";
|
import { getQr } from "./qr";
|
||||||
import { Restaurants } from "./types";
|
|
||||||
import { generateToken, getLogin, verify } from "./auth";
|
import { generateToken, getLogin, verify } from "./auth";
|
||||||
|
import { Restaurants } from "../../types";
|
||||||
|
|
||||||
const ENVIRONMENT = process.env.NODE_ENV || 'production'
|
const ENVIRONMENT = process.env.NODE_ENV || 'production';
|
||||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) });
|
||||||
|
|
||||||
|
// Validace nastavení JWT tokenu - nemá bez něj smysl vůbec povolit server spustit
|
||||||
|
if (!process.env.JWT_SECRET) {
|
||||||
|
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = require("http").createServer(app);
|
const server = require("http").createServer(app);
|
||||||
@ -30,6 +35,8 @@ app.use(cors({
|
|||||||
origin: '*'
|
origin: '*'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
app.use(express.static('public'))
|
||||||
|
|
||||||
const parseToken = (req: any) => {
|
const parseToken = (req: any) => {
|
||||||
if (req?.headers?.authorization) {
|
if (req?.headers?.authorization) {
|
||||||
return req.headers.authorization.split(' ')[1];
|
return req.headers.authorization.split(' ')[1];
|
||||||
@ -95,7 +102,7 @@ app.get("/api/data", (req, res) => {
|
|||||||
|
|
||||||
/** Vrátí obědové menu pro dostupné podniky. */
|
/** Vrátí obědové menu pro dostupné podniky. */
|
||||||
app.get("/api/food", async (req, res) => {
|
app.get("/api/food", async (req, res) => {
|
||||||
const mock = !!req.query?.mock;
|
const mock = !!process.env.MOCK_DATA;
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const data = {
|
const data = {
|
||||||
[Restaurants.SLADOVNICKA]: await getMenuSladovnicka(date, mock),
|
[Restaurants.SLADOVNICKA]: await getMenuSladovnicka(date, mock),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** Notifikace pro gotify*/
|
/** Notifikace pro gotify*/
|
||||||
import axios, { AxiosError, AxiosResponse } from 'axios';
|
import { GotifyServer, NotififaceInput, NotifikaceData } from '../../types';
|
||||||
import { GotifyServer, NotififaceInput, NotifikaceData, UdalostEnum } from "./types";
|
import axios, { AxiosError } from 'axios';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
import { formatDate } from "./utils";
|
import { formatDate } from "./utils";
|
||||||
import { Food } from "./types";
|
import { Food } from "../../types";
|
||||||
|
|
||||||
// Fráze v názvech jídel, které naznačují že se jedná o polévku
|
// Fráze v názvech jídel, které naznačují že se jedná o polévku
|
||||||
const SOUP_NAMES = ['polévka', 'česnečka', 'česnekový krém', 'cibulačka', 'vývar']
|
const SOUP_NAMES = ['polévka', 'česnečka', 'česnekový krém', 'cibulačka', 'vývar']
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ClientData, Locations, Order, Pizza, PizzaDayState, PizzaOrder, PizzaSize, UdalostEnum } from "./types";
|
|
||||||
import { db } from "./database";
|
import { db } from "./database";
|
||||||
import { formatDate, getHumanDate, getIsWeekend } from "./utils";
|
import { formatDate, getHumanDate, getIsWeekend } from "./utils";
|
||||||
import { callNotifikace } from "./notifikace";
|
import { callNotifikace } from "./notifikace";
|
||||||
import { generateQr } from "./qr";
|
import { generateQr } from "./qr";
|
||||||
|
import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, Locations } from "../../types";
|
||||||
|
|
||||||
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
|
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
|
||||||
function getToday(): Date {
|
function getToday(): Date {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"../types/**/*"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2016",
|
"target": "ES2016",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
@ -6,7 +10,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "../",
|
||||||
"strict": true,
|
"strict": true
|
||||||
}
|
}
|
||||||
}
|
}
|
1194
server/yarn.lock
1194
server/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,9 @@
|
|||||||
import exp from "constants";
|
/** Výčtový typ pro restaurace, pro které umíme získat a parsovat obědové menu. */
|
||||||
|
export enum Restaurants {
|
||||||
|
SLADOVNICKA = 'sladovnicka',
|
||||||
|
UMOTLIKU = 'uMotliku',
|
||||||
|
TECHTOWER = 'techTower',
|
||||||
|
}
|
||||||
|
|
||||||
export interface Choices {
|
export interface Choices {
|
||||||
[location: string]: string[],
|
[location: string]: string[],
|
||||||
@ -69,13 +74,6 @@ export interface Food {
|
|||||||
isSoup: boolean, // příznak, zda se jedná o polévku
|
isSoup: boolean, // příznak, zda se jedná o polévku
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Výčtový typ pro restaurace, pro které umíme získat a parsovat obědové menu. */
|
|
||||||
export enum Restaurants {
|
|
||||||
SLADOVNICKA = 'sladovnicka',
|
|
||||||
UMOTLIKU = 'uMotliku',
|
|
||||||
TECHTOWER = 'techTower',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Locations {
|
export enum Locations {
|
||||||
SLADOVNICKA = 'Sladovnická',
|
SLADOVNICKA = 'Sladovnická',
|
||||||
UMOTLIKU = 'U Motlíků',
|
UMOTLIKU = 'U Motlíků',
|
1
types/index.ts
Normal file
1
types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Types';
|
15
types/tsconfig.json
Normal file
15
types/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
// "emitDeclarationOnly": true,
|
||||||
|
// "outDir": "./dist",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"index.ts",
|
||||||
|
"Types.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user