feat: implement hide app name feature with toggle in admin settings and update branding context
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
This commit is contained in:
101
package-lock.json
generated
101
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
"concurrently": "^9.0.0",
|
"concurrently": "^9.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.0",
|
"dotenv": "^16.4.0",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"express-rate-limit": "^7.5.1",
|
"express-rate-limit": "^7.5.1",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"rate-limit-redis": "^4.3.1",
|
"rate-limit-redis": "^4.3.1",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^18.3.0",
|
"react-dom": "^18.3.0",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
@@ -777,6 +779,59 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@ioredis/commands": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
|
||||||
@@ -1840,6 +1895,15 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/cluster-key-slot": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
"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==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -3881,6 +3955,27 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
@@ -4510,6 +4605,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.19",
|
"version": "3.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
"concurrently": "^9.0.0",
|
"concurrently": "^9.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.0",
|
"dotenv": "^16.4.0",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"express-rate-limit": "^7.5.1",
|
"express-rate-limit": "^7.5.1",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"rate-limit-redis": "^4.3.1",
|
"rate-limit-redis": "^4.3.1",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^18.3.0",
|
"react-dom": "^18.3.0",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ router.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
} catch { /* not configured */ }
|
} catch { /* not configured */ }
|
||||||
|
|
||||||
|
const hideAppName = await getSetting('hide_app_name');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
appName: appName || 'Redlight',
|
appName: appName || 'Redlight',
|
||||||
hasLogo: !!logoFile,
|
hasLogo: !!logoFile,
|
||||||
@@ -118,6 +120,7 @@ router.get('/', async (req, res) => {
|
|||||||
privacyUrl: privacyUrl || null,
|
privacyUrl: privacyUrl || null,
|
||||||
oauthEnabled,
|
oauthEnabled,
|
||||||
oauthDisplayName,
|
oauthDisplayName,
|
||||||
|
hideAppName: hideAppName === 'true',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.branding.error('Get branding error:', 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;
|
export default router;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const sizes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function BrandLogo({ size = 'md', className = '' }) {
|
export default function BrandLogo({ size = 'md', className = '' }) {
|
||||||
const { appName, hasLogo, logoUrl } = useBranding();
|
const { appName, hasLogo, logoUrl, hideAppName } = useBranding();
|
||||||
const s = sizes[size] || sizes.md;
|
const s = sizes[size] || sizes.md;
|
||||||
|
|
||||||
if (hasLogo && logoUrl) {
|
if (hasLogo && logoUrl) {
|
||||||
@@ -19,7 +19,7 @@ export default function BrandLogo({ size = 'md', className = '' }) {
|
|||||||
alt={appName}
|
alt={appName}
|
||||||
className={`${s.box} ${s.rounded} object-contain`}
|
className={`${s.box} ${s.rounded} object-contain`}
|
||||||
/>
|
/>
|
||||||
<span className={`${s.text} font-bold gradient-text`}>{appName}</span>
|
{!hideAppName && <span className={`${s.text} font-bold gradient-text`}>{appName}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/components/DateTimePicker.jsx
Normal file
126
src/components/DateTimePicker.jsx
Normal file
@@ -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 (
|
||||||
|
<div className="dtp-wrapper">
|
||||||
|
{label && (
|
||||||
|
<label className="block text-sm font-medium text-th-text mb-1.5">
|
||||||
|
{label}{required && ' *'}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className="dtp-input-wrap">
|
||||||
|
<Icon size={15} className="dtp-icon" />
|
||||||
|
<ReactDatePicker
|
||||||
|
selected={selected}
|
||||||
|
onChange={handleChange}
|
||||||
|
showTimeSelect
|
||||||
|
timeFormat="HH:mm"
|
||||||
|
timeIntervals={15}
|
||||||
|
dateFormat="dd.MM.yyyy HH:mm"
|
||||||
|
locale={de}
|
||||||
|
minDate={minDate}
|
||||||
|
required={required}
|
||||||
|
popperPlacement="bottom-start"
|
||||||
|
popperModifiers={[
|
||||||
|
{ name: 'offset', options: { offset: [0, 4] } },
|
||||||
|
{ name: 'preventOverflow', options: { rootBoundary: 'viewport', tether: false, altAxis: true } },
|
||||||
|
]}
|
||||||
|
customInput={<CustomInput />}
|
||||||
|
calendarClassName="dtp-calendar"
|
||||||
|
dayClassName={(date) => 'dtp-day'}
|
||||||
|
timeClassName={() => 'dtp-time-item'}
|
||||||
|
renderCustomHeader={CalendarHeader}
|
||||||
|
isClearable
|
||||||
|
clearButtonClassName="dtp-clear-btn"
|
||||||
|
wrapperClassName="dtp-dp-wrapper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Custom Input ─────────────────────────────────────────────────────────────
|
||||||
|
const CustomInput = forwardRef(({ value, onClick, onClear, ...rest }, ref) => (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className="dtp-custom-input"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<span className={value ? 'dtp-value' : 'dtp-placeholder'}>
|
||||||
|
{value || 'Datum & Uhrzeit wählen …'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
));
|
||||||
|
CustomInput.displayName = 'CustomInput';
|
||||||
|
|
||||||
|
// ── Custom Header ─────────────────────────────────────────────────────────────
|
||||||
|
function CalendarHeader({
|
||||||
|
date,
|
||||||
|
decreaseMonth, increaseMonth,
|
||||||
|
prevMonthButtonDisabled, nextMonthButtonDisabled,
|
||||||
|
}) {
|
||||||
|
const label = date.toLocaleString('de', { month: 'long', year: 'numeric' });
|
||||||
|
return (
|
||||||
|
<div className="dtp-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={decreaseMonth}
|
||||||
|
disabled={prevMonthButtonDisabled}
|
||||||
|
className="dtp-nav-btn"
|
||||||
|
aria-label="Vorheriger Monat"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<span className="dtp-month-label">{label}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={increaseMonth}
|
||||||
|
disabled={nextMonthButtonDisabled}
|
||||||
|
className="dtp-nav-btn"
|
||||||
|
aria-label="Nächster Monat"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export function BrandingProvider({ children }) {
|
|||||||
defaultTheme: null,
|
defaultTheme: null,
|
||||||
imprintUrl: null,
|
imprintUrl: null,
|
||||||
privacyUrl: null,
|
privacyUrl: null,
|
||||||
|
hideAppName: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchBranding = useCallback(async () => {
|
const fetchBranding = useCallback(async () => {
|
||||||
|
|||||||
@@ -373,6 +373,9 @@
|
|||||||
"appNameLabel": "App-Name",
|
"appNameLabel": "App-Name",
|
||||||
"appNameUpdated": "App-Name aktualisiert",
|
"appNameUpdated": "App-Name aktualisiert",
|
||||||
"appNameUpdateFailed": "App-Name konnte nicht aktualisiert werden",
|
"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",
|
"defaultThemeLabel": "Standard-Theme",
|
||||||
"defaultThemeDesc": "Wird für nicht angemeldete Seiten (Gast-Join, Login, Startseite) verwendet, wenn keine persönliche Einstellung gesetzt ist.",
|
"defaultThemeDesc": "Wird für nicht angemeldete Seiten (Gast-Join, Login, Startseite) verwendet, wenn keine persönliche Einstellung gesetzt ist.",
|
||||||
"defaultThemeSaved": "Standard-Theme gespeichert",
|
"defaultThemeSaved": "Standard-Theme gespeichert",
|
||||||
|
|||||||
@@ -373,6 +373,9 @@
|
|||||||
"appNameLabel": "App name",
|
"appNameLabel": "App name",
|
||||||
"appNameUpdated": "App name updated",
|
"appNameUpdated": "App name updated",
|
||||||
"appNameUpdateFailed": "Could not update app name",
|
"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",
|
"defaultThemeLabel": "Default Theme",
|
||||||
"defaultThemeDesc": "Applied to unauthenticated pages (guest join, login, home) when no personal preference is set.",
|
"defaultThemeDesc": "Applied to unauthenticated pages (guest join, login, home) when no personal preference is set.",
|
||||||
"defaultThemeSaved": "Default theme saved",
|
"defaultThemeSaved": "Default theme saved",
|
||||||
|
|||||||
347
src/index.css
347
src/index.css
@@ -714,39 +714,6 @@
|
|||||||
transition-all duration-200;
|
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 {
|
.card {
|
||||||
@apply bg-th-card rounded-xl border border-th-border
|
@apply bg-th-card rounded-xl border border-th-border
|
||||||
shadow-th transition-all duration-200;
|
shadow-th transition-all duration-200;
|
||||||
@@ -772,3 +739,317 @@
|
|||||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import toast from 'react-hot-toast';
|
|||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { t, language } = useLanguage();
|
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 navigate = useNavigate();
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -47,6 +47,7 @@ export default function Admin() {
|
|||||||
const [savingImprintUrl, setSavingImprintUrl] = useState(false);
|
const [savingImprintUrl, setSavingImprintUrl] = useState(false);
|
||||||
const [editPrivacyUrl, setEditPrivacyUrl] = useState('');
|
const [editPrivacyUrl, setEditPrivacyUrl] = useState('');
|
||||||
const [savingPrivacyUrl, setSavingPrivacyUrl] = useState(false);
|
const [savingPrivacyUrl, setSavingPrivacyUrl] = useState(false);
|
||||||
|
const [savingHideAppName, setSavingHideAppName] = useState(false);
|
||||||
|
|
||||||
// OAuth state
|
// OAuth state
|
||||||
const [oauthConfig, setOauthConfig] = useState(null);
|
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 () => {
|
const handleAppNameSave = async () => {
|
||||||
if (!editAppName.trim()) return;
|
if (!editAppName.trim()) return;
|
||||||
setSavingName(true);
|
setSavingName(true);
|
||||||
@@ -447,6 +460,28 @@ export default function Admin() {
|
|||||||
{savingName ? <Loader2 size={14} className="animate-spin" /> : t('common.save')}
|
{savingName ? <Loader2 size={14} className="animate-spin" /> : t('common.save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{hasLogo && (
|
||||||
|
<div className="flex items-center justify-between mt-3 p-3 rounded-lg bg-th-bg-s border border-th-border">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium text-th-text">{t('admin.hideAppNameLabel')}</p>
|
||||||
|
<p className="text-xs text-th-text-s mt-0.5">{t('admin.hideAppNameHint')}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={savingHideAppName}
|
||||||
|
onClick={() => handleHideAppNameToggle(!hideAppName)}
|
||||||
|
className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-1 disabled:opacity-50 ml-4 ${
|
||||||
|
hideAppName ? 'bg-th-accent' : 'bg-th-border'
|
||||||
|
}`}
|
||||||
|
aria-checked={hideAppName}
|
||||||
|
role="switch"
|
||||||
|
>
|
||||||
|
<span className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
hideAppName ? 'translate-x-4' : 'translate-x-0'
|
||||||
|
}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
ChevronLeft, ChevronRight, Plus, Calendar as CalendarIcon, Clock, Video,
|
ChevronLeft, ChevronRight, Plus, Clock, Video,
|
||||||
Loader2, Download, Share2, Globe, Trash2, Edit, X, UserPlus, Send, ExternalLink,
|
Loader2, Download, Share2, Globe, Trash2, Edit, X, UserPlus, Send, ExternalLink,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import Modal from '../components/Modal';
|
import Modal from '../components/Modal';
|
||||||
|
import DateTimePicker from '../components/DateTimePicker';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const COLORS = ['#6366f1', '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6'];
|
const COLORS = ['#6366f1', '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6'];
|
||||||
@@ -533,30 +534,21 @@ export default function Calendar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<DateTimePicker
|
||||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('calendar.startTime')} *</label>
|
label={t('calendar.startTime')}
|
||||||
<div className="datetime-picker">
|
value={form.start_time}
|
||||||
<CalendarIcon size={16} className="datetime-icon" />
|
onChange={v => setForm({ ...form, start_time: v })}
|
||||||
<input
|
required
|
||||||
type="datetime-local"
|
icon="calendar"
|
||||||
value={form.start_time}
|
/>
|
||||||
onChange={e => setForm({ ...form, start_time: e.target.value })}
|
<DateTimePicker
|
||||||
required
|
label={t('calendar.endTime')}
|
||||||
/>
|
value={form.end_time}
|
||||||
</div>
|
onChange={v => setForm({ ...form, end_time: v })}
|
||||||
</div>
|
required
|
||||||
<div>
|
icon="clock"
|
||||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('calendar.endTime')} *</label>
|
minDate={form.start_time ? new Date(form.start_time) : null}
|
||||||
<div className="datetime-picker">
|
/>
|
||||||
<Clock size={16} className="datetime-icon" />
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
value={form.end_time}
|
|
||||||
onChange={e => setForm({ ...form, end_time: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 -mt-2 text-xs text-th-text-s">
|
<div className="flex items-center gap-1.5 -mt-2 text-xs text-th-text-s">
|
||||||
<Globe size={12} className="flex-shrink-0" />
|
<Globe size={12} className="flex-shrink-0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user