Fix guest demo UX and enforce guest limits
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import type { EventBranding } from '../types/event-branding';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { getContrastingTextColor, relativeLuminance } from '../lib/color';
|
||||
|
||||
type EventBrandingContextValue = {
|
||||
branding: EventBranding;
|
||||
@@ -7,16 +9,16 @@ type EventBrandingContextValue = {
|
||||
};
|
||||
|
||||
export const DEFAULT_EVENT_BRANDING: EventBranding = {
|
||||
primaryColor: '#FF5A5F',
|
||||
secondaryColor: '#FFF8F5',
|
||||
backgroundColor: '#FFF8F5',
|
||||
primaryColor: '#E94B5A',
|
||||
secondaryColor: '#F7C7CF',
|
||||
backgroundColor: '#FFF6F2',
|
||||
fontFamily: 'Montserrat, Inter, "Helvetica Neue", system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
||||
logoUrl: null,
|
||||
palette: {
|
||||
primary: '#FF5A5F',
|
||||
secondary: '#FFF8F5',
|
||||
background: '#FFF8F5',
|
||||
surface: '#FFF8F5',
|
||||
primary: '#E94B5A',
|
||||
secondary: '#F7C7CF',
|
||||
background: '#FFF6F2',
|
||||
surface: '#FFFFFF',
|
||||
},
|
||||
typography: {
|
||||
heading: 'Playfair Display, "Times New Roman", serif',
|
||||
@@ -114,15 +116,46 @@ function applyCssVariables(branding: EventBranding) {
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
const background = branding.backgroundColor;
|
||||
const surfaceCandidate = branding.palette?.surface ?? background;
|
||||
const backgroundLuminance = relativeLuminance(background);
|
||||
const surfaceLuminance = relativeLuminance(surfaceCandidate);
|
||||
const surface = Math.abs(surfaceLuminance - backgroundLuminance) < 0.06
|
||||
? backgroundLuminance >= 0.6
|
||||
? '#ffffff'
|
||||
: '#0f172a'
|
||||
: surfaceCandidate;
|
||||
const isLight = backgroundLuminance >= 0.6;
|
||||
const foreground = isLight ? '#1f2937' : '#f8fafc';
|
||||
const mutedForeground = isLight ? '#6b7280' : '#cbd5e1';
|
||||
const muted = isLight ? '#f6efec' : '#1f2937';
|
||||
const border = isLight ? '#e6d9d6' : '#334155';
|
||||
const input = isLight ? '#eadfda' : '#273247';
|
||||
const primaryForeground = getContrastingTextColor(branding.primaryColor, '#ffffff', '#0f172a');
|
||||
const secondaryForeground = getContrastingTextColor(branding.secondaryColor, '#ffffff', '#0f172a');
|
||||
root.style.setProperty('--guest-primary', branding.primaryColor);
|
||||
root.style.setProperty('--guest-secondary', branding.secondaryColor);
|
||||
root.style.setProperty('--guest-background', branding.backgroundColor);
|
||||
root.style.setProperty('--guest-surface', branding.palette?.surface ?? branding.backgroundColor);
|
||||
root.style.setProperty('--guest-background', background);
|
||||
root.style.setProperty('--guest-surface', surface);
|
||||
root.style.setProperty('--guest-button-radius', `${branding.buttons?.radius ?? 12}px`);
|
||||
root.style.setProperty('--guest-radius', `${branding.buttons?.radius ?? 12}px`);
|
||||
root.style.setProperty('--guest-link', branding.buttons?.linkColor ?? branding.secondaryColor);
|
||||
root.style.setProperty('--guest-button-style', branding.buttons?.style ?? 'filled');
|
||||
root.style.setProperty('--guest-font-scale', String(FONT_SCALE_MAP[branding.typography?.sizePreset ?? 'm'] ?? 1));
|
||||
root.style.setProperty('--foreground', foreground);
|
||||
root.style.setProperty('--card-foreground', foreground);
|
||||
root.style.setProperty('--popover-foreground', foreground);
|
||||
root.style.setProperty('--muted', muted);
|
||||
root.style.setProperty('--muted-foreground', mutedForeground);
|
||||
root.style.setProperty('--border', border);
|
||||
root.style.setProperty('--input', input);
|
||||
root.style.setProperty('--primary', branding.primaryColor);
|
||||
root.style.setProperty('--primary-foreground', primaryForeground);
|
||||
root.style.setProperty('--secondary', branding.secondaryColor);
|
||||
root.style.setProperty('--secondary-foreground', secondaryForeground);
|
||||
root.style.setProperty('--accent', branding.secondaryColor);
|
||||
root.style.setProperty('--accent-foreground', secondaryForeground);
|
||||
root.style.setProperty('--ring', branding.primaryColor);
|
||||
|
||||
const headingFont = branding.typography?.heading ?? branding.fontFamily;
|
||||
const bodyFont = branding.typography?.body ?? branding.fontFamily;
|
||||
@@ -160,9 +193,27 @@ function resetCssVariables() {
|
||||
root.style.removeProperty('--guest-font-family');
|
||||
root.style.removeProperty('--guest-body-font');
|
||||
root.style.removeProperty('--guest-heading-font');
|
||||
root.style.removeProperty('--foreground');
|
||||
root.style.removeProperty('--card-foreground');
|
||||
root.style.removeProperty('--popover-foreground');
|
||||
root.style.removeProperty('--muted');
|
||||
root.style.removeProperty('--muted-foreground');
|
||||
root.style.removeProperty('--border');
|
||||
root.style.removeProperty('--input');
|
||||
root.style.removeProperty('--primary');
|
||||
root.style.removeProperty('--primary-foreground');
|
||||
root.style.removeProperty('--secondary');
|
||||
root.style.removeProperty('--secondary-foreground');
|
||||
root.style.removeProperty('--accent');
|
||||
root.style.removeProperty('--accent-foreground');
|
||||
root.style.removeProperty('--ring');
|
||||
}
|
||||
|
||||
function applyThemeMode(mode: EventBranding['mode']) {
|
||||
function applyThemeMode(
|
||||
mode: EventBranding['mode'],
|
||||
backgroundColor: string,
|
||||
appearanceOverride: 'light' | 'dark' | null,
|
||||
) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
@@ -171,6 +222,12 @@ function applyThemeMode(mode: EventBranding['mode']) {
|
||||
const prefersDark = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: false;
|
||||
const backgroundLuminance = relativeLuminance(backgroundColor || DEFAULT_EVENT_BRANDING.backgroundColor);
|
||||
const backgroundPrefers = backgroundLuminance >= 0.65
|
||||
? 'light'
|
||||
: backgroundLuminance <= 0.35
|
||||
? 'dark'
|
||||
: null;
|
||||
const applyDark = () => root.classList.add('dark');
|
||||
const applyLight = () => root.classList.remove('dark');
|
||||
|
||||
@@ -186,6 +243,28 @@ function applyThemeMode(mode: EventBranding['mode']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appearanceOverride) {
|
||||
if (appearanceOverride === 'dark') {
|
||||
applyDark();
|
||||
root.style.colorScheme = 'dark';
|
||||
} else {
|
||||
applyLight();
|
||||
root.style.colorScheme = 'light';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (backgroundPrefers) {
|
||||
if (backgroundPrefers === 'dark') {
|
||||
applyDark();
|
||||
root.style.colorScheme = 'dark';
|
||||
} else {
|
||||
applyLight();
|
||||
root.style.colorScheme = 'light';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefersDark) {
|
||||
applyDark();
|
||||
root.style.colorScheme = 'dark';
|
||||
@@ -204,6 +283,8 @@ export function EventBrandingProvider({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const resolved = useMemo(() => resolveBranding(branding), [branding]);
|
||||
const { appearance } = useAppearance();
|
||||
const appearanceOverride = appearance === 'light' || appearance === 'dark' ? appearance : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -211,7 +292,7 @@ export function EventBrandingProvider({
|
||||
}
|
||||
applyCssVariables(resolved);
|
||||
const previousDark = typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : false;
|
||||
applyThemeMode(resolved.mode ?? 'auto');
|
||||
applyThemeMode(resolved.mode ?? 'auto', resolved.backgroundColor, appearanceOverride);
|
||||
|
||||
return () => {
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -224,9 +305,9 @@ export function EventBrandingProvider({
|
||||
}
|
||||
resetCssVariables();
|
||||
applyCssVariables(DEFAULT_EVENT_BRANDING);
|
||||
applyThemeMode(DEFAULT_EVENT_BRANDING.mode ?? 'auto');
|
||||
applyThemeMode(DEFAULT_EVENT_BRANDING.mode ?? 'auto', DEFAULT_EVENT_BRANDING.backgroundColor, appearanceOverride);
|
||||
};
|
||||
}, [resolved]);
|
||||
}, [appearanceOverride, resolved]);
|
||||
|
||||
const value = useMemo<EventBrandingContextValue>(() => ({
|
||||
branding: resolved,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { EventBrandingProvider } from '../EventBrandingContext';
|
||||
import { AppearanceProvider } from '@/hooks/use-appearance';
|
||||
import type { EventBranding } from '../../types/event-branding';
|
||||
|
||||
const sampleBranding: EventBranding = {
|
||||
@@ -23,6 +24,7 @@ describe('EventBrandingProvider', () => {
|
||||
document.documentElement.style.removeProperty('color-scheme');
|
||||
document.documentElement.style.removeProperty('--guest-background');
|
||||
document.documentElement.style.removeProperty('--guest-font-scale');
|
||||
localStorage.removeItem('theme');
|
||||
});
|
||||
|
||||
it('applies guest theme classes and variables', async () => {
|
||||
@@ -44,4 +46,27 @@ describe('EventBrandingProvider', () => {
|
||||
|
||||
expect(document.documentElement.classList.contains('guest-theme')).toBe(false);
|
||||
});
|
||||
|
||||
it('respects appearance override in auto mode', async () => {
|
||||
localStorage.setItem('theme', 'dark');
|
||||
const autoBranding: EventBranding = {
|
||||
...sampleBranding,
|
||||
mode: 'auto',
|
||||
backgroundColor: '#fff7ed',
|
||||
};
|
||||
|
||||
const { unmount } = render(
|
||||
<AppearanceProvider>
|
||||
<EventBrandingProvider branding={autoBranding}>
|
||||
<div>Guest</div>
|
||||
</EventBrandingProvider>
|
||||
</AppearanceProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user