From ce2cf499dc5c719554a1da5cd63b624f802cc32f Mon Sep 17 00:00:00 2001 From: Michelle Date: Wed, 4 Mar 2026 10:11:35 +0100 Subject: [PATCH] feat: implement hide app name feature with toggle in admin settings and update branding context --- package-lock.json | 101 +++++++++ package.json | 2 + server/routes/branding.js | 22 ++ src/components/BrandLogo.jsx | 4 +- src/components/DateTimePicker.jsx | 126 +++++++++++ src/contexts/BrandingContext.jsx | 1 + src/i18n/de.json | 3 + src/i18n/en.json | 3 + src/index.css | 347 +++++++++++++++++++++++++++--- src/pages/Admin.jsx | 37 +++- src/pages/Calendar.jsx | 42 ++-- 11 files changed, 627 insertions(+), 61 deletions(-) create mode 100644 src/components/DateTimePicker.jsx diff --git a/package-lock.json b/package-lock.json index 54b4a18..6f30c4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "better-sqlite3": "^11.0.0", "concurrently": "^9.0.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^16.4.0", "express": "^4.21.0", "express-rate-limit": "^7.5.1", @@ -24,6 +25,7 @@ "pg": "^8.18.0", "rate-limit-redis": "^4.3.1", "react": "^18.3.0", + "react-datepicker": "^9.1.0", "react-dom": "^18.3.0", "react-hot-toast": "^2.4.0", "react-router-dom": "^6.28.0", @@ -777,6 +779,59 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -1840,6 +1895,15 @@ "node": ">=12" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -2007,6 +2071,16 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3881,6 +3955,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz", + "integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "date-fns-tz": "^3.0.0", + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "date-fns-tz": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -4510,6 +4605,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", diff --git a/package.json b/package.json index 6d607a2..6e4d06d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "better-sqlite3": "^11.0.0", "concurrently": "^9.0.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^16.4.0", "express": "^4.21.0", "express-rate-limit": "^7.5.1", @@ -28,6 +29,7 @@ "pg": "^8.18.0", "rate-limit-redis": "^4.3.1", "react": "^18.3.0", + "react-datepicker": "^9.1.0", "react-dom": "^18.3.0", "react-hot-toast": "^2.4.0", "react-router-dom": "^6.28.0", diff --git a/server/routes/branding.js b/server/routes/branding.js index 5bd3223..3a8e5f9 100644 --- a/server/routes/branding.js +++ b/server/routes/branding.js @@ -108,6 +108,8 @@ router.get('/', async (req, res) => { } } catch { /* not configured */ } + const hideAppName = await getSetting('hide_app_name'); + res.json({ appName: appName || 'Redlight', hasLogo: !!logoFile, @@ -118,6 +120,7 @@ router.get('/', async (req, res) => { privacyUrl: privacyUrl || null, oauthEnabled, oauthDisplayName, + hideAppName: hideAppName === 'true', }); } catch (err) { log.branding.error('Get branding error:', err); @@ -282,4 +285,23 @@ router.put('/privacy-url', authenticateToken, requireAdmin, async (req, res) => } }); +// PUT /api/branding/hide-app-name - Toggle app name visibility (admin only) +router.put('/hide-app-name', authenticateToken, requireAdmin, async (req, res) => { + try { + const { hideAppName } = req.body; + if (typeof hideAppName !== 'boolean') { + return res.status(400).json({ error: 'hideAppName must be a boolean' }); + } + if (hideAppName) { + await setSetting('hide_app_name', 'true'); + } else { + await deleteSetting('hide_app_name'); + } + res.json({ hideAppName }); + } catch (err) { + log.branding.error('Update hide app name error:', err); + res.status(500).json({ error: 'Could not update setting' }); + } +}); + export default router; diff --git a/src/components/BrandLogo.jsx b/src/components/BrandLogo.jsx index cd6e92a..5964e6c 100644 --- a/src/components/BrandLogo.jsx +++ b/src/components/BrandLogo.jsx @@ -8,7 +8,7 @@ const sizes = { }; export default function BrandLogo({ size = 'md', className = '' }) { - const { appName, hasLogo, logoUrl } = useBranding(); + const { appName, hasLogo, logoUrl, hideAppName } = useBranding(); const s = sizes[size] || sizes.md; if (hasLogo && logoUrl) { @@ -19,7 +19,7 @@ export default function BrandLogo({ size = 'md', className = '' }) { alt={appName} className={`${s.box} ${s.rounded} object-contain`} /> - {appName} + {!hideAppName && {appName}} ); } diff --git a/src/components/DateTimePicker.jsx b/src/components/DateTimePicker.jsx new file mode 100644 index 0000000..b2ab1e5 --- /dev/null +++ b/src/components/DateTimePicker.jsx @@ -0,0 +1,126 @@ +import ReactDatePicker from 'react-datepicker'; +import { de } from 'date-fns/locale'; +import { Calendar as CalendarIcon, Clock, X } from 'lucide-react'; +import { forwardRef } from 'react'; +import 'react-datepicker/dist/react-datepicker.css'; + +/** + * Custom DateTimePicker that reads the app's CSS variables and + * fully matches whatever theme is active. + * + * Props: + * value – ISO datetime string (or '') + * onChange – (isoString) => void + * label – string + * required – bool + * minDate – Date | null + * icon – 'calendar' (default) | 'clock' + */ +export default function DateTimePicker({ + value, + onChange, + label, + required = false, + minDate = null, + icon = 'calendar', +}) { + const selected = value ? new Date(value) : null; + + const handleChange = (date) => { + if (!date) { onChange(''); return; } + // Produce local datetime string yyyy-MM-ddTHH:mm + const y = date.getFullYear(); + const mo = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + const h = String(date.getHours()).padStart(2, '0'); + const mi = String(date.getMinutes()).padStart(2, '0'); + onChange(`${y}-${mo}-${d}T${h}:${mi}`); + }; + + const Icon = icon === 'clock' ? Clock : CalendarIcon; + + return ( +
+ {label && ( + + )} +
+ + } + calendarClassName="dtp-calendar" + dayClassName={(date) => 'dtp-day'} + timeClassName={() => 'dtp-time-item'} + renderCustomHeader={CalendarHeader} + isClearable + clearButtonClassName="dtp-clear-btn" + wrapperClassName="dtp-dp-wrapper" + /> +
+
+ ); +} + +// ── Custom Input ───────────────────────────────────────────────────────────── +const CustomInput = forwardRef(({ value, onClick, onClear, ...rest }, ref) => ( + +)); +CustomInput.displayName = 'CustomInput'; + +// ── Custom Header ───────────────────────────────────────────────────────────── +function CalendarHeader({ + date, + decreaseMonth, increaseMonth, + prevMonthButtonDisabled, nextMonthButtonDisabled, +}) { + const label = date.toLocaleString('de', { month: 'long', year: 'numeric' }); + return ( +
+ + {label} + +
+ ); +} diff --git a/src/contexts/BrandingContext.jsx b/src/contexts/BrandingContext.jsx index 6f8102d..56d5f66 100644 --- a/src/contexts/BrandingContext.jsx +++ b/src/contexts/BrandingContext.jsx @@ -13,6 +13,7 @@ export function BrandingProvider({ children }) { defaultTheme: null, imprintUrl: null, privacyUrl: null, + hideAppName: false, }); const fetchBranding = useCallback(async () => { diff --git a/src/i18n/de.json b/src/i18n/de.json index 9a923a6..486497b 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -373,6 +373,9 @@ "appNameLabel": "App-Name", "appNameUpdated": "App-Name aktualisiert", "appNameUpdateFailed": "App-Name konnte nicht aktualisiert werden", + "hideAppNameLabel": "App-Namen ausblenden", + "hideAppNameHint": "Nur das Logo anzeigen, den App-Namen daneben ausblenden.", + "hideAppNameFailed": "Einstellung konnte nicht gespeichert werden", "defaultThemeLabel": "Standard-Theme", "defaultThemeDesc": "Wird für nicht angemeldete Seiten (Gast-Join, Login, Startseite) verwendet, wenn keine persönliche Einstellung gesetzt ist.", "defaultThemeSaved": "Standard-Theme gespeichert", diff --git a/src/i18n/en.json b/src/i18n/en.json index 7d62fc2..a1f8f06 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -373,6 +373,9 @@ "appNameLabel": "App name", "appNameUpdated": "App name updated", "appNameUpdateFailed": "Could not update app name", + "hideAppNameLabel": "Hide app name", + "hideAppNameHint": "Only show the logo, hide the app name text next to it.", + "hideAppNameFailed": "Could not update setting", "defaultThemeLabel": "Default Theme", "defaultThemeDesc": "Applied to unauthenticated pages (guest join, login, home) when no personal preference is set.", "defaultThemeSaved": "Default theme saved", diff --git a/src/index.css b/src/index.css index dbd44e4..2160055 100644 --- a/src/index.css +++ b/src/index.css @@ -714,39 +714,6 @@ transition-all duration-200; } - /* ── Styled date/time picker wrapper ── */ - .datetime-picker { - @apply relative; - } - - .datetime-picker .datetime-icon { - @apply absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-th-text-s z-10; - } - - .datetime-picker input[type="datetime-local"], - .datetime-picker input[type="date"], - .datetime-picker input[type="time"] { - @apply w-full pl-11 pr-3 py-2.5 rounded-lg - bg-th-input text-th-text - border border-th-input-b - focus:outline-none focus:ring-2 focus:ring-th-ring focus:border-transparent - transition-all duration-200; - font-variant-numeric: tabular-nums; - } - - .datetime-picker input::-webkit-calendar-picker-indicator { - cursor: pointer; - border-radius: 4px; - padding: 4px; - opacity: 0.6; - transition: opacity 0.15s; - filter: var(--picker-icon-filter, none); - } - - .datetime-picker input::-webkit-calendar-picker-indicator:hover { - opacity: 1; - } - .card { @apply bg-th-card rounded-xl border border-th-border shadow-th transition-all duration-200; @@ -772,3 +739,317 @@ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); } } + +/* ═══════════════════════════════════════════════════════════════ + CUSTOM DATE/TIME PICKER — fully themed via CSS variables + ═══════════════════════════════════════════════════════════════ */ + +/* ── Wrapper & trigger button ─────────────────────────────────── */ +.dtp-dp-wrapper { + width: 100%; +} + +.dtp-input-wrap { + position: relative; + width: 100%; +} + +.dtp-icon { + position: absolute; + left: 0.875rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); + pointer-events: none; + z-index: 1; +} + +.dtp-custom-input { + width: 100%; + display: flex; + align-items: center; + padding: 0.625rem 0.875rem 0.625rem 2.5rem; + border-radius: 0.5rem; + background: var(--input-bg); + border: 1px solid var(--input-border); + color: var(--text-primary); + font-size: 0.875rem; + text-align: left; + transition: border-color 0.15s, box-shadow 0.15s; + cursor: pointer; + line-height: 1.5; +} + +.dtp-custom-input:focus, +.dtp-custom-input:focus-visible { + outline: none; + border-color: transparent; + box-shadow: 0 0 0 2px var(--ring); +} + +.dtp-custom-input:hover { + border-color: var(--accent); +} + +.dtp-placeholder { + color: var(--text-secondary); +} + +.dtp-value { + color: var(--text-primary); + font-variant-numeric: tabular-nums; +} + +/* ── Popper & calendar container ──────────────────────────────── */ +.react-datepicker-popper { + z-index: 100 !important; +} + +.dtp-calendar.react-datepicker { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 0.75rem; + box-shadow: 0 10px 25px -5px var(--shadow-color), 0 4px 10px -6px var(--shadow-color); + font-family: inherit; + font-size: 0.8125rem; + color: var(--text-primary); + overflow: hidden; + display: flex; +} + +/* hide the default triangle/arrow */ +.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle, +.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle { + display: none; +} + +/* ── Header (custom rendered) ─────────────────────────────────── */ +.dtp-calendar .react-datepicker__header { + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + padding: 0; +} + +.dtp-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 0.75rem 0.5rem; +} + +.dtp-month-label { + font-weight: 600; + font-size: 0.875rem; + color: var(--text-primary); + letter-spacing: 0.01em; +} + +.dtp-nav-btn { + width: 1.75rem; + height: 1.75rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + background: transparent; + border: none; + color: var(--text-secondary); + font-size: 1.2rem; + cursor: pointer; + transition: background 0.15s, color 0.15s; + line-height: 1; + padding-bottom: 1px; +} + +.dtp-nav-btn:hover:not(:disabled) { + background: var(--hover-bg); + color: var(--text-primary); +} + +.dtp-nav-btn:disabled { + opacity: 0.3; + cursor: default; +} + +/* ── Day names row ────────────────────────────────────────────── */ +.dtp-calendar .react-datepicker__day-names { + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-around; + padding: 0.25rem 0.5rem 0.375rem; + margin: 0; +} + +.dtp-calendar .react-datepicker__day-name { + color: var(--text-secondary); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + width: 2rem; + text-align: center; + line-height: 1.5; +} + +/* ── Month / days grid ────────────────────────────────────────── */ +.dtp-calendar .react-datepicker__month { + background: var(--card-bg); + padding: 0.5rem; + margin: 0; +} + +.dtp-calendar .react-datepicker__week { + display: flex; + justify-content: space-around; +} + +.dtp-calendar .react-datepicker__day { + width: 2rem; + height: 2rem; + line-height: 2rem; + border-radius: 0.375rem; + color: var(--text-primary); + font-size: 0.8125rem; + transition: background 0.12s, color 0.12s; + margin: 0.1rem; + text-align: center; +} + +.dtp-calendar .react-datepicker__day:hover:not(.react-datepicker__day--selected):not(.react-datepicker__day--disabled) { + background: var(--hover-bg); + color: var(--text-primary); + border-radius: 0.375rem; +} + +.dtp-calendar .react-datepicker__day--selected, +.dtp-calendar .react-datepicker__day--keyboard-selected { + background: var(--accent) !important; + color: var(--accent-text) !important; + border-radius: 0.375rem; + font-weight: 600; +} + +.dtp-calendar .react-datepicker__day--today:not(.react-datepicker__day--selected) { + font-weight: 700; + color: var(--accent); + position: relative; +} + +.dtp-calendar .react-datepicker__day--today:not(.react-datepicker__day--selected)::after { + content: ''; + position: absolute; + bottom: 3px; + left: 50%; + transform: translateX(-50%); + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--accent); +} + +.dtp-calendar .react-datepicker__day--outside-month { + color: var(--text-secondary); + opacity: 0.4; +} + +.dtp-calendar .react-datepicker__day--disabled { + color: var(--text-secondary); + opacity: 0.3; + cursor: not-allowed; +} + +/* ── Time column ──────────────────────────────────────────────── */ +.dtp-calendar .react-datepicker__time-container { + border-left: 1px solid var(--border); + background: var(--card-bg); + width: 90px; +} + +.dtp-calendar .react-datepicker__time-container .react-datepicker__header { + padding: 0.5rem; + text-align: center; +} + +.dtp-calendar .react-datepicker__header--time { + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + padding: 0.5rem !important; +} + +.dtp-calendar .react-datepicker__header--time .react-datepicker-time__header { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.dtp-calendar .react-datepicker__time { + background: var(--card-bg); +} + +.dtp-calendar .react-datepicker__time-box { + width: 90px !important; +} + +.dtp-calendar .react-datepicker__time-list { + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.dtp-calendar .react-datepicker__time-list-item { + display: flex; + align-items: center; + justify-content: center; + padding: 0.375rem 0 !important; + height: auto !important; + font-size: 0.8125rem; + color: var(--text-primary); + transition: background 0.1s, color 0.1s; + border-radius: 0; +} + +.dtp-calendar .react-datepicker__time-list-item:hover:not(.react-datepicker__time-list-item--selected) { + background: var(--hover-bg) !important; + color: var(--text-primary); +} + +.dtp-calendar .react-datepicker__time-list-item--selected { + background: var(--accent) !important; + color: var(--accent-text) !important; + font-weight: 600; +} + +.dtp-calendar .react-datepicker__time-list-item--disabled { + color: var(--text-secondary); + opacity: 0.3; +} + +/* ── Clear button ─────────────────────────────────────────────── */ +.react-datepicker__close-icon { + right: 0.5rem; + padding: 0; +} + +.react-datepicker__close-icon::after { + background: var(--bg-tertiary) !important; + color: var(--text-secondary) !important; + font-size: 0.875rem; + width: 1.25rem; + height: 1.25rem; + line-height: 1.25rem; + border-radius: 0.25rem; + transition: background 0.15s, color 0.15s; +} + +.react-datepicker__close-icon:hover::after { + background: var(--error) !important; + color: #fff !important; +} + +/* ── Month navigation (react-datepicker default, hidden since custom header used) ── */ +.dtp-calendar .react-datepicker__navigation { + display: none; +} + diff --git a/src/pages/Admin.jsx b/src/pages/Admin.jsx index ff58771..912e87c 100644 --- a/src/pages/Admin.jsx +++ b/src/pages/Admin.jsx @@ -16,7 +16,7 @@ import toast from 'react-hot-toast'; export default function Admin() { const { user } = useAuth(); const { t, language } = useLanguage(); - const { appName, hasLogo, logoUrl, defaultTheme, registrationMode, imprintUrl, privacyUrl, refreshBranding } = useBranding(); + const { appName, hasLogo, logoUrl, defaultTheme, registrationMode, imprintUrl, privacyUrl, hideAppName, refreshBranding } = useBranding(); const navigate = useNavigate(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); @@ -47,6 +47,7 @@ export default function Admin() { const [savingImprintUrl, setSavingImprintUrl] = useState(false); const [editPrivacyUrl, setEditPrivacyUrl] = useState(''); const [savingPrivacyUrl, setSavingPrivacyUrl] = useState(false); + const [savingHideAppName, setSavingHideAppName] = useState(false); // OAuth state const [oauthConfig, setOauthConfig] = useState(null); @@ -168,6 +169,18 @@ export default function Admin() { } }; + const handleHideAppNameToggle = async (value) => { + setSavingHideAppName(true); + try { + await api.put('/branding/hide-app-name', { hideAppName: value }); + refreshBranding(); + } catch { + toast.error(t('admin.hideAppNameFailed')); + } finally { + setSavingHideAppName(false); + } + }; + const handleAppNameSave = async () => { if (!editAppName.trim()) return; setSavingName(true); @@ -447,6 +460,28 @@ export default function Admin() { {savingName ? : t('common.save')} + {hasLogo && ( +
+
+

{t('admin.hideAppNameLabel')}

+

{t('admin.hideAppNameHint')}

+
+ +
+ )} diff --git a/src/pages/Calendar.jsx b/src/pages/Calendar.jsx index 421b4b4..df4aed5 100644 --- a/src/pages/Calendar.jsx +++ b/src/pages/Calendar.jsx @@ -1,12 +1,13 @@ import { useState, useEffect, useMemo } from 'react'; import { - ChevronLeft, ChevronRight, Plus, Calendar as CalendarIcon, Clock, Video, + ChevronLeft, ChevronRight, Plus, Clock, Video, Loader2, Download, Share2, Globe, Trash2, Edit, X, UserPlus, Send, ExternalLink, } from 'lucide-react'; import api from '../services/api'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import Modal from '../components/Modal'; +import DateTimePicker from '../components/DateTimePicker'; import toast from 'react-hot-toast'; const COLORS = ['#6366f1', '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6']; @@ -533,30 +534,21 @@ export default function Calendar() {
-
- -
- - setForm({ ...form, start_time: e.target.value })} - required - /> -
-
-
- -
- - setForm({ ...form, end_time: e.target.value })} - required - /> -
-
+ setForm({ ...form, start_time: v })} + required + icon="calendar" + /> + setForm({ ...form, end_time: v })} + required + icon="clock" + minDate={form.start_time ? new Date(form.start_time) : null} + />