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}
+ />