dodelej me z pc
This commit is contained in:
+9
-10
@@ -7,6 +7,7 @@ self.addEventListener('push', (event) => {
|
||||
body: data.body,
|
||||
icon: '/favicon.ico',
|
||||
tag: 'lunch-reminder',
|
||||
data: { login: data.login },
|
||||
actions: [
|
||||
{ action: 'neobedvam', title: 'Mám vlastní/neobědvám' },
|
||||
],
|
||||
@@ -18,28 +19,26 @@ self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'neobedvam') {
|
||||
event.waitUntil(
|
||||
self.registration.pushManager.getSubscription().then((subscription) => {
|
||||
if (!subscription) return;
|
||||
return fetch('/api/notifications/push/quickChoice', {
|
||||
const login = event.notification.data?.login;
|
||||
if (login) {
|
||||
event.waitUntil(
|
||||
fetch('/api/notifications/push/quickChoice', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ endpoint: subscription.endpoint }),
|
||||
});
|
||||
})
|
||||
);
|
||||
body: JSON.stringify({ login }),
|
||||
})
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
self.clients.matchAll({ type: 'window' }).then((clientList) => {
|
||||
// Pokud je již otevřené okno, zaostříme na něj
|
||||
for (const client of clientList) {
|
||||
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
// Jinak otevřeme nové
|
||||
return self.clients.openWindow('/');
|
||||
})
|
||||
);
|
||||
|
||||
+2
-1
@@ -18,7 +18,7 @@ import Loader from './components/Loader';
|
||||
import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils';
|
||||
import NoteModal from './components/modals/NoteModal';
|
||||
import { useEasterEgg } from './context/eggs';
|
||||
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr } from '../../types';
|
||||
import { ClientData, Food, MealSlot, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr } from '../../types';
|
||||
import { getLunchChoiceName } from './enums';
|
||||
// import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
||||
// import './FallingLeaves.scss';
|
||||
@@ -124,6 +124,7 @@ function App() {
|
||||
});
|
||||
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
||||
// console.log("Přijata nová data ze socketu", newData);
|
||||
if (newData.slot === MealSlot.EXTRA) return;
|
||||
// Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený
|
||||
if (dayIndexRef.current == null || newData.dayIndex === dayIndexRef.current) {
|
||||
setData(newData);
|
||||
|
||||
@@ -5,14 +5,24 @@ import { SnowOverlay } from 'react-snow-overlay';
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { SocketContext, socket } from "./context/socket";
|
||||
import StatsPage from "./pages/StatsPage";
|
||||
import ExtraPage from "./pages/ExtraPage";
|
||||
import App from "./App";
|
||||
|
||||
export const STATS_URL = '/stats';
|
||||
export const VECERE_URL = '/vecere';
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={STATS_URL} element={<StatsPage />} />
|
||||
<Route path={VECERE_URL} element={
|
||||
<ProvideSettings>
|
||||
<SocketContext.Provider value={socket}>
|
||||
<ExtraPage />
|
||||
<ToastContainer />
|
||||
</SocketContext.Provider>
|
||||
</ProvideSettings>
|
||||
} />
|
||||
<Route path="/" element={
|
||||
<ProvideSettings>
|
||||
<SocketContext.Provider value={socket}>
|
||||
|
||||
@@ -10,7 +10,7 @@ import GenerateQrModal from "./modals/GenerateQrModal";
|
||||
import GenerateMockDataModal from "./modals/GenerateMockDataModal";
|
||||
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
||||
import { useNavigate } from "react-router";
|
||||
import { STATS_URL } from "../AppRoutes";
|
||||
import { STATS_URL, VECERE_URL } from "../AppRoutes";
|
||||
import { FeatureRequest, getVotes, updateVote, LunchChoices } from "../../../types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -197,6 +197,7 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => navigate(VECERE_URL)}>Večeře</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setChangelogModalOpen(true)}>Novinky</NavDropdown.Item>
|
||||
{IS_DEV && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Table } from 'react-bootstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faBasketShopping, faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
ClientData, LunchChoice, MealSlot, UserLunchChoice,
|
||||
addChoice, removeChoices, updateNote, setBuyer, getData,
|
||||
} from '../../../types';
|
||||
import { EVENT_MESSAGE, SocketContext } from '../context/socket';
|
||||
import { useAuth } from '../context/auth';
|
||||
import Login from '../Login';
|
||||
import Header from '../components/Header';
|
||||
import Footer from '../components/Footer';
|
||||
import Loader from '../components/Loader';
|
||||
import NoteModal from '../components/modals/NoteModal';
|
||||
|
||||
const SLOT = MealSlot.EXTRA;
|
||||
|
||||
export default function ExtraPage() {
|
||||
const auth = useAuth();
|
||||
const socket = useContext(SocketContext);
|
||||
const [data, setData] = useState<ClientData | undefined>();
|
||||
const [failure, setFailure] = useState(false);
|
||||
const [noteModalOpen, setNoteModalOpen] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const r = await getData({ query: { slot: SLOT } });
|
||||
if (r.data) setData(r.data);
|
||||
} catch {
|
||||
setFailure(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth?.login) return;
|
||||
fetchData();
|
||||
}, [auth?.login]);
|
||||
|
||||
useEffect(() => {
|
||||
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
||||
if (newData.slot === SLOT) setData(newData);
|
||||
});
|
||||
return () => { socket.off(EVENT_MESSAGE); };
|
||||
}, [socket]);
|
||||
|
||||
const myChoice = data?.choices?.OBJEDNAVAM?.[auth?.login ?? ''];
|
||||
const isIn = !!myChoice;
|
||||
const isBuyer = myChoice?.isBuyer ?? false;
|
||||
|
||||
const joinOrder = async () => {
|
||||
if (!auth?.login) return;
|
||||
await addChoice({ body: { locationKey: LunchChoice.OBJEDNAVAM, slot: SLOT } });
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const joinAndBuy = async () => {
|
||||
if (!auth?.login) return;
|
||||
await addChoice({ body: { locationKey: LunchChoice.OBJEDNAVAM, slot: SLOT } });
|
||||
await setBuyer({ body: { slot: SLOT } });
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const leaveOrder = async () => {
|
||||
if (!auth?.login) return;
|
||||
await removeChoices({ body: { locationKey: LunchChoice.OBJEDNAVAM, slot: SLOT } });
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const toggleBuyer = async () => {
|
||||
if (!auth?.login) return;
|
||||
await setBuyer({ body: { slot: SLOT } });
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const saveNote = async (note?: string) => {
|
||||
if (!auth?.login) return;
|
||||
await updateNote({ body: { note, slot: SLOT } });
|
||||
setNoteModalOpen(false);
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
if (!auth?.login) return <Login />;
|
||||
|
||||
if (failure) return (
|
||||
<Loader icon={faSearch} description="Nepodařilo se načíst data" animation="fa-beat" />
|
||||
);
|
||||
|
||||
if (!data) return (
|
||||
<Loader icon={faSearch} description="Načítám..." animation="fa-bounce" />
|
||||
);
|
||||
|
||||
const orderEntries = Object.entries(data.choices?.OBJEDNAVAM ?? {}) as [string, UserLunchChoice][];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<Header choices={data.choices} />
|
||||
<div className="wrapper">
|
||||
<h1 className="title">Večeře</h1>
|
||||
<p style={{ color: 'var(--luncher-text-muted)' }}>Extra jídlo pro ty, kdo zůstávají déle</p>
|
||||
|
||||
<div className="content-wrapper">
|
||||
<div className="content">
|
||||
<div className="choice-section fade-in">
|
||||
{!isIn ? (
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
<Button variant="primary" onClick={joinOrder}>
|
||||
Přidám se
|
||||
</Button>
|
||||
<Button variant="outline-primary" onClick={joinAndBuy}>
|
||||
<FontAwesomeIcon icon={faBasketShopping} className="me-2" />
|
||||
Budu objednávat
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex gap-2 flex-wrap align-items-center">
|
||||
<span style={{ color: 'var(--luncher-text-secondary)' }}>
|
||||
{isBuyer ? 'Objednáváš.' : 'Jsi přidán/a k objednávce.'}
|
||||
</span>
|
||||
<Button variant="outline-secondary" size="sm" onClick={toggleBuyer}>
|
||||
<FontAwesomeIcon icon={faBasketShopping} className="me-1" />
|
||||
{isBuyer ? 'Odebrat roli objednávajícího' : 'Označit se jako objednávající'}
|
||||
</Button>
|
||||
<Button variant="outline-secondary" size="sm" onClick={() => setNoteModalOpen(true)}>
|
||||
<FontAwesomeIcon icon={faNoteSticky} className="me-1" />
|
||||
Poznámka
|
||||
</Button>
|
||||
<Button variant="outline-danger" size="sm" onClick={leaveOrder}>
|
||||
<FontAwesomeIcon icon={faTrashCan} className="me-1" />
|
||||
Odhlásit se
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{orderEntries.length > 0 && (
|
||||
<Table className="choices-table mt-4 fade-in">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Budu objednávat / Přidám se</td>
|
||||
<td className="p-0">
|
||||
<Table className="nested-table">
|
||||
<tbody>
|
||||
{orderEntries.map(([login, payload]) => (
|
||||
<tr key={login}>
|
||||
<td>
|
||||
<div className="user-row">
|
||||
<div className="user-info">
|
||||
{payload.trusted && (
|
||||
<span className="trusted-icon" title="Ověřený uživatel">
|
||||
<FontAwesomeIcon icon={faCircleCheck} style={{ cursor: 'help' }} />
|
||||
</span>
|
||||
)}
|
||||
<strong>{login}</strong>
|
||||
{payload.note && (
|
||||
<span className="ms-2" style={{ fontSize: 'small', color: 'var(--luncher-text-secondary)' }}>
|
||||
({payload.note})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="user-actions">
|
||||
{payload.isBuyer && (
|
||||
<span title="Objednávající">
|
||||
<FontAwesomeIcon icon={faBasketShopping} className="buyer-icon" />
|
||||
</span>
|
||||
)}
|
||||
{login === auth.login && (
|
||||
<>
|
||||
<span title="Upravit poznámku">
|
||||
<FontAwesomeIcon
|
||||
onClick={() => setNoteModalOpen(true)}
|
||||
className="action-icon"
|
||||
icon={faNoteSticky}
|
||||
/>
|
||||
</span>
|
||||
<span title="Odhlásit se z objednávky">
|
||||
<FontAwesomeIcon
|
||||
onClick={leaveOrder}
|
||||
className="action-icon"
|
||||
icon={faTrashCan}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
<NoteModal
|
||||
isOpen={noteModalOpen}
|
||||
onClose={() => setNoteModalOpen(false)}
|
||||
onSave={saveNote}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user