From 37cacd895aa0f23e1dd9c2b98707dfca230e75c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A1nek=20Pavel?= Date: Tue, 3 Feb 2026 10:37:27 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20redesign=20aplikace=20pomoc=C3=AD=20cla?= =?UTF-8?q?ude=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/index.html | 21 +- client/src/App.scss | 1071 +++++++++++++++-- client/src/App.tsx | 272 +++-- client/src/Login.css | 90 +- client/src/Login.tsx | 42 +- client/src/components/Footer.tsx | 22 +- client/src/components/Header.tsx | 65 +- client/src/components/Loader.tsx | 12 +- client/src/components/PizzaOrderList.tsx | 58 +- .../components/modals/RefreshMenuModal.tsx | 34 +- .../src/components/modals/SettingsModal.tsx | 113 +- client/src/context/settings.tsx | 18 +- client/src/index.css | 22 +- client/src/pages/StatsPage.scss | 84 +- 14 files changed, 1537 insertions(+), 387 deletions(-) diff --git a/client/index.html b/client/index.html index 3956158..94def8b 100644 --- a/client/index.html +++ b/client/index.html @@ -12,13 +12,22 @@ Luncher diff --git a/client/src/App.scss b/client/src/App.scss index c9fd1cf..9bdabc3 100644 --- a/client/src/App.scss +++ b/client/src/App.scss @@ -1,98 +1,422 @@ +// ============================================ +// DESIGN SYSTEM - LUNCHER APP +// ============================================ + +// Color palette - Spotify-style green scheme :root, [data-bs-theme="light"] { - --luncher-navbar-bg: #3c3c3c; - --luncher-action-icon: rgb(0, 89, 255); - --luncher-buyer-icon: #dbba00; - --luncher-text-muted: gray; + // Primary colors - Vibrant green + --luncher-primary: #16a34a; + --luncher-primary-hover: #15803d; + --luncher-primary-light: #dcfce7; + + // Background colors + --luncher-bg: #f9fafb; + --luncher-bg-card: #ffffff; + --luncher-bg-hover: #f3f4f6; + + // Text colors + --luncher-text: #111827; + --luncher-text-secondary: #4b5563; + --luncher-text-muted: #9ca3af; + + // Border colors + --luncher-border: #e5e7eb; + --luncher-border-light: #f3f4f6; + + // Navbar - Dark + --luncher-navbar-bg: #111827; + --luncher-navbar-text: #f9fafb; + + // Action colors + --luncher-action-icon: #16a34a; + --luncher-buyer-icon: #d97706; + --luncher-success: #16a34a; + --luncher-warning: #d97706; + --luncher-danger: #dc2626; + + // Shadows + --luncher-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --luncher-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --luncher-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + // Border radius + --luncher-radius-sm: 6px; + --luncher-radius: 10px; + --luncher-radius-lg: 12px; + --luncher-radius-xl: 16px; + + // Transitions + --luncher-transition: all 0.2s ease; + --luncher-transition-slow: all 0.3s ease; } [data-bs-theme="dark"] { - --luncher-navbar-bg: #1a1d21; - --luncher-action-icon: #5c9aff; - --luncher-buyer-icon: #ffd700; - --luncher-text-muted: #9ca3af; + // Primary colors - Spotify green + --luncher-primary: #1db954; + --luncher-primary-hover: #1ed760; + --luncher-primary-light: #052e16; + + // Background colors - True dark like Spotify + --luncher-bg: #121212; + --luncher-bg-card: #181818; + --luncher-bg-hover: #282828; + + // Text colors + --luncher-text: #ffffff; + --luncher-text-secondary: #b3b3b3; + --luncher-text-muted: #6b7280; + + // Border colors + --luncher-border: #282828; + --luncher-border-light: #1f1f1f; + + // Navbar + --luncher-navbar-bg: #000000; + --luncher-navbar-text: #ffffff; + + // Action colors + --luncher-action-icon: #1db954; + --luncher-buyer-icon: #fbbf24; + --luncher-success: #1db954; + --luncher-warning: #fbbf24; + --luncher-danger: #f87171; + + // Shadows + --luncher-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5); + --luncher-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.4); + --luncher-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4); } -.App { - text-align: center; -} +// ============================================ +// BASE STYLES +// ============================================ -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } -} - -.loader { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.loader>.loader-icon { - font-size: 64px; -} - -/* Sticky footer layout */ -html, -body, -#root { +html, body, #root { height: 100%; margin: 0; } +html { + overflow-x: hidden; +} + +body { + background-color: var(--luncher-bg); + color: var(--luncher-text); + transition: var(--luncher-transition-slow); +} + +// ============================================ +// APP CONTAINER +// ============================================ + .app-container { min-height: 100vh; display: flex; flex-direction: column; + background-color: var(--luncher-bg); } .wrapper { - padding: 20px; + padding: 24px; flex: 1; - /* Zabere zbytek místa */ + max-width: 1400px; + margin: 0 auto; + width: 100%; + + @media (max-width: 768px) { + padding: 16px; + } } +// ============================================ +// NAVBAR +// ============================================ + +.navbar { + background: var(--luncher-navbar-bg) !important; + padding: 12px 24px; + box-shadow: none; + border: none; + border-bottom: 1px solid var(--luncher-border); + + .navbar-brand { + font-weight: 700; + font-size: 1.5rem; + letter-spacing: -0.5px; + color: var(--luncher-navbar-text) !important; + transition: var(--luncher-transition); + + &:hover { + opacity: 0.9; + } + } + + .nav-link, .dropdown-toggle { + color: var(--luncher-navbar-text) !important; + font-weight: 500; + transition: var(--luncher-transition); + + &:hover { + opacity: 0.85; + } + } + + .dropdown-menu { + background: var(--luncher-bg-card); + border: 1px solid var(--luncher-border); + border-radius: var(--luncher-radius); + box-shadow: var(--luncher-shadow-lg); + padding: 8px; + margin-top: 8px; + + .dropdown-item { + color: var(--luncher-text); + border-radius: var(--luncher-radius-sm); + padding: 10px 16px; + transition: var(--luncher-transition); + font-weight: 500; + + &:hover { + background: var(--luncher-bg-hover); + color: var(--luncher-primary); + } + } + + .dropdown-divider { + border-color: var(--luncher-border); + margin: 8px 0; + } + } +} + +#basic-navbar-nav { + justify-content: flex-end; +} + +// Theme toggle button +.theme-toggle { + background: transparent; + border: none; + color: var(--luncher-navbar-text); + padding: 8px 12px; + margin-right: 16px; + border-radius: var(--luncher-radius-sm); + cursor: pointer; + transition: var(--luncher-transition); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + + &:hover { + background: rgba(255, 255, 255, 0.1); + transform: scale(1.1); + } + + &:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); + } + + svg { + transition: transform 0.3s ease; + } + + &:hover svg { + transform: rotate(15deg); + } +} + +// ============================================ +// TYPOGRAPHY & TITLES +// ============================================ + .title { - margin: 50px 20px; + margin: 32px 0; + font-weight: 700; + font-size: 2rem; + color: var(--luncher-text); + + &.text-muted { + color: var(--luncher-text-muted) !important; + } } -.food-tables { - margin-bottom: 50px; +// ============================================ +// DAY NAVIGATOR +// ============================================ + +.day-navigator { + display: flex; + align-items: center; + justify-content: center; + gap: 24px; + margin-bottom: 32px; + + svg { + font-size: 1.5rem; + color: var(--luncher-text-secondary); + cursor: pointer; + padding: 12px; + border-radius: 50%; + background: var(--luncher-bg-card); + box-shadow: var(--luncher-shadow-sm); + transition: var(--luncher-transition); + + &:hover { + color: var(--luncher-primary); + background: var(--luncher-primary-light); + transform: scale(1.05); + } + } + + h1 { + margin: 0; + min-width: 300px; + text-align: center; + } } +// ============================================ +// FOOD TABLES - CARD STYLE +// ============================================ + +.food-tables { + margin-bottom: 48px; + display: flex; + flex-wrap: wrap; + + > [class*="col-"] { + margin-bottom: 24px; + display: flex; + } +} + +.restaurant-card { + background: var(--luncher-bg-card); + border-radius: var(--luncher-radius-lg); + box-shadow: var(--luncher-shadow); + overflow: hidden; + width: 100%; + display: flex; + flex-direction: column; + transition: var(--luncher-transition); + border: 1px solid var(--luncher-border-light); + + &:hover { + box-shadow: var(--luncher-shadow-lg); + transform: translateY(-2px); + } + + .restaurant-header { + background: var(--luncher-primary); + padding: 16px 20px; + border-bottom: none; + cursor: pointer; + transition: var(--luncher-transition); + + &:hover { + background: var(--luncher-primary-hover); + } + + h3 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: #ffffff; + cursor: pointer; + } + + small { + display: block; + margin-top: 4px; + color: rgba(255, 255, 255, 0.7); + font-size: 0.75rem; + } + } + + .restaurant-closed { + padding: 40px 20px; + text-align: center; + color: var(--luncher-text-muted); + font-weight: 500; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .restaurant-error { + padding: 40px 20px; + text-align: center; + color: var(--luncher-danger); + font-weight: 500; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .food-table { + flex: 1; + } +} + +.food-table { + margin: 0; + + tbody tr { + transition: var(--luncher-transition); + cursor: pointer; + + &:hover { + background: var(--luncher-bg-hover); + } + + &:last-child td { + border-bottom: none; + } + } + + td { + padding: 12px 16px; + border-color: var(--luncher-border-light); + vertical-align: middle; + color: var(--luncher-text); + + &:first-child { + width: 60px; + text-align: center; + font-weight: 500; + color: var(--luncher-text-secondary); + } + + &:last-child { + width: 80px; + text-align: right; + font-weight: 600; + color: var(--luncher-primary); + } + } + + .allergen-link { + cursor: help; + text-decoration: underline; + text-decoration-style: dotted; + color: var(--luncher-text-muted); + font-size: 0.85em; + + &:hover { + color: var(--luncher-primary); + } + } +} + +// ============================================ +// CHOICE FORM +// ============================================ + .content-wrapper { width: 100%; display: flex; @@ -100,69 +424,456 @@ body, align-items: center; } -.navbar { - background-color: var(--luncher-navbar-bg); - padding-left: 20px; - padding-right: 20px; +.content { + width: 100%; + max-width: 700px; } -#basic-navbar-nav { - justify-content: flex-end; -} +.choice-section { + background: var(--luncher-bg-card); + border-radius: var(--luncher-radius-lg); + padding: 24px; + box-shadow: var(--luncher-shadow); + margin-bottom: 32px; + border: 1px solid var(--luncher-border-light); -.table { - margin-bottom: 0; -} - -.table> :not(caption) { - .action-icon { - color: var(--luncher-action-icon); - cursor: pointer; - margin-left: 10px; - padding: 0; + p { + margin-bottom: 12px; + font-weight: 500; + color: var(--luncher-text); } - .buyer-icon { - color: var(--luncher-buyer-icon); - margin-left: 10px; - padding: 0; + small { + display: block; + margin-top: 8px; + color: var(--luncher-text-muted); + font-size: 0.85rem; } } -.table ul { - padding: 0; - margin-left: 20px; - margin-bottom: 0; +// ============================================ +// FORM CONTROLS +// ============================================ + +.form-select, .form-control { + background-color: var(--luncher-bg); + border: 2px solid var(--luncher-border); + border-radius: var(--luncher-radius-sm); + padding: 12px 16px; + font-size: 1rem; + color: var(--luncher-text); + transition: var(--luncher-transition); + + &:hover { + border-color: var(--luncher-text-muted); + } + + &:focus { + border-color: var(--luncher-primary); + box-shadow: 0 0 0 3px var(--luncher-primary-light); + outline: none; + } } -.table td { - vertical-align: top; +input[type="text"], input[type="email"], input[type="password"] { + background-color: var(--luncher-bg); + border: 2px solid var(--luncher-border); + border-radius: var(--luncher-radius-sm); + padding: 12px 16px; + font-size: 1rem; + color: var(--luncher-text); + transition: var(--luncher-transition); + width: 100%; + + &:hover { + border-color: var(--luncher-text-muted); + } + + &:focus { + border-color: var(--luncher-primary); + box-shadow: 0 0 0 3px var(--luncher-primary-light); + outline: none; + } + + &::placeholder { + color: var(--luncher-text-muted); + } } -.table>tbody>tr>td>table>tbody>tr>td { +// ============================================ +// BUTTONS +// ============================================ + +.btn { + border-radius: var(--luncher-radius-sm); + padding: 12px 24px; + font-weight: 600; + transition: var(--luncher-transition); border: none; + + &:focus { + box-shadow: 0 0 0 3px var(--luncher-primary-light); + } } -.qr-code { +.btn-primary { + background: var(--luncher-primary); + color: white; + + &:hover { + background: var(--luncher-primary-hover); + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } +} + +.btn-secondary { + background: var(--luncher-bg-hover); + color: var(--luncher-text); + + &:hover { + background: var(--luncher-border); + } +} + +.btn-danger, .btn.danger { + background: var(--luncher-danger); + color: white; + + &:hover { + background: #dc2626; + } +} + +.btn-outline-primary { + background: transparent; + border: 2px solid var(--luncher-primary); + color: var(--luncher-primary); + + &:hover { + background: var(--luncher-primary); + color: white; + } +} + +// ============================================ +// CHOICES TABLE +// ============================================ + +.choices-table { + background: var(--luncher-bg-card); + border-radius: var(--luncher-radius-lg); + box-shadow: var(--luncher-shadow); + overflow: hidden; + border: 1px solid var(--luncher-border-light); + + > tbody > tr { + &:last-child > td { + border-bottom: none; + } + + > td { + padding: 0; + border-color: var(--luncher-border); + vertical-align: top; + + &:first-child { + background: var(--luncher-primary-light); + padding: 16px 20px; + font-weight: 600; + color: var(--luncher-primary); + width: 180px; + + @media (max-width: 576px) { + width: 120px; + padding: 12px 16px; + font-size: 0.9rem; + } + } + } + } + + .nested-table { + margin: 0; + + tbody tr { + transition: var(--luncher-transition); + + &:hover { + background: var(--luncher-bg-hover); + } + + &:last-child td { + border-bottom: none; + } + } + + td { + padding: 12px 16px; + border-color: var(--luncher-border-light); + color: var(--luncher-text); + } + } + + ul { + padding: 0; + margin: 8px 0 0 20px; + + li { + color: var(--luncher-text-secondary); + font-size: 0.9rem; + margin-bottom: 4px; + } + } +} + +.no-votes { text-align: center; - margin-top: 30px; + padding: 48px 24px; + color: var(--luncher-text-muted); + font-style: italic; + background: var(--luncher-bg-card); + border-radius: var(--luncher-radius-lg); + box-shadow: var(--luncher-shadow); } -.select-search-container { - margin: auto; +// ============================================ +// ACTION ICONS +// ============================================ + +.action-icon { + color: var(--luncher-action-icon); + cursor: pointer; + margin-left: 12px; + padding: 4px; + border-radius: 4px; + transition: var(--luncher-transition); + + &:hover { + background: var(--luncher-primary-light); + transform: scale(1.1); + } +} + +.buyer-icon { + color: var(--luncher-buyer-icon); + margin-left: 12px; + padding: 4px; } .trusted-icon { - color: var(--luncher-action-icon); - margin-right: 10px; + color: var(--luncher-success); + margin-right: 8px; } -.day-navigator { - display: flex; - align-items: center; - font-size: xx-large; +// ============================================ +// PIZZA DAY SECTION +// ============================================ + +.pizza-section { + background: var(--luncher-bg-card); + border-radius: var(--luncher-radius-lg); + padding: 32px; + box-shadow: var(--luncher-shadow); + text-align: center; + margin-top: 48px; + border: 1px solid var(--luncher-border-light); + + h3 { + color: var(--luncher-primary); + font-weight: 700; + margin-bottom: 16px; + } + + p { + color: var(--luncher-text-secondary); + margin-bottom: 24px; + } + + .btn + .btn { + margin-left: 16px; + } } +.pizza-order-form { + display: flex; + flex-wrap: wrap; + gap: 16px; + justify-content: center; + align-items: center; + margin-top: 24px; + + input[type="text"] { + width: auto; + min-width: 200px; + } +} + +// ============================================ +// QR CODE +// ============================================ + +.qr-code { + text-align: center; + margin-top: 32px; + padding: 24px; + background: var(--luncher-bg); + border-radius: var(--luncher-radius); + + h3 { + color: var(--luncher-text); + margin-bottom: 16px; + } + + img { + border-radius: var(--luncher-radius-sm); + box-shadow: var(--luncher-shadow); + } +} + +// ============================================ +// ALERT +// ============================================ + +.alert { + border-radius: var(--luncher-radius); + border: none; + box-shadow: var(--luncher-shadow-sm); + position: relative; + overflow: visible; + + &.alert-primary { + background: var(--luncher-primary-light); + color: var(--luncher-primary); + border-left: 4px solid var(--luncher-primary); + } + + ul { + margin-bottom: 0; + padding-left: 20px; + + li { + margin-bottom: 4px; + } + } +} + +// ============================================ +// LOADER +// ============================================ + +.loader { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: var(--luncher-bg); + padding: 24px; + + .loader-icon { + font-size: 64px; + color: var(--luncher-primary); + margin-bottom: 24px; + } + + .loader-title { + font-size: 1.5rem; + font-weight: 600; + color: var(--luncher-text); + margin-bottom: 12px; + } + + .loader-description { + color: var(--luncher-text-secondary); + max-width: 400px; + text-align: center; + line-height: 1.6; + } +} + +// ============================================ +// SELECT SEARCH +// ============================================ + +.select-search-container { + margin: auto; + + .select-search-input { + background: var(--luncher-bg); + border: 2px solid var(--luncher-border); + border-radius: var(--luncher-radius-sm); + padding: 12px 16px; + font-size: 1rem; + color: var(--luncher-text); + height: auto; + + &:focus { + border-color: var(--luncher-primary); + box-shadow: 0 0 0 3px var(--luncher-primary-light); + } + } + + .select-search-select { + background: var(--luncher-bg-card); + border: 1px solid var(--luncher-border); + border-radius: var(--luncher-radius); + box-shadow: var(--luncher-shadow-lg); + overflow: hidden; + } + + .select-search-option { + padding: 12px 16px; + color: var(--luncher-text); + + &:hover, &.is-selected { + background: var(--luncher-primary-light); + color: var(--luncher-primary); + } + } + + .select-search-group-header { + background: var(--luncher-bg-hover); + color: var(--luncher-text-secondary); + font-weight: 600; + padding: 8px 16px; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.5px; + } +} + +// ============================================ +// FOOTER +// ============================================ + +.footer { + background: var(--luncher-navbar-bg); + color: var(--luncher-navbar-text); + padding: 16px 24px; + text-align: center; + margin-top: auto; + flex-shrink: 0; + font-size: 0.9rem; + + a { + color: var(--luncher-primary); + transition: var(--luncher-transition); + + &:hover { + color: var(--luncher-primary-hover); + } + } +} + +// ============================================ +// ANIMATIONS +// ============================================ + @keyframes bounce-in { 0% { left: var(--start-left); @@ -170,28 +881,24 @@ body, 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); @@ -200,7 +907,141 @@ body, } } -// 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; -} \ No newline at end of file +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 0.3s ease; +} + +// ============================================ +// UTILITIES +// ============================================ + +.text-muted { + color: var(--luncher-text-muted) !important; +} + +.mt-3 { margin-top: 16px !important; } +.mt-5 { margin-top: 32px !important; } +.mb-3 { margin-bottom: 16px !important; } +.mb-5 { margin-bottom: 32px !important; } + +// ============================================ +// MODALS +// ============================================ + +.modal-content { + background: var(--luncher-bg-card); + border: 1px solid var(--luncher-border); + border-radius: var(--luncher-radius-lg); + box-shadow: var(--luncher-shadow-lg); +} + +.modal-header { + border-bottom: 1px solid var(--luncher-border); + padding: 20px 24px; + + .modal-title { + h2 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--luncher-text); + } + } + + .btn-close { + opacity: 0.5; + transition: var(--luncher-transition); + + &:hover { + opacity: 1; + } + } +} + +.modal-body { + padding: 24px; + color: var(--luncher-text); + + h4 { + font-size: 1rem; + font-weight: 600; + color: var(--luncher-primary); + margin-bottom: 12px; + } + + p { + color: var(--luncher-text-secondary); + font-size: 0.9rem; + line-height: 1.6; + } + + hr { + border-color: var(--luncher-border); + margin: 20px 0; + } + + .form-label { + font-weight: 500; + color: var(--luncher-text); + margin-bottom: 8px; + } +} + +.modal-footer { + border-top: 1px solid var(--luncher-border); + padding: 16px 24px; + gap: 12px; +} + +// ============================================ +// LEGACY COMPATIBILITY +// ============================================ + +.App { + text-align: center; +} + +.table { + margin-bottom: 0; + color: var(--luncher-text); + + > :not(caption) { + .action-icon { + color: var(--luncher-action-icon); + cursor: pointer; + margin-left: 10px; + padding: 0; + } + + .buyer-icon { + color: var(--luncher-buyer-icon); + margin-left: 10px; + padding: 0; + } + } + + ul { + padding: 0; + margin-left: 20px; + margin-bottom: 0; + } + + td { + vertical-align: top; + } + + > tbody > tr > td > table > tbody > tr > td { + border: none; + } +} diff --git a/client/src/App.tsx b/client/src/App.tsx index c4bbe4f..12d70b6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -344,10 +344,10 @@ function App() { const renderFoodTable = (location: Restaurant, menu: RestaurantDayMenu) => { let content; if (menu?.closed) { - content =

Zavřeno

+ content =
Zavřeno
} else if (menu?.food?.length && menu.food.length > 0) { const hideSoups = settings?.hideSoups; - content = + content =
{menu.food.map((f: Food, index: number) => (!hideSoups || !f.isSoup) && @@ -356,15 +356,17 @@ function App() { @@ -373,12 +375,18 @@ function App() {
{f.name} {f.allergens && f.allergens.length > 0 && ( - <> ({f.allergens.map((a, idx) => ( - - { - e.stopPropagation(); - window.open(LINK_ALLERGENS, '_blank'); - }}>{a} - {idx < f.allergens!.length - 1 && ','} - - ))}) + + ({f.allergens.map((a, idx) => ( + + { + e.stopPropagation(); + window.open(LINK_ALLERGENS, '_blank'); + }}>{a} + {idx < f.allergens!.length - 1 && ', '} + + ))}) + )} {f.price}
} else { - content =

Chyba načtení dat

+ content =
Chyba načtení dat
} - return -

doAddClickFoodChoice(location)}>{getLunchChoiceName(location)}

- {menu?.lastUpdate && Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}} - {content} + return +
+
+

doAddClickFoodChoice(location)}> + {getLunchChoiceName(location)} +

+ {menu?.lastUpdate && Aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}} +
+ {content} +
} @@ -421,16 +429,6 @@ function App() {
{data.isWeekend ?

Užívejte víkend :)

: <> - - - - Poslední změny: -
    -
  • Oprava parsování Sladovnické a TechTower
  • -
  • Zimní atmosféra
  • -
  • Možnost označit se jako objednávající u volby "budu objednávat"
  • -
-
{dayIndex != null &&
@@ -450,10 +448,10 @@ function App() {
- {canChangeChoice && <> + {canChangeChoice &&

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

- + {Object.entries(LunchChoice) .filter(entry => { const locationKey = entry[0] as Restaurant; @@ -463,44 +461,44 @@ function App() { Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí. {foodChoiceList && !closed && <> -

Na co dobrého? (nepovinné)

+

Na co dobrého? (nepovinné)

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

V kolik hodin preferuješ odchod?

+

V kolik hodin preferuješ odchod?

- + {Object.values(DepartureTime) .filter(time => isInTheFuture(time)) .map(time => )} } - } +
} {Object.keys(data.choices).length > 0 ? - +
{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 ( - {(locationPickCount ?? 0) > 1 ? ( - - ) : ( - )} +
{locationName} ({locationPickCount}){locationName} + {locationName} + {(locationPickCount ?? 0) > 1 && ({locationPickCount})} + - +
- {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
- {/* TODO zrefaktorovat, oddělit řádek do samostatné komponenty (a akce možná zvlášť) */} {trusted && } - {login} - {userPayload.departureTime && ({userPayload.departureTime})} - {userPayload.note && ({userPayload.note})} + {login} + {userPayload.departureTime && ({userPayload.departureTime})} + {userPayload.note && ({userPayload.note})} {login === auth.login && canChangeChoice && locationKey === LunchChoice.OBJEDNAVAM && { markAsBuyer(); @@ -569,97 +566,94 @@ function App() { )}
- :
Zatím nikdo nehlasoval...
+ :
Zatím nikdo nehlasoval...
} {dayIndex === data.todayDayIndex && -
+
{!data.pizzaDay && -
+ <> +

Pizza Day

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

{loadingPizzaDay ? - - Zjišťujeme dostupné pizzy + + 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.` : ''} -

-
- } -
+ <> +

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 && -
+
{ }} onFocus={_ => { }} /> - Poznámka: { - if (event.key === 'Enter') { - handlePizzaPoznamkaChange(); - } - event.stopPropagation(); - }} /> - +
+ + { + if (event.key === 'Enter') { + handlePizzaPoznamkaChange(); + } + event.stopPropagation(); + }} /> + +
} { - data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr ? -
-

QR platba

- QR kód -
: null + data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && +
+

QR platba

+ QR kód +
} -
+ }
} diff --git a/client/src/Login.css b/client/src/Login.css index 1b0e950..0965166 100644 --- a/client/src/Login.css +++ b/client/src/Login.css @@ -1,13 +1,89 @@ -.login { - height: 100%; +.login-page { + min-height: 100vh; display: flex; - flex-direction: column; - text-align: center; + align-items: center; justify-content: center; + background: var(--luncher-bg); + padding: 24px; } -.login-inner { +.login-card { + background: var(--luncher-bg-card); + border-radius: var(--luncher-radius-xl); + box-shadow: var(--luncher-shadow-lg); + padding: 48px; + max-width: 420px; + width: 100%; + text-align: center; + border: 1px solid var(--luncher-border-light); +} + +.login-logo { + font-size: 2.5rem; + font-weight: 700; + color: var(--luncher-text); + margin-bottom: 8px; + letter-spacing: -0.5px; +} + +.login-subtitle { + color: var(--luncher-text-secondary); + font-size: 1rem; + margin-bottom: 40px; + line-height: 1.5; +} + +.login-form { display: flex; flex-direction: column; - align-items: center; -} \ No newline at end of file + gap: 20px; +} + +.login-form label { + display: block; + text-align: left; + font-weight: 500; + color: var(--luncher-text); + margin-bottom: 8px; +} + +.login-form .hint { + font-size: 0.85rem; + color: var(--luncher-text-muted); + margin-top: 8px; + text-align: left; + line-height: 1.5; +} + +.login-form input[type="text"] { + width: 100%; + padding: 14px 18px; + font-size: 1rem; + border: 2px solid var(--luncher-border); + border-radius: var(--luncher-radius-sm); + background: var(--luncher-bg); + color: var(--luncher-text); + transition: var(--luncher-transition); +} + +.login-form input[type="text"]:hover { + border-color: var(--luncher-text-muted); +} + +.login-form input[type="text"]:focus { + border-color: var(--luncher-primary); + box-shadow: 0 0 0 3px var(--luncher-primary-light); + outline: none; +} + +.login-form input[type="text"]::placeholder { + color: var(--luncher-text-muted); +} + +.login-form .btn { + width: 100%; + padding: 14px 24px; + font-size: 1rem; + font-weight: 600; + margin-top: 8px; +} diff --git a/client/src/Login.tsx b/client/src/Login.tsx index 650d764..b5cc446 100644 --- a/client/src/Login.tsx +++ b/client/src/Login.tsx @@ -36,21 +36,35 @@ export default function Login() { }, [auth]); if (!auth?.login) { - return
-

Luncher

-

Aplikace pro profesionální management obědů

-
-

- Zobrazované jméno by mělo být vaše jméno nebo přezdívka, pod kterou vás kolegové dokáží snadno identifikovat. Jméno je možné kdykoli změnit. -

- Zobrazované jméno: { - if (event.key === 'Enter') { - doLogin() - } - }} /> - + return ( +
+
+

Luncher

+

Aplikace pro profesionální management obědů

+
+
+ + { + if (event.key === 'Enter') { + doLogin() + } + }} + /> +

+ Zadejte jméno nebo přezdívku, pod kterou vás kolegové snadno identifikují. + Jméno je možné kdykoli změnit. +

+
+ +
+
-
+ ); } return
Neplatný stav
} diff --git a/client/src/components/Footer.tsx b/client/src/components/Footer.tsx index 42b56c0..1e23b07 100644 --- a/client/src/components/Footer.tsx +++ b/client/src/components/Footer.tsx @@ -1,12 +1,12 @@ -import { Navbar } from "react-bootstrap"; - export default function Footer() { - return - 🄯 Žádná práva nevyhrazena. TODO a zdrojové kódy dostupné zde. - -} \ No newline at end of file + return ( + + ); +} diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 7803d9b..9fdd257 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Navbar, Nav, NavDropdown } from "react-bootstrap"; +import { Navbar, Nav, NavDropdown, Modal, Button } from "react-bootstrap"; import { useAuth } from "../context/auth"; import SettingsModal from "./modals/SettingsModal"; import { useSettings, ThemePreference } from "../context/settings"; @@ -9,6 +9,14 @@ import RefreshMenuModal from "./modals/RefreshMenuModal"; import { useNavigate } from "react-router"; import { STATS_URL } from "../AppRoutes"; import { FeatureRequest, getVotes, updateVote } from "../../../types"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons"; + +const CHANGELOG = [ + "Nový moderní design aplikace", + "Oprava parsování Sladovnické a TechTower", + "Možnost označit se jako objednávající u volby \"budu objednávat\"", +]; export default function Header() { const auth = useAuth(); @@ -18,8 +26,29 @@ export default function Header() { const [votingModalOpen, setVotingModalOpen] = useState(false); const [pizzaModalOpen, setPizzaModalOpen] = useState(false); const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState(false); + const [changelogModalOpen, setChangelogModalOpen] = useState(false); const [featureVotes, setFeatureVotes] = useState([]); + // Zjistíme aktuální efektivní téma (pro zobrazení správné ikony) + const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>('light'); + + useEffect(() => { + const updateEffectiveTheme = () => { + if (settings?.themePreference === 'system') { + const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + setEffectiveTheme(isDark ? 'dark' : 'light'); + } else { + setEffectiveTheme(settings?.themePreference || 'light'); + } + }; + + updateEffectiveTheme(); + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', updateEffectiveTheme); + return () => mediaQuery.removeEventListener('change', updateEffectiveTheme); + }, [settings?.themePreference]); + useEffect(() => { if (auth?.login) { getVotes().then(response => { @@ -44,6 +73,12 @@ export default function Header() { setRefreshMenuModalOpen(false); } + const toggleTheme = () => { + // Přepínáme mezi light a dark (ignorujeme system pro jednoduchost) + const newTheme: ThemePreference = effectiveTheme === 'dark' ? 'light' : 'dark'; + settings?.setThemePreference(newTheme); + } + const isValidInteger = (str: string) => { str = str.trim(); if (!str) { @@ -121,12 +156,21 @@ export default function Header() {