Redesign UI and add configurable branding logo
- Modern dark/light theme with CSS variables, pills, cards and a sticky nav - New Branding section in admin settings: upload logo (png/jpg/svg/webp/gif, 2 MB) plus width/height sliders, proportional scale and reset - Logo served publicly via /logo and displayed on admin portal and the customer upload page; files persist under ./data/logo Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+66
-1
@@ -12,7 +12,9 @@ const janitor = require('./janitor');
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||
const UPLOAD_ROOT = process.env.UPLOAD_ROOT || '/data/uploads';
|
||||
const LOGO_DIR = process.env.LOGO_DIR || '/data/logo';
|
||||
|
||||
fs.mkdirSync(LOGO_DIR, { recursive: true });
|
||||
settings.seedFromEnv();
|
||||
|
||||
fs.mkdirSync(UPLOAD_ROOT, { recursive: true });
|
||||
@@ -83,6 +85,14 @@ function canAccessCustomer(user, customer, needWrite = false) {
|
||||
// ---------- Setup & Auth API ----------
|
||||
const publicApi = express.Router();
|
||||
|
||||
publicApi.get('/branding', (req, res) => {
|
||||
res.json({
|
||||
logo_filename: settings.get('logo_filename', ''),
|
||||
logo_width_px: parseInt(settings.get('logo_width_px', '0'), 10),
|
||||
logo_height_px: parseInt(settings.get('logo_height_px', '0'), 10),
|
||||
});
|
||||
});
|
||||
|
||||
publicApi.get('/status', (req, res) => {
|
||||
const u = auth.getSessionUser(req);
|
||||
res.json({
|
||||
@@ -189,11 +199,14 @@ api.get('/settings', auth.requireAdmin, (req, res) => {
|
||||
res.json({
|
||||
public_base_url: settings.get('public_base_url', ''),
|
||||
janitor_interval_minutes: parseInt(settings.get('janitor_interval_minutes', '30'), 10),
|
||||
logo_filename: settings.get('logo_filename', ''),
|
||||
logo_width_px: parseInt(settings.get('logo_width_px', '0'), 10),
|
||||
logo_height_px: parseInt(settings.get('logo_height_px', '0'), 10),
|
||||
});
|
||||
});
|
||||
|
||||
api.put('/settings', auth.requireAdmin, (req, res) => {
|
||||
const { public_base_url, janitor_interval_minutes } = req.body || {};
|
||||
const { public_base_url, janitor_interval_minutes, logo_width_px, logo_height_px } = req.body || {};
|
||||
if (public_base_url !== undefined) {
|
||||
const v = String(public_base_url || '').trim().replace(/\/+$/, '');
|
||||
settings.set('public_base_url', v);
|
||||
@@ -203,6 +216,48 @@ api.put('/settings', auth.requireAdmin, (req, res) => {
|
||||
settings.set('janitor_interval_minutes', String(n));
|
||||
janitor.restart(n * 60 * 1000);
|
||||
}
|
||||
if (logo_width_px !== undefined) {
|
||||
const n = Math.max(0, Math.min(800, parseInt(logo_width_px, 10) || 0));
|
||||
settings.set('logo_width_px', String(n));
|
||||
}
|
||||
if (logo_height_px !== undefined) {
|
||||
const n = Math.max(0, Math.min(600, parseInt(logo_height_px, 10) || 0));
|
||||
settings.set('logo_height_px', String(n));
|
||||
}
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// --- Logo (admin manages, public serves) ---
|
||||
const logoUpload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 2 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
const ok = /^image\/(png|jpeg|gif|svg\+xml|webp)$/.test(file.mimetype);
|
||||
cb(ok ? null : new Error('invalid image type'), ok);
|
||||
},
|
||||
});
|
||||
|
||||
api.post('/logo', auth.requireAdmin, logoUpload.single('logo'), (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'no file' });
|
||||
const ext = {
|
||||
'image/png': '.png',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/gif': '.gif',
|
||||
'image/svg+xml': '.svg',
|
||||
'image/webp': '.webp',
|
||||
}[req.file.mimetype] || '';
|
||||
const oldFn = settings.get('logo_filename', '');
|
||||
if (oldFn) { try { fs.unlinkSync(path.join(LOGO_DIR, oldFn)); } catch {} }
|
||||
const fn = `logo${ext}`;
|
||||
fs.writeFileSync(path.join(LOGO_DIR, fn), req.file.buffer);
|
||||
settings.set('logo_filename', fn);
|
||||
res.json({ ok: true, filename: fn });
|
||||
});
|
||||
|
||||
api.delete('/logo', auth.requireAdmin, (req, res) => {
|
||||
const fn = settings.get('logo_filename', '');
|
||||
if (fn) { try { fs.unlinkSync(path.join(LOGO_DIR, fn)); } catch {} }
|
||||
settings.set('logo_filename', '');
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
@@ -420,6 +475,16 @@ app.post('/u/:token/upload', uploadAuth, upload.single('file'), (req, res) => {
|
||||
res.json({ ok: true, file: { name: f.filename, path: rel, size: f.size } });
|
||||
});
|
||||
|
||||
// Public logo (no auth) — used by admin & upload page
|
||||
app.get('/logo', (req, res) => {
|
||||
const fn = settings.get('logo_filename', '');
|
||||
if (!fn) return res.status(404).end();
|
||||
const fp = path.join(LOGO_DIR, fn);
|
||||
if (!fs.existsSync(fp)) return res.status(404).end();
|
||||
res.setHeader('Cache-Control', 'public, max-age=60');
|
||||
res.sendFile(fp);
|
||||
});
|
||||
|
||||
// ---------- Root ----------
|
||||
app.get('/', (req, res) => res.redirect('/admin/'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user