158 lines
6.3 KiB
JavaScript
158 lines
6.3 KiB
JavaScript
import { useState, useEffect } from 'react';
|
||
import { api } from '../utils/api.js';
|
||
import { useToast } from '../contexts/ToastContext.jsx';
|
||
|
||
export default function BrandingModal({ onClose }) {
|
||
const toast = useToast();
|
||
const [settings, setSettings] = useState({});
|
||
const [appName, setAppName] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
const [resetting, setResetting] = useState(false);
|
||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||
|
||
useEffect(() => {
|
||
api.getSettings().then(({ settings }) => {
|
||
setSettings(settings);
|
||
setAppName(settings.app_name || 'jama');
|
||
}).catch(() => {});
|
||
}, []);
|
||
|
||
const notifySidebarRefresh = () => window.dispatchEvent(new Event('jama:settings-changed'));
|
||
|
||
const handleSaveName = async () => {
|
||
if (!appName.trim()) return;
|
||
setLoading(true);
|
||
try {
|
||
await api.updateAppName(appName.trim());
|
||
setSettings(prev => ({ ...prev, app_name: appName.trim() }));
|
||
toast('App name updated', 'success');
|
||
notifySidebarRefresh();
|
||
} catch (e) {
|
||
toast(e.message, 'error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleLogoUpload = async (e) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
if (file.size > 1024 * 1024) return toast('Logo must be less than 1MB', 'error');
|
||
try {
|
||
const { logoUrl } = await api.uploadLogo(file);
|
||
setSettings(prev => ({ ...prev, logo_url: logoUrl }));
|
||
toast('Logo updated', 'success');
|
||
notifySidebarRefresh();
|
||
} catch (e) {
|
||
toast(e.message, 'error');
|
||
}
|
||
};
|
||
|
||
const handleReset = async () => {
|
||
setResetting(true);
|
||
try {
|
||
await api.resetSettings();
|
||
const { settings: fresh } = await api.getSettings();
|
||
setSettings(fresh);
|
||
setAppName(fresh.app_name || 'jama');
|
||
toast('Settings reset to defaults', 'success');
|
||
notifySidebarRefresh();
|
||
setShowResetConfirm(false);
|
||
} catch (e) {
|
||
toast(e.message, 'error');
|
||
} finally {
|
||
setResetting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
|
||
<div className="modal" style={{ maxWidth: 460 }}>
|
||
<div className="flex items-center justify-between" style={{ marginBottom: 20 }}>
|
||
<h2 className="modal-title" style={{ margin: 0 }}>Branding</h2>
|
||
<button className="btn-icon" onClick={onClose}>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* App Logo */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<div className="settings-section-label">App Logo</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||
<div style={{
|
||
width: 72, height: 72, borderRadius: 16, background: 'var(--background)',
|
||
border: '1px solid var(--border)', overflow: 'hidden', display: 'flex',
|
||
alignItems: 'center', justifyContent: 'center', flexShrink: 0
|
||
}}>
|
||
<img
|
||
src={settings.logo_url || '/icons/jama.png'}
|
||
alt="logo"
|
||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="btn btn-secondary btn-sm" style={{ cursor: 'pointer', display: 'inline-block' }}>
|
||
Upload Logo
|
||
<input type="file" accept="image/*" style={{ display: 'none' }} onChange={handleLogoUpload} />
|
||
</label>
|
||
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 6 }}>
|
||
Square format, max 1MB. Used in sidebar, login page and browser tab.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* App Name */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<div className="settings-section-label">App Name</div>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<input className="input flex-1" value={appName} onChange={e => setAppName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSaveName()} />
|
||
<button className="btn btn-primary btn-sm" onClick={handleSaveName} disabled={loading}>
|
||
{loading ? '...' : 'Save'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Reset + Version */}
|
||
<div style={{ marginBottom: settings.pw_reset_active === 'true' ? 16 : 0 }}>
|
||
<div className="settings-section-label">Reset</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||
{!showResetConfirm ? (
|
||
<button className="btn btn-secondary btn-sm" onClick={() => setShowResetConfirm(true)}>
|
||
Reset All to Defaults
|
||
</button>
|
||
) : (
|
||
<div style={{
|
||
background: '#fce8e6', border: '1px solid #f5c6c2',
|
||
borderRadius: 'var(--radius)', padding: '12px 14px'
|
||
}}>
|
||
<p style={{ fontSize: 13, color: 'var(--error)', marginBottom: 12 }}>
|
||
This will reset the app name and logo to their install defaults. This cannot be undone.
|
||
</p>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<button className="btn btn-sm" style={{ background: 'var(--error)', color: 'white' }} onClick={handleReset} disabled={resetting}>
|
||
{resetting ? 'Resetting...' : 'Yes, Reset Everything'}
|
||
</button>
|
||
<button className="btn btn-secondary btn-sm" onClick={() => setShowResetConfirm(false)}>Cancel</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{settings.app_version && (
|
||
<span style={{ fontSize: 11, color: 'var(--text-tertiary)', whiteSpace: 'nowrap' }}>
|
||
v{settings.app_version}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{settings.pw_reset_active === 'true' && (
|
||
<div className="warning-banner">
|
||
<span>⚠️</span>
|
||
<span><strong>ADMPW_RESET is active.</strong> The default admin password is being reset on every restart. Set ADMPW_RESET=false in your environment variables to stop this.</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|