Podpora easter eggů
This commit is contained in:
@@ -123,4 +123,46 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
left: var(--start-left);
|
||||
right: var(--start-right);
|
||||
top: var(--start-top);
|
||||
bottom: var(--start-bottom);
|
||||
}
|
||||
|
||||
25% {
|
||||
left: var(--end-left);
|
||||
right: var(--end-right);
|
||||
top: var(--end-top);
|
||||
bottom: var(--end-bottom);
|
||||
}
|
||||
|
||||
50% {
|
||||
left: var(--start-left);
|
||||
right: var(--start-right);
|
||||
top: var(--start-top);
|
||||
bottom: var(--start-bottom);
|
||||
}
|
||||
|
||||
75% {
|
||||
left: var(--end-left);
|
||||
right: var(--end-right);
|
||||
top: var(--end-top);
|
||||
bottom: var(--end-bottom);
|
||||
}
|
||||
|
||||
100% {
|
||||
left: var(--start-left);
|
||||
right: var(--start-right);
|
||||
top: var(--start-top);
|
||||
bottom: var(--start-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO zjistit, zda to nedokážeme lépe - tohle je kvůli overflow easter egg obrázků, ale skrývá to úplně scrollbar
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ 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.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';
|
||||
@@ -22,12 +22,25 @@ 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<boolean>(false);
|
||||
const [data, setData] = useState<ClientData>();
|
||||
const [food, setFood] = useState<{ [key in Restaurants]?: DayMenu }>();
|
||||
@@ -43,6 +56,8 @@ function App() {
|
||||
const [dayIndex, setDayIndex] = useState<number>();
|
||||
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
|
||||
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
|
||||
const [eggImage, setEggImage] = useState<Blob>();
|
||||
const eggRef = useRef<HTMLImageElement>(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<number | undefined>(dayIndex);
|
||||
@@ -161,6 +176,23 @@ function App() {
|
||||
}
|
||||
}, [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<HTMLSelectElement>) => {
|
||||
const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations);
|
||||
if (auth?.login) {
|
||||
@@ -361,8 +393,11 @@ function App() {
|
||||
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 && <img ref={eggRef} alt='' src={URL.createObjectURL(eggImage)} style={{ position: 'absolute', ...EASTER_EGG_STYLE, ...style, animationDuration: `${duration ?? EASTER_EGG_DEFAULT_DURATION}s` }} />}
|
||||
<Header />
|
||||
<div className='wrapper'>
|
||||
{data.isWeekend ? <h4>Užívejte víkend :)</h4> : <>
|
||||
|
||||
@@ -25,11 +25,36 @@ async function request<TResponse>(
|
||||
try {
|
||||
const response = await fetch(getBaseUrl() + url, config);
|
||||
if (!response.ok) {
|
||||
// TODO tohle je blbě, jelikož automaticky očekáváme, že v případě chyby přijde vždy JSON, což není pravda
|
||||
const json = await response.json();
|
||||
// Vyhodíme samotnou hlášku z odpovědi, odchytí si jí errorHandler
|
||||
throw new Error(json.error);
|
||||
}
|
||||
return response.json() as TResponse;
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
||||
return response.json() as TResponse;
|
||||
} else {
|
||||
return response.text() as TResponse;
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function blobRequest(
|
||||
url: string,
|
||||
config: RequestInit = {}
|
||||
): Promise<Blob> {
|
||||
config.headers = config?.headers ? new Headers(config.headers) : new Headers();
|
||||
config.headers.set("Authorization", `Bearer ${getToken()}`);
|
||||
try {
|
||||
const response = await fetch(getBaseUrl() + url, config);
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
// Vyhodíme samotnou hlášku z odpovědi, odchytí si jí errorHandler
|
||||
throw new Error(json.error);
|
||||
}
|
||||
return response.blob()
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
@@ -37,6 +62,7 @@ async function request<TResponse>(
|
||||
|
||||
export const api = {
|
||||
get: <TResponse>(url: string) => request<TResponse>(url),
|
||||
blobGet: (url: string) => blobRequest(url),
|
||||
post: <TBody extends BodyInit, TResponse>(url: string, body: TBody) => request<TResponse>(url, { method: 'POST', body, headers: { 'Content-Type': 'application/json' } }),
|
||||
}
|
||||
|
||||
|
||||
12
client/src/api/EasterEggApi.ts
Normal file
12
client/src/api/EasterEggApi.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { EasterEgg } from "../types";
|
||||
import { api } from "./Api";
|
||||
|
||||
const EASTER_EGGS_API_PREFIX = '/api/easterEggs';
|
||||
|
||||
export const getEasterEgg = async (): Promise<EasterEgg | undefined> => {
|
||||
return await api.get(`${EASTER_EGGS_API_PREFIX}`);
|
||||
}
|
||||
|
||||
export const getImage = async (url: string) => {
|
||||
return await api.blobGet(`${EASTER_EGGS_API_PREFIX}/${url}`);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { deleteToken, getToken, storeToken } from "../Utils";
|
||||
|
||||
export type AuthContextProps = {
|
||||
login?: string,
|
||||
trusted?: boolean,
|
||||
setToken: (name: string) => void,
|
||||
logout: () => void,
|
||||
}
|
||||
@@ -26,6 +27,7 @@ export const useAuth = () => {
|
||||
|
||||
function useProvideAuth(): AuthContextProps {
|
||||
const [loginName, setLoginName] = useState<string | undefined>();
|
||||
const [trusted, setTrusted] = useState<boolean | undefined>();
|
||||
const [token, setToken] = useState<string | null>(getToken());
|
||||
const { decodedToken } = useJwt(token || '');
|
||||
|
||||
@@ -40,8 +42,10 @@ function useProvideAuth(): AuthContextProps {
|
||||
useEffect(() => {
|
||||
if (decodedToken) {
|
||||
setLoginName((decodedToken as any).login);
|
||||
setTrusted((decodedToken as any).trusted);
|
||||
} else {
|
||||
setLoginName(undefined);
|
||||
setTrusted(undefined);
|
||||
}
|
||||
}, [decodedToken]);
|
||||
|
||||
@@ -50,6 +54,7 @@ function useProvideAuth(): AuthContextProps {
|
||||
const logoutUrl = (decodedToken as any).logoutUrl;
|
||||
setToken(null);
|
||||
setLoginName(undefined);
|
||||
setTrusted(undefined);
|
||||
if (trusted && logoutUrl?.length) {
|
||||
window.location.replace(logoutUrl);
|
||||
}
|
||||
@@ -57,6 +62,7 @@ function useProvideAuth(): AuthContextProps {
|
||||
|
||||
return {
|
||||
login: loginName,
|
||||
trusted,
|
||||
setToken,
|
||||
logout,
|
||||
}
|
||||
|
||||
24
client/src/context/eggs.tsx
Normal file
24
client/src/context/eggs.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getEasterEgg } from "../api/EasterEggApi";
|
||||
import { AuthContextProps } from "./auth";
|
||||
import { EasterEgg } from "../types";
|
||||
|
||||
export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undefined, boolean] => {
|
||||
const [result, setResult] = useState<EasterEgg | undefined>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchEasterEgg() {
|
||||
if (auth?.login) {
|
||||
setLoading(true);
|
||||
const egg = await getEasterEgg();
|
||||
setResult(egg);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchEasterEgg();
|
||||
|
||||
}, [auth?.login]);
|
||||
|
||||
return [result, loading];
|
||||
}
|
||||
Reference in New Issue
Block a user