import React, { useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket'; import { addPizza, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, lockPizzaDay, removePizza, unlockPizzaDay, updatePizzaDayNote } from './api/PizzaDayApi'; import { useAuth } from './context/auth'; import Login from './Login'; import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap'; import Header from './components/Header'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import PizzaOrderList from './components/PizzaOrderList'; import SelectSearch, { SelectedOptionValue } from 'react-select-search'; import 'react-select-search/style.css'; import './App.scss'; import { SelectSearchOption } from 'react-select-search'; import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { useSettings } from './context/settings'; import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime } from './types'; import Footer from './components/Footer'; import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import Loader from './components/Loader'; import { getData, errorHandler, getQrUrl } from './api/Api'; import { addChoice, removeChoices, removeChoice, changeDepartureTime, jdemeObed, updateNote } from './api/FoodApi'; import { getHumanDateTime } from './Utils'; import NoteModal from './components/modals/NoteModal'; import { useEasterEgg } from './context/eggs'; import { getImage } from './api/EasterEggApi'; const EVENT_CONNECT = "connect" // Fixní styl pro všechny easter egg obrázky const EASTER_EGG_STYLE = { zIndex: 1, animationName: "bounce-in", animationTimingFunction: "ease" } // Výchozí doba trvání animace v sekundách, pokud není přetíženo v konfiguračním JSONu const EASTER_EGG_DEFAULT_DURATION = 0.75; function App() { const auth = useAuth(); const settings = useSettings(); const [easterEgg, easterEggLoading] = useEasterEgg(auth); const [isConnected, setIsConnected] = useState(false); const [data, setData] = useState(); const [food, setFood] = useState<{ [key in Restaurants]?: DayMenu }>(); const [myOrder, setMyOrder] = useState(); const [foodChoiceList, setFoodChoiceList] = useState(); const [closed, setClosed] = useState(false); const socket = useContext(SocketContext); const choiceRef = useRef(null); const foodChoiceRef = useRef(null); const departureChoiceRef = useRef(null); const pizzaPoznamkaRef = useRef(null); const [failure, setFailure] = useState(false); const [dayIndex, setDayIndex] = useState(); const [loadingPizzaDay, setLoadingPizzaDay] = useState(false); const [noteModalOpen, setNoteModalOpen] = useState(false); const [eggImage, setEggImage] = useState(); const eggRef = useRef(null); // Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu // https://medium.com/@kishorkrishna/cant-access-latest-state-inside-socket-io-listener-heres-how-to-fix-it-1522a5abebdb const dayIndexRef = useRef(dayIndex); // Načtení dat po přihlášení useEffect(() => { if (!auth || !auth.login) { return } getData().then((data: ClientData) => { setData(data); setDayIndex(data.weekIndex); dayIndexRef.current = data.weekIndex; setFood(data.menus); }).catch(e => { setFailure(true); }) }, [auth, auth?.login]); // Přenačtení pro zvolený den useEffect(() => { if (!auth || !auth.login) { return } getData(dayIndex).then((data: ClientData) => { setData(data); setFood(data.menus); }).catch(e => { setFailure(true); }) }, [dayIndex, auth]); // 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) => { // console.log("Přijata nová data ze socketu", newData); // Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený if (dayIndexRef.current == null || newData.weekIndex === dayIndexRef.current) { setData(newData); } }); return () => { socket.off(EVENT_CONNECT); socket.off(EVENT_DISCONNECT); socket.off(EVENT_MESSAGE); } }, [socket]); useEffect(() => { if (!auth || !auth.login) { return } // TODO tohle občas náhodně nezafunguje, nutno přepsat, viz https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780 // TODO nutno opravit // if (data?.choices && choiceRef.current) { // for (let entry of Object.entries(data.choices)) { // if (entry[1].includes(auth.login)) { // const value = entry[0] as any as number; // TODO tohle je absurdní // choiceRef.current.value = Object.values(Locations)[value]; // } // } // } }, [auth, auth?.login, data?.choices]) // Reference na mojí objednávku useEffect(() => { if (data?.pizzaDay?.orders) { const myOrder = data.pizzaDay.orders.find(o => o.customer === auth?.login); setMyOrder(myOrder); } }, [auth?.login, data?.pizzaDay?.orders]) useEffect(() => { if (choiceRef?.current?.value && choiceRef.current.value !== "") { // TODO: wtf, cos pil, když jsi tohle psal? const key = choiceRef?.current?.value; const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations); const locationsKey = Object.keys(Locations)[locationIndex]; const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); if (restaurantKey > -1 && food) { const restaurant = Object.values(Restaurants)[restaurantKey]; setFoodChoiceList(food[restaurant]?.food); setClosed(food[restaurant]?.closed ?? false); } else { setFoodChoiceList(undefined); setClosed(false); } } else { setFoodChoiceList(undefined); setClosed(false); } }, [choiceRef.current?.value, food]) // Navigace mezi dny pomocí klávesových šípek const handleKeyDown = useCallback((e: any) => { if (e.keyCode === 37 && dayIndex != null && dayIndex > 0) { handleDayChange(dayIndex - 1); } else if (e.keyCode === 39 && dayIndex != null && dayIndex < 4) { handleDayChange(dayIndex + 1); } }, [dayIndex]); useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); } }, [handleKeyDown]); // Stažení a nastavení easter egg obrázku useEffect(() => { if (auth?.login && easterEgg?.url && !eggImage) { getImage(easterEgg.url).then(data => { if (data) { setEggImage(data); // Smazání obrázku z DOMu po animaci setTimeout(() => { if (eggRef?.current) { eggRef.current.remove(); } }, (easterEgg.duration || EASTER_EGG_DEFAULT_DURATION) * 1000); } }); } }, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]); const doAddChoice = async (event: React.ChangeEvent) => { const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations); if (auth?.login) { await errorHandler(() => addChoice(index, undefined, dayIndex)); if (foodChoiceRef.current?.value) { foodChoiceRef.current.value = ""; } } } const doJdemeObed = async () => { if (auth?.login) { await jdemeObed(); } } const doAddFoodChoice = async (event: React.ChangeEvent) => { if (event.target.value && foodChoiceList?.length && choiceRef.current?.value) { const restaurantKey = choiceRef.current.value; if (auth?.login) { const locationIndex = Object.keys(Locations).indexOf(restaurantKey as unknown as Locations); await errorHandler(() => addChoice(locationIndex, Number(event.target.value), dayIndex)); } } } const doRemoveChoices = async (locationKey: string) => { if (auth?.login) { await errorHandler(() => removeChoices(Number(locationKey), dayIndex)); // Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo if (choiceRef?.current?.value) { choiceRef.current.value = ""; } if (foodChoiceRef?.current?.value) { foodChoiceRef.current.value = ""; } } } const doRemoveFoodChoice = async (locationKey: string, foodIndex: number) => { if (auth?.login) { await errorHandler(() => removeChoice(Number(locationKey), foodIndex, dayIndex)); if (choiceRef?.current?.value) { choiceRef.current.value = ""; } if (foodChoiceRef?.current?.value) { foodChoiceRef.current.value = ""; } } } const saveNote = async (note?: string) => { if (auth?.login) { await errorHandler(() => updateNote(note, dayIndex)); setNoteModalOpen(false); } } const pizzaSuggestions = useMemo(() => { if (!data?.pizzaList) { return []; } const suggestions: SelectSearchOption[] = []; data.pizzaList.forEach((pizza, index) => { const group: SelectSearchOption = { name: pizza.name, type: "group", items: [] } pizza.sizes.forEach((size, sizeIndex) => { const name = `${size.size} (${size.price} Kč)`; const value = `${index}|${sizeIndex}`; group.items?.push({ name, value }); }) suggestions.push(group); }) return suggestions; }, [data?.pizzaList]); const handlePizzaChange = async (value: SelectedOptionValue | SelectedOptionValue[]) => { if (auth?.login && data?.pizzaList) { if (!(typeof value === 'string')) { throw Error('Nepodporovaný typ hodnoty'); } const s = value.split('|'); const pizzaIndex = Number.parseInt(s[0]); const pizzaSizeIndex = Number.parseInt(s[1]); await addPizza(pizzaIndex, pizzaSizeIndex); } } const handlePizzaDelete = async (pizzaOrder: PizzaOrder) => { await removePizza(pizzaOrder); } const handlePizzaPoznamkaChange = async () => { if (pizzaPoznamkaRef.current?.value && pizzaPoznamkaRef.current.value.length > 70) { alert("Poznámka může mít maximálně 70 znaků"); return; } updatePizzaDayNote(pizzaPoznamkaRef.current?.value); } // const addToCart = async () => { // TODO aktuálně nefunkční - nedokážeme poslat PHPSESSIONID cookie // if (data?.pizzaDay?.orders) { // for (const order of data?.pizzaDay?.orders) { // for (const pizzaOrder of order.pizzaList) { // const url = 'https://www.pizzachefie.cz/pridat.html'; // const payload = new URLSearchParams(); // payload.append('varId', pizzaOrder.varId.toString()); // await fetch(url, { // method: "POST", // mode: "no-cors", // cache: "no-cache", // credentials: "same-origin", // headers: { // 'Content-Type': 'application/x-www-form-urlencoded', // }, // body: payload, // }) // } // } // // TODO otevřít košík v nové záložce // } // } const handleChangeDepartureTime = async (event: React.ChangeEvent) => { if (foodChoiceList?.length && choiceRef.current?.value) { await changeDepartureTime(event.target.value, dayIndex); } } const handleDayChange = async (dayIndex: number) => { setDayIndex(dayIndex); dayIndexRef.current = dayIndex; if (choiceRef?.current?.value) { choiceRef.current.value = ""; } if (foodChoiceRef?.current?.value) { foodChoiceRef.current.value = ""; } if (departureChoiceRef?.current?.value) { departureChoiceRef.current.value = ""; } } const renderFoodTable = (name: string, menu: DayMenu) => { let content; if (menu?.closed) { content =

Zavřeno

} else if (menu?.food?.length > 0) { content = {menu.food.filter(f => (settings?.hideSoups ? !f.isSoup : true)).map((f: any, index: number) => )}
{f.amount} {f.name} {f.price}
} else { content =

Chyba načtení dat

} return

{name}

{menu?.lastUpdate && Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}} {content} } if (!auth || !auth.login) { return ; } if (!isConnected) { return } if (failure) { return } if (!data || !food) { return } const noOrders = data?.pizzaDay?.orders?.length === 0; const canChangeChoice = dayIndex == null || data.todayWeekIndex == null || dayIndex >= data.todayWeekIndex; const { path, url, startOffset, endOffset, duration, ...style } = easterEgg || {}; return ( <> {easterEgg && eggImage && }
{data.isWeekend ?

Užívejte víkend :)

: <> Poslední změny:
  • Vánoční atmosféra
{dayIndex != null &&
0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} />

{data.date}

handleDayChange(dayIndex + 1)} />
} {food[Restaurants.SLADOVNICKA] && renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])} {food[Restaurants.UMOTLIKU] && renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])} {food[Restaurants.TECHTOWER] && renderFoodTable('TechTower', food[Restaurants.TECHTOWER])}
{canChangeChoice && <>

{`Jak to ${dayIndex == null || dayIndex === data.todayWeekIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}

{Object.entries(Locations) .filter(entry => { // TODO: wtf, cos pil, když jsi tohle psal? v2 const key = entry[0]; const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations); const locationsKey = Object.keys(Locations)[locationIndex]; const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); const v = Object.values(Restaurants)[restaurantKey]; return v == null || !food[v]?.closed; }) .map(entry => )} Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí. {foodChoiceList && !closed && <>

Na co dobrého? (nepovinné)

{foodChoiceList.map((food, index) => )} } {foodChoiceList && !closed && <>

V kolik hodin preferuješ odchod?

{Object.values(DepartureTime).map(time => )} } } {Object.keys(data.choices).length > 0 ? {Object.keys(data.choices).map((locationKey: string) => { const locationName = Object.values(Locations)[Number(locationKey)]; const locationLoginList = Object.entries(data.choices[Number(locationKey)]); return ( ) } )}
{locationName} {locationLoginList.map((entry: [string, FoodChoices], index) => { const login = entry[0]; const userPayload = entry[1]; const userChoices = userPayload?.options; const trusted = userPayload?.trusted || false; return {userChoices?.length && food ? : null} } )}
{trusted && } {login} {userPayload.departureTime && ({userPayload.departureTime})} {userPayload.note && ({userPayload.note})} {login === auth.login && canChangeChoice && { setNoteModalOpen(true); }} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />} {login === auth.login && canChangeChoice && { doRemoveChoices(locationKey); }} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
    {userChoices?.map(foodIndex => { const locationsKey = Object.keys(Locations)[Number(locationKey)] const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); const restaurant = Object.values(Restaurants)[restaurantKey]; const foodName = food[restaurant]?.food[foodIndex].name; return
  • {foodName} {login === auth.login && canChangeChoice && { doRemoveFoodChoice(locationKey, foodIndex); }} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
  • })}
:
Zatím nikdo nehlasoval...
}
{dayIndex === data.todayWeekIndex &&
{!data.pizzaDay &&

Pro dnešní den není aktuálně založen Pizza day.

{loadingPizzaDay ? Zjišťujeme dostupné pizzy : <> }
} {data.pizzaDay &&

Pizza day

{ data.pizzaDay.state === PizzaDayState.CREATED &&

Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.
Můžete upravovat své objednávky.

{ data.pizzaDay.creator === auth.login && <> }
} { data.pizzaDay.state === PizzaDayState.LOCKED &&

Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}

{data.pizzaDay.creator === auth.login && <> {/* */} }
} { data.pizzaDay.state === PizzaDayState.ORDERED &&

Pizzy byly objednány uživatelem {data.pizzaDay.creator}

{data.pizzaDay.creator === auth.login &&
}
} { data.pizzaDay.state === PizzaDayState.DELIVERED &&

{`Pizzy byly doručeny.${myOrder?.hasQr ? ` Objednávku můžete uživateli ${data.pizzaDay.creator} uhradit pomocí QR kódu níže.` : ''}`}

}
{data.pizzaDay.state === PizzaDayState.CREATED &&
Poznámka: { if (event.key === 'Enter') { handlePizzaPoznamkaChange(); } event.stopPropagation(); }} />
} { data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&

QR platba

QR kód
}
}
}
}