mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:46:39 +02:00
feat: Primärfarbe (Button-/Akzentfarbe) im Branding konfigurierbar
- Backend: primaryColor-Feld in GET/POST /settings/branding (Redis, Hex-Validierung) - Frontend AppLayout: --color-primary/--color-primary-hover/--color-primary-light CSS-Variablen dynamisch aus Branding-Einstellungen setzen - AdminCustomizePage: neuer Card-Block „Button-/Primärfarbe" mit 6 Farbpresets, Color-Picker, Hex-Input und Live-Vorschau (Solid + Outline Button) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0a5a37d169
commit
bfaf718596
3 changed files with 150 additions and 1 deletions
|
|
@ -164,11 +164,13 @@ export class SettingsController {
|
|||
loginBgColor1: string | null;
|
||||
loginBgColor2: string | null;
|
||||
loginBgImage: string | null;
|
||||
primaryColor: string | null;
|
||||
}> {
|
||||
const raw = await this.redis.get(BRANDING_LOGO_KEY);
|
||||
const empty = {
|
||||
logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null,
|
||||
loginBgType: null, loginBgColor1: null, loginBgColor2: null, loginBgImage: null,
|
||||
primaryColor: null,
|
||||
};
|
||||
if (!raw) return empty;
|
||||
|
||||
|
|
@ -177,6 +179,8 @@ export class SettingsController {
|
|||
const loginBgType = ['gradient', 'solid', 'image'].includes(data.loginBgType)
|
||||
? (data.loginBgType as 'gradient' | 'solid' | 'image')
|
||||
: null;
|
||||
// Hex-Farbe validieren (#rrggbb)
|
||||
const hexColorRe = /^#[0-9a-fA-F]{6}$/;
|
||||
return {
|
||||
logo: data.logo || null,
|
||||
sidebarColor: data.sidebarColor || null,
|
||||
|
|
@ -186,6 +190,9 @@ export class SettingsController {
|
|||
loginBgColor1: data.loginBgColor1 || null,
|
||||
loginBgColor2: data.loginBgColor2 || null,
|
||||
loginBgImage: data.loginBgImage || null,
|
||||
primaryColor: typeof data.primaryColor === 'string' && hexColorRe.test(data.primaryColor)
|
||||
? data.primaryColor
|
||||
: null,
|
||||
};
|
||||
} catch {
|
||||
// Legacy: nur Logo als String
|
||||
|
|
@ -211,6 +218,7 @@ export class SettingsController {
|
|||
loginBgColor1?: string | null;
|
||||
loginBgColor2?: string | null;
|
||||
loginBgImage?: string | null;
|
||||
primaryColor?: string | null;
|
||||
},
|
||||
): Promise<{ success: boolean }> {
|
||||
if (body.logo && body.logo.length > 500_000) {
|
||||
|
|
@ -234,6 +242,12 @@ export class SettingsController {
|
|||
? Math.min(Math.max(Math.round(body.sidebarWidth), 200), 360)
|
||||
: null;
|
||||
|
||||
// Primärfarbe validieren — muss gültiges #rrggbb sein
|
||||
const hexColorRe = /^#[0-9a-fA-F]{6}$/;
|
||||
const primaryColor = typeof body.primaryColor === 'string' && hexColorRe.test(body.primaryColor)
|
||||
? body.primaryColor
|
||||
: null;
|
||||
|
||||
const data = {
|
||||
logo: body.logo || null,
|
||||
sidebarColor: body.sidebarColor || null,
|
||||
|
|
@ -243,6 +257,7 @@ export class SettingsController {
|
|||
loginBgColor1: body.loginBgColor1 || null,
|
||||
loginBgColor2: body.loginBgColor2 || null,
|
||||
loginBgImage: body.loginBgImage || null,
|
||||
primaryColor,
|
||||
};
|
||||
|
||||
await this.redis.set(BRANDING_LOGO_KEY, JSON.stringify(data));
|
||||
|
|
|
|||
|
|
@ -11,8 +11,18 @@ interface BrandingData {
|
|||
loginBgColor1: string | null;
|
||||
loginBgColor2: string | null;
|
||||
loginBgImage: string | null;
|
||||
primaryColor: string | null;
|
||||
}
|
||||
|
||||
const PRIMARY_COLOR_PRESETS = [
|
||||
{ label: 'Standard', color: '#1040bb' },
|
||||
{ label: 'Indigo', color: '#4f46e5' },
|
||||
{ label: 'Grün', color: '#059669' },
|
||||
{ label: 'Orange', color: '#d97706' },
|
||||
{ label: 'Lila', color: '#7c3aed' },
|
||||
{ label: 'Rot', color: '#dc2626' },
|
||||
];
|
||||
|
||||
const SIDEBAR_PRESETS = [
|
||||
{ label: 'Standard', color: '#1e293b' },
|
||||
{ label: 'Dunkelblau', color: '#0f172a' },
|
||||
|
|
@ -76,6 +86,7 @@ export function AdminCustomizePage() {
|
|||
const [loginBgColor1, setLoginBgColor1] = useState<string>('#1a56db');
|
||||
const [loginBgColor2, setLoginBgColor2] = useState<string>('#1e3a5f');
|
||||
const [loginBgImage, setLoginBgImage] = useState<string | null>(null);
|
||||
const [primaryColor, setPrimaryColor] = useState<string>('#1040bb');
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
|
||||
|
|
@ -97,6 +108,7 @@ export function AdminCustomizePage() {
|
|||
setLoginBgColor1(data.loginBgColor1 || '#1a56db');
|
||||
setLoginBgColor2(data.loginBgColor2 || '#1e3a5f');
|
||||
setLoginBgImage(data.loginBgImage || null);
|
||||
setPrimaryColor(data.primaryColor || '#1040bb');
|
||||
setHasChanges(false);
|
||||
}
|
||||
}, [data]);
|
||||
|
|
@ -111,6 +123,7 @@ export function AdminCustomizePage() {
|
|||
loginBgColor1: string;
|
||||
loginBgColor2: string;
|
||||
loginBgImage: string | null;
|
||||
primaryColor: string;
|
||||
}) => {
|
||||
const res = await api.post('/settings/branding', branding);
|
||||
return res.data;
|
||||
|
|
@ -154,7 +167,7 @@ export function AdminCustomizePage() {
|
|||
};
|
||||
|
||||
const handleSave = () => {
|
||||
saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth, loginBgType, loginBgColor1, loginBgColor2, loginBgImage });
|
||||
saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth, loginBgType, loginBgColor1, loginBgColor2, loginBgImage, primaryColor });
|
||||
};
|
||||
|
||||
const handleLoginBgFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -733,6 +746,103 @@ export function AdminCustomizePage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Button-/Primärfarbe */}
|
||||
<div style={cardStyle}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.5rem' }}>
|
||||
Button-/Primärfarbe
|
||||
</h3>
|
||||
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', marginBottom: '1.25rem' }}>
|
||||
Farbe für Buttons, aktive Links und Akzente in der gesamten Oberfläche.
|
||||
</p>
|
||||
|
||||
{/* Voreinstellungen */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={labelStyle}>Voreinstellungen</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
{PRIMARY_COLOR_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset.color}
|
||||
onClick={() => { setPrimaryColor(preset.color); setHasChanges(true); }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '0.375rem 0.75rem',
|
||||
background: primaryColor === preset.color ? 'var(--color-primary-light)' : 'none',
|
||||
border: primaryColor === preset.color ? '2px solid var(--color-primary)' : '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8125rem',
|
||||
color: 'var(--color-text)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: preset.color,
|
||||
border: '1px solid rgba(0,0,0,0.15)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Eigene Farbe + Vorschau */}
|
||||
<div>
|
||||
<label style={labelStyle}>Eigene Farbe</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<input
|
||||
type="color"
|
||||
value={primaryColor}
|
||||
onChange={(e) => { setPrimaryColor(e.target.value); setHasChanges(true); }}
|
||||
style={{ width: 40, height: 40, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={primaryColor}
|
||||
onChange={(e) => { setPrimaryColor(e.target.value); setHasChanges(true); }}
|
||||
style={{ padding: '0.5rem 0.75rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', width: 120, fontFamily: 'monospace' }}
|
||||
/>
|
||||
{/* Vorschau-Button */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<button
|
||||
style={{
|
||||
padding: '0.5rem 1.25rem',
|
||||
background: primaryColor,
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
Vorschau
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
padding: '0.5rem 1.25rem',
|
||||
background: 'none',
|
||||
color: primaryColor,
|
||||
border: `1px solid ${primaryColor}`,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
Outline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Speichern */}
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ export function AppLayout() {
|
|||
sidebarColor: string | null;
|
||||
logoWidth: number | null;
|
||||
sidebarWidth: number | null;
|
||||
primaryColor: string | null;
|
||||
}>({
|
||||
queryKey: ['settings', 'branding'],
|
||||
queryFn: async () => {
|
||||
|
|
@ -164,12 +165,35 @@ export function AppLayout() {
|
|||
sidebarColor: string | null;
|
||||
logoWidth: number | null;
|
||||
sidebarWidth: number | null;
|
||||
primaryColor: string | null;
|
||||
}>('/settings/branding');
|
||||
return res.data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Primärfarbe als CSS-Variable setzen
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const hex = branding?.primaryColor;
|
||||
if (hex && /^#[0-9a-fA-F]{6}$/.test(hex)) {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
const rh = Math.round(r * 0.82);
|
||||
const gh = Math.round(g * 0.82);
|
||||
const bh = Math.round(b * 0.82);
|
||||
const hoverHex = `#${rh.toString(16).padStart(2, '0')}${gh.toString(16).padStart(2, '0')}${bh.toString(16).padStart(2, '0')}`;
|
||||
root.style.setProperty('--color-primary', hex);
|
||||
root.style.setProperty('--color-primary-hover', hoverHex);
|
||||
root.style.setProperty('--color-primary-light', `rgba(${r}, ${g}, ${b}, 0.12)`);
|
||||
} else {
|
||||
root.style.removeProperty('--color-primary');
|
||||
root.style.removeProperty('--color-primary-hover');
|
||||
root.style.removeProperty('--color-primary-light');
|
||||
}
|
||||
}, [branding?.primaryColor]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
navigate('/login');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue