feat: redesign aplikace pomocí claude code

This commit is contained in:
Stánek Pavel
2026-02-03 10:37:27 +01:00
parent 6a1da97ef1
commit 37cacd895a
14 changed files with 1537 additions and 387 deletions

View File

@@ -344,10 +344,10 @@ function App() {
const renderFoodTable = (location: Restaurant, menu: RestaurantDayMenu) => {
let content;
if (menu?.closed) {
content = <h3>Zavřeno</h3>
content = <div className="restaurant-closed">Zavřeno</div>
} else if (menu?.food?.length && menu.food.length > 0) {
const hideSoups = settings?.hideSoups;
content = <Table striped bordered hover>
content = <Table className="food-table">
<tbody style={{ cursor: canChangeChoice ? 'pointer' : 'default' }}>
{menu.food.map((f: Food, index: number) =>
(!hideSoups || !f.isSoup) &&
@@ -356,15 +356,17 @@ function App() {
<td>
{f.name}
{f.allergens && f.allergens.length > 0 && (
<> ({f.allergens.map((a, idx) => (
<span key={a}>
<span title={ALLERGENS[a]} style={{ cursor: 'help', textDecoration: 'underline' }} onClick={e => {
e.stopPropagation();
window.open(LINK_ALLERGENS, '_blank');
}}>{a}</span>
{idx < f.allergens!.length - 1 && ','}
</span>
))})</>
<span className="ms-1">
({f.allergens.map((a, idx) => (
<span key={a}>
<span className="allergen-link" title={ALLERGENS[a]} onClick={e => {
e.stopPropagation();
window.open(LINK_ALLERGENS, '_blank');
}}>{a}</span>
{idx < f.allergens!.length - 1 && ', '}
</span>
))})
</span>
)}
</td>
<td>{f.price}</td>
@@ -373,12 +375,18 @@ function App() {
</tbody>
</Table>
} else {
content = <h3>Chyba načtení dat</h3>
content = <div className="restaurant-error">Chyba načtení dat</div>
}
return <Col md={12} lg={3} className='mt-3'>
<h3 style={{ cursor: canChangeChoice ? 'pointer' : 'default' }} onClick={() => doAddClickFoodChoice(location)}>{getLunchChoiceName(location)}</h3>
{menu?.lastUpdate && <small>Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}</small>}
{content}
return <Col md={6} lg={3} className='mt-3'>
<div className="restaurant-card">
<div className="restaurant-header">
<h3 style={{ cursor: canChangeChoice ? 'pointer' : 'default' }} onClick={() => doAddClickFoodChoice(location)}>
{getLunchChoiceName(location)}
</h3>
{menu?.lastUpdate && <small>Aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}</small>}
</div>
{content}
</div>
</Col>
}
@@ -421,16 +429,6 @@ function App() {
<Header />
<div className='wrapper'>
{data.isWeekend ? <h4>Užívejte víkend :)</h4> : <>
<Alert variant={'primary'}>
<img alt="" src='hat.png' style={{ position: "absolute", width: "70px", rotate: "-45deg", left: -40, top: -58 }} />
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
Poslední změny:
<ul>
<li>Oprava parsování Sladovnické a TechTower</li>
<li>Zimní atmosféra</li>
<li>Možnost označit se jako objednávající u volby "budu objednávat"</li>
</ul>
</Alert>
{dayIndex != null &&
<div className='day-navigator'>
<span title='Předchozí den'>
@@ -450,10 +448,10 @@ function App() {
</Row>
<div className='content-wrapper'>
<div className='content'>
{canChangeChoice && <>
{canChangeChoice && <div className="choice-section fade-in">
<p>{`Jak to ${dayIndex == null || dayIndex === data.todayDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<Form.Select ref={choiceRef} onChange={doAddChoice}>
<option></option>
<option value="">Vyber možnost...</option>
{Object.entries(LunchChoice)
.filter(entry => {
const locationKey = entry[0] as Restaurant;
@@ -463,44 +461,44 @@ function App() {
</Form.Select>
<small>Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí.</small>
{foodChoiceList && !closed && <>
<p style={{ marginTop: "10px" }}>Na co dobrého? <small>(nepovinné)</small></p>
<p className="mt-3">Na co dobrého? <small style={{ color: 'var(--luncher-text-muted)' }}>(nepovinné)</small></p>
<Form.Select ref={foodChoiceRef} onChange={doAddFoodChoice}>
<option></option>
<option value="">Vyber jídlo...</option>
{foodChoiceList.map((food, index) => <option key={food.name} value={index}>{food.name}</option>)}
</Form.Select>
</>}
{foodChoiceList && !closed && <>
<p style={{ marginTop: "10px" }}>V kolik hodin preferuješ odchod?</p>
<p className="mt-3">V kolik hodin preferuješ odchod?</p>
<Form.Select ref={departureChoiceRef} onChange={handleChangeDepartureTime}>
<option></option>
<option value="">Vyber čas...</option>
{Object.values(DepartureTime)
.filter(time => isInTheFuture(time))
.map(time => <option key={time} value={time}>{time}</option>)}
</Form.Select>
</>}
</>}
</div>}
{Object.keys(data.choices).length > 0 ?
<Table bordered className='mt-5'>
<Table className='choices-table mt-4 fade-in'>
<tbody>
{Object.keys(data.choices).map(key => {
const locationKey = key as LunchChoice;
const locationName = getLunchChoiceName(locationKey);
const loginObject = data.choices[locationKey];
if (!loginObject) {
return;
return null;
}
const locationLoginList = Object.entries(loginObject);
const locationPickCount = locationLoginList.length
return (
<tr key={key}>
{(locationPickCount ?? 0) > 1 ? (
<td>{locationName} ({locationPickCount})</td>
) : (
<td>{locationName}</td>)}
<td>
{locationName}
{(locationPickCount ?? 0) > 1 && <span className="ms-1">({locationPickCount})</span>}
</td>
<td className='p-0'>
<Table>
<Table className="nested-table">
<tbody>
{locationLoginList.map((entry: [string, UserLunchChoice], index) => {
{locationLoginList.map((entry: [string, UserLunchChoice]) => {
const login = entry[0];
const userPayload = entry[1];
const userChoices = userPayload?.selectedFoods;
@@ -508,13 +506,12 @@ function App() {
const isBuyer = userPayload?.isBuyer || false;
return <tr key={entry[0]}>
<td>
{/* TODO zrefaktorovat, oddělit řádek do samostatné komponenty (a akce možná zvlášť) */}
{trusted && <span className='trusted-icon' title='Uživatel ověřený doménovým přihlášením'>
<FontAwesomeIcon icon={faCircleCheck} style={{ cursor: "help" }} />
</span>}
{login}
{userPayload.departureTime && <small> ({userPayload.departureTime})</small>}
{userPayload.note && <span style={{ fontSize: 'small' }}> ({userPayload.note})</span>}
<strong>{login}</strong>
{userPayload.departureTime && <small className="ms-2" style={{ color: 'var(--luncher-text-muted)' }}>({userPayload.departureTime})</small>}
{userPayload.note && <span className="ms-2" style={{ fontSize: 'small', color: 'var(--luncher-text-secondary)' }}>({userPayload.note})</span>}
{login === auth.login && canChangeChoice && locationKey === LunchChoice.OBJEDNAVAM && <span title='Označit/odznačit se jako objednávající'>
<FontAwesomeIcon onClick={() => {
markAsBuyer();
@@ -569,97 +566,94 @@ function App() {
)}
</tbody>
</Table>
: <div className='mt-5'><i>Zatím nikdo nehlasoval...</i></div>
: <div className='no-votes mt-4'>Zatím nikdo nehlasoval...</div>
}
</div>
{dayIndex === data.todayDayIndex &&
<div className='mt-5'>
<div className='pizza-section fade-in'>
{!data.pizzaDay &&
<div style={{ textAlign: 'center' }}>
<>
<h3>Pizza Day</h3>
<p>Pro dnešní den není aktuálně založen Pizza day.</p>
{loadingPizzaDay ?
<span>
<FontAwesomeIcon icon={faGear} className='fa-spin' /> Zjišťujeme dostupné pizzy
<span style={{ color: 'var(--luncher-primary)' }}>
<FontAwesomeIcon icon={faGear} className='fa-spin me-2' /> Zjišťujeme dostupné pizzy
</span>
:
<>
<div>
<Button onClick={async () => {
setLoadingPizzaDay(true);
await createPizzaDay().then(() => setLoadingPizzaDay(false));
}}>Založit Pizza day</Button>
<Button onClick={doJdemeObed} style={{ marginLeft: "14px" }}>Jdeme na oběd !</Button>
</>
<Button variant="outline-primary" onClick={doJdemeObed}>Jdeme na oběd!</Button>
</div>
}
</div>
</>
}
{data.pizzaDay &&
<div>
<div style={{ textAlign: 'center' }}>
<h3>Pizza day</h3>
{
data.pizzaDay.state === PizzaDayState.CREATED &&
<div>
<p>
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
Můžete upravovat své objednávky.
</p>
{
data.pizzaDay.creator === auth.login &&
<>
<Button className='danger mb-3' title="Smaže kompletně pizza day, včetně dosud zadaných objednávek." onClick={async () => {
await deletePizzaDay();
}}>Smazat Pizza day</Button>
<Button className='mb-3' style={{ marginLeft: '20px' }} title={noOrders ? "Nelze uzamknout - neexistuje žádná objednávka" : "Zamezí přidávat/odebírat objednávky. Použij před samotným objednáním, aby již nemohlo docházet ke změnám."} disabled={noOrders} onClick={async () => {
await lockPizzaDay();
}}>Uzamknout</Button>
</>
}
</div>
}
{
data.pizzaDay.state === PizzaDayState.LOCKED &&
<div>
<p>Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}</p>
{data.pizzaDay.creator === auth.login &&
<>
<Button className='danger mb-3' title="Umožní znovu editovat objednávky." onClick={async () => {
await unlockPizzaDay();
}}>Odemknout</Button>
<Button className='danger mb-3' style={{ marginLeft: '20px' }} title={noOrders ? "Nelze objednat - neexistuje žádná objednávka" : "Použij po objednání. Objednávky zůstanou zamčeny."} disabled={noOrders} onClick={async () => {
await finishOrder();
}}>Objednáno</Button>
</>
}
</div>
}
{
data.pizzaDay.state === PizzaDayState.ORDERED &&
<div>
<p>Pizzy byly objednány uživatelem {data.pizzaDay.creator}</p>
{data.pizzaDay.creator === auth.login &&
<div>
<Button className='danger mb-3' title="Vrátí stav do předchozího kroku (před objednáním)." onClick={async () => {
await lockPizzaDay();
}}>Vrátit do "uzamčeno"</Button>
<Button className='danger mb-3' style={{ marginLeft: '20px' }} title="Nastaví stav na 'Doručeno' - koncový stav." onClick={async () => {
await finishDelivery({ body: { bankAccount: settings?.bankAccount, bankAccountHolder: settings?.holderName } });
}}>Doručeno</Button>
</div>
}
</div>
}
{
data.pizzaDay.state === PizzaDayState.DELIVERED &&
<div>
<p>
Pizzy byly doručeny.
{myOrder?.hasQr ? ` Objednávku můžete uživateli ${data.pizzaDay.creator} uhradit pomocí QR kódu níže.` : ''}
</p>
</div>
}
</div>
<>
<h3>Pizza Day</h3>
{
data.pizzaDay.state === PizzaDayState.CREATED &&
<>
<p>
Pizza Day je založen a spravován uživatelem <strong>{data.pizzaDay.creator}</strong>.<br />
Můžete upravovat své objednávky.
</p>
{
data.pizzaDay.creator === auth.login &&
<div className="mb-4">
<Button variant="danger" title="Smaže kompletně pizza day, včetně dosud zadaných objednávek." onClick={async () => {
await deletePizzaDay();
}}>Smazat Pizza day</Button>
<Button title={noOrders ? "Nelze uzamknout - neexistuje žádná objednávka" : "Zamezí přidávat/odebírat objednávky. Použij před samotným objednáním, aby již nemohlo docházet ke změnám."} disabled={noOrders} onClick={async () => {
await lockPizzaDay();
}}>Uzamknout</Button>
</div>
}
</>
}
{
data.pizzaDay.state === PizzaDayState.LOCKED &&
<>
<p>Objednávky jsou uzamčeny uživatelem <strong>{data.pizzaDay.creator}</strong></p>
{data.pizzaDay.creator === auth.login &&
<div className="mb-4">
<Button variant="secondary" title="Umožní znovu editovat objednávky." onClick={async () => {
await unlockPizzaDay();
}}>Odemknout</Button>
<Button title={noOrders ? "Nelze objednat - neexistuje žádná objednávka" : "Použij po objednání. Objednávky zůstanou zamčeny."} disabled={noOrders} onClick={async () => {
await finishOrder();
}}>Objednáno</Button>
</div>
}
</>
}
{
data.pizzaDay.state === PizzaDayState.ORDERED &&
<>
<p>Pizzy byly objednány uživatelem <strong>{data.pizzaDay.creator}</strong></p>
{data.pizzaDay.creator === auth.login &&
<div className="mb-4">
<Button variant="secondary" title="Vrátí stav do předchozího kroku (před objednáním)." onClick={async () => {
await lockPizzaDay();
}}>Vrátit do "uzamčeno"</Button>
<Button title="Nastaví stav na 'Doručeno' - koncový stav." onClick={async () => {
await finishDelivery({ body: { bankAccount: settings?.bankAccount, bankAccountHolder: settings?.holderName } });
}}>Doručeno</Button>
</div>
}
</>
}
{
data.pizzaDay.state === PizzaDayState.DELIVERED &&
<p>
Pizzy byly doručeny.
{myOrder?.hasQr ? ` Objednávku můžete uživateli ${data.pizzaDay.creator} uhradit pomocí QR kódu níže.` : ''}
</p>
}
{data.pizzaDay.state === PizzaDayState.CREATED &&
<div style={{ textAlign: 'center' }}>
<div className="pizza-order-form">
<SelectSearch
search={true}
options={pizzaSuggestions}
@@ -668,29 +662,31 @@ function App() {
onBlur={_ => { }}
onFocus={_ => { }}
/>
Poznámka: <input ref={pizzaPoznamkaRef} className='mt-3' type="text" onKeyDown={event => {
if (event.key === 'Enter') {
handlePizzaPoznamkaChange();
}
event.stopPropagation();
}} />
<Button
style={{ marginLeft: '20px' }}
disabled={!myOrder?.pizzaList?.length}
onClick={handlePizzaPoznamkaChange}>
Uložit
</Button>
<div className="d-flex align-items-center gap-2">
<label style={{ color: 'var(--luncher-text-secondary)' }}>Poznámka:</label>
<input ref={pizzaPoznamkaRef} type="text" placeholder="Např. bez cibule" onKeyDown={event => {
if (event.key === 'Enter') {
handlePizzaPoznamkaChange();
}
event.stopPropagation();
}} />
<Button
disabled={!myOrder?.pizzaList?.length}
onClick={handlePizzaPoznamkaChange}>
Uložit
</Button>
</div>
</div>
}
<PizzaOrderList state={data.pizzaDay.state!} orders={data.pizzaDay.orders!} onDelete={handlePizzaDelete} creator={data.pizzaDay.creator!} />
{
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr ?
<div className='qr-code'>
<h3>QR platba</h3>
<img src={`/api/qr?login=${auth.login}`} alt='QR kód' />
</div> : null
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
<div className='qr-code'>
<h3>QR platba</h3>
<img src={`/api/qr?login=${auth.login}`} alt='QR kód' />
</div>
}
</div>
</>
}
</div>
}