feat: draft mode, admin branding, and workflow improvements

Draft Mode:
- Events are created as drafts by default — no email sent until published
- Add "Publish & Notify Client" button with confirmation dialog
- Draft banner with yellow styling on event details page
- Draft filter tab in events list
- Gallery middleware blocks public access to draft events
- Migration 076 adds is_draft column to events table

Admin Draft Preview:
- Admins can preview draft galleries via JWT preview token (?preview=)
- "View Gallery" link on drafts auto-appends preview token

Admin & Login Page Branding:
- Admin header uses configured company logo/name from branding settings
- Login page shows configured logo instead of hardcoded PicPeak
- Respects logo_display_mode (logo_only, text_only, logo_and_text)

OG Tag Branding:
- DynamicFavicon component updates OG meta tags and page title from
  branding settings

Editable Client Email:
- Customer email is now editable after event creation in edit mode

Branding Inheritance:
- New events inherit hero logo settings (visibility, size, position)
  from global branding configuration

Share Link Full Domain URL:
- New getFrontendBaseUrl() utility with DB fallback to general_site_url
- Used in email processor and share link service
This commit is contained in:
Paul Nothaft
2026-04-08 11:42:38 +02:00
parent 125cd0d003
commit 40332a71db
19 changed files with 456 additions and 62 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ const nodemailer = require('nodemailer');
const Handlebars = require('handlebars');
const { db } = require('../database/db');
const logger = require('../utils/logger');
const { getFrontendBaseUrl } = require('../utils/frontendUrl');
let transporter = null;
let lastConfigHash = null;
@@ -161,7 +162,7 @@ async function wrapEmailHtml(htmlBody, subject, language = 'en') {
const hoverColor = darkenColor(primaryColor, 0.15);
// Build full logo URL - ensure logoUrl is a valid non-empty string
const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/+$/, '');
const frontendUrl = (await getFrontendBaseUrl()) || 'http://localhost:3000';
const logoPath = (typeof logoUrl === 'string' && logoUrl.trim()) ? logoUrl : '/picpeak-logo-transparent.png';
const logoFullUrl = `${frontendUrl}${logoPath.startsWith('/') ? '' : '/'}${logoPath}`;
logger.debug('Email logo URL:', { frontendUrl, logoPath, logoFullUrl });
+4 -2
View File
@@ -1,6 +1,7 @@
const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { extractShareToken, isPotentialShareToken, buildSharePath } = require('../utils/shareLinkUtils');
const { getFrontendBaseUrl } = require('../utils/frontendUrl');
const SETTING_KEY = 'general_short_gallery_urls';
const CACHE_TTL_MS = 60_000;
@@ -87,7 +88,7 @@ const buildShareLinkVariants = async ({ slug, shareToken }) => {
const shortEnabled = await isShortGalleryUrlsEnabled();
const sharePath = buildSharePath(slug, shareToken, shortEnabled);
const frontendBase = (process.env.FRONTEND_URL || '').replace(/\/$/, '');
const frontendBase = await getFrontendBaseUrl();
const shareUrl = frontendBase ? `${frontendBase}${sharePath}` : sharePath;
return {
@@ -112,7 +113,8 @@ const getEventShareToken = (event) => {
const ACTIVE_EVENT_FILTER = {
is_active: formatBoolean(true),
is_archived: formatBoolean(false)
is_archived: formatBoolean(false),
is_draft: formatBoolean(false)
};
const resolveShareIdentifier = async (identifier) => {