Fix guest demo UX and enforce guest limits
This commit is contained in:
@@ -17,25 +17,26 @@ import { MobileSheet } from './components/Sheet';
|
||||
import toast from 'react-hot-toast';
|
||||
import { adminPath } from '../constants';
|
||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||
import { ADMIN_COLORS, ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||
import { extractBrandingForm, type BrandingFormValues } from '../lib/brandingForm';
|
||||
import { getContrastingTextColor } from '@/guest/lib/color';
|
||||
import { DEFAULT_EVENT_BRANDING } from '@/guest/context/EventBrandingContext';
|
||||
import { getContrastingTextColor, relativeLuminance } from '@/guest/lib/color';
|
||||
|
||||
const BRANDING_FORM_DEFAULTS = {
|
||||
primary: ADMIN_COLORS.primary,
|
||||
accent: ADMIN_COLORS.accent,
|
||||
background: '#ffffff',
|
||||
surface: '#ffffff',
|
||||
mode: 'auto' as const,
|
||||
buttonStyle: 'filled' as const,
|
||||
buttonRadius: 12,
|
||||
buttonPrimary: ADMIN_COLORS.primary,
|
||||
buttonSecondary: ADMIN_COLORS.accent,
|
||||
linkColor: ADMIN_COLORS.accent,
|
||||
fontSize: 'm' as const,
|
||||
logoMode: 'upload' as const,
|
||||
logoPosition: 'left' as const,
|
||||
logoSize: 'm' as const,
|
||||
primary: DEFAULT_EVENT_BRANDING.primaryColor,
|
||||
accent: DEFAULT_EVENT_BRANDING.secondaryColor,
|
||||
background: DEFAULT_EVENT_BRANDING.backgroundColor,
|
||||
surface: DEFAULT_EVENT_BRANDING.palette?.surface ?? DEFAULT_EVENT_BRANDING.backgroundColor,
|
||||
mode: DEFAULT_EVENT_BRANDING.mode ?? 'auto',
|
||||
buttonStyle: DEFAULT_EVENT_BRANDING.buttons?.style ?? 'filled',
|
||||
buttonRadius: DEFAULT_EVENT_BRANDING.buttons?.radius ?? 12,
|
||||
buttonPrimary: DEFAULT_EVENT_BRANDING.buttons?.primary ?? DEFAULT_EVENT_BRANDING.primaryColor,
|
||||
buttonSecondary: DEFAULT_EVENT_BRANDING.buttons?.secondary ?? DEFAULT_EVENT_BRANDING.secondaryColor,
|
||||
linkColor: DEFAULT_EVENT_BRANDING.buttons?.linkColor ?? DEFAULT_EVENT_BRANDING.secondaryColor,
|
||||
fontSize: DEFAULT_EVENT_BRANDING.typography?.sizePreset ?? 'm',
|
||||
logoMode: DEFAULT_EVENT_BRANDING.logo?.mode ?? 'emoticon',
|
||||
logoPosition: DEFAULT_EVENT_BRANDING.logo?.position ?? 'left',
|
||||
logoSize: DEFAULT_EVENT_BRANDING.logo?.size ?? 'm',
|
||||
};
|
||||
|
||||
const BRANDING_FORM_BASE: BrandingFormValues = {
|
||||
@@ -176,10 +177,22 @@ export default function MobileBrandingPage() {
|
||||
}, [tenantBrandingLoaded]);
|
||||
|
||||
const previewForm = form.useDefaultBranding && tenantBranding ? tenantBranding : form;
|
||||
const previewBackground = previewForm.background;
|
||||
const previewSurfaceCandidate = previewForm.surface || previewBackground;
|
||||
const backgroundLuminance = relativeLuminance(previewBackground);
|
||||
const surfaceLuminance = relativeLuminance(previewSurfaceCandidate);
|
||||
const previewSurface = Math.abs(surfaceLuminance - backgroundLuminance) < 0.06
|
||||
? backgroundLuminance >= 0.6
|
||||
? '#ffffff'
|
||||
: '#0f172a'
|
||||
: previewSurfaceCandidate;
|
||||
const previewForeground = backgroundLuminance >= 0.6 ? '#1f2937' : '#f8fafc';
|
||||
const previewMutedForeground = backgroundLuminance >= 0.6 ? '#6b7280' : '#cbd5e1';
|
||||
const previewBorder = backgroundLuminance >= 0.6 ? '#e6d9d6' : '#334155';
|
||||
const previewTitle = event ? renderName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event');
|
||||
const previewHeadingFont = previewForm.headingFont || 'Archivo Black';
|
||||
const previewBodyFont = previewForm.bodyFont || 'Manrope';
|
||||
const previewSurfaceText = getContrastingTextColor(previewForm.surface, '#ffffff', '#0f172a');
|
||||
const previewHeadingFont = previewForm.headingFont || DEFAULT_EVENT_BRANDING.typography?.heading || DEFAULT_EVENT_BRANDING.fontFamily;
|
||||
const previewBodyFont = previewForm.bodyFont || DEFAULT_EVENT_BRANDING.typography?.body || DEFAULT_EVENT_BRANDING.fontFamily;
|
||||
const previewSurfaceText = previewForeground;
|
||||
const previewScale = FONT_SIZE_SCALE[previewForm.fontSize] ?? 1;
|
||||
const previewButtonColor = previewForm.buttonPrimary || previewForm.primary;
|
||||
const previewButtonText = getContrastingTextColor(previewButtonColor, '#ffffff', '#0f172a');
|
||||
@@ -560,8 +573,8 @@ export default function MobileBrandingPage() {
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.branding.previewTitle', 'Guest App Preview')}
|
||||
</Text>
|
||||
<YStack borderRadius={16} borderWidth={1} borderColor={border} backgroundColor={previewForm.background} padding="$3" space="$2" alignItems="center">
|
||||
<YStack width="100%" borderRadius={12} backgroundColor={previewForm.surface} borderWidth={1} borderColor={border} overflow="hidden">
|
||||
<YStack borderRadius={16} borderWidth={1} borderColor={previewBorder} backgroundColor={previewBackground} padding="$3" space="$2" alignItems="center">
|
||||
<YStack width="100%" borderRadius={12} backgroundColor={previewSurface} borderWidth={1} borderColor={previewBorder} overflow="hidden">
|
||||
<YStack
|
||||
height={64}
|
||||
style={{ background: `linear-gradient(135deg, ${previewForm.primary}, ${previewForm.accent})` }}
|
||||
@@ -602,18 +615,18 @@ export default function MobileBrandingPage() {
|
||||
{previewTitle}
|
||||
</Text>
|
||||
<Text
|
||||
color={previewSurfaceText}
|
||||
style={{ fontFamily: previewBodyFont, opacity: 0.7, fontSize: 13 * previewScale }}
|
||||
color={previewMutedForeground}
|
||||
style={{ fontFamily: previewBodyFont, fontSize: 13 * previewScale }}
|
||||
>
|
||||
{t('events.branding.previewSubtitle', 'Aktuelle Farben & Schriften')}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
<XStack space="$2" marginTop="$1">
|
||||
<ColorSwatch color={previewForm.primary} label={t('events.branding.primary', 'Primary')} />
|
||||
<ColorSwatch color={previewForm.accent} label={t('events.branding.accent', 'Accent')} />
|
||||
<ColorSwatch color={previewForm.background} label={t('events.branding.background', 'Background')} />
|
||||
<ColorSwatch color={previewForm.surface} label={t('events.branding.surface', 'Surface')} />
|
||||
<ColorSwatch color={previewForm.primary} label={t('events.branding.primary', 'Primary')} borderColor={previewBorder} />
|
||||
<ColorSwatch color={previewForm.accent} label={t('events.branding.accent', 'Accent')} borderColor={previewBorder} />
|
||||
<ColorSwatch color={previewBackground} label={t('events.branding.background', 'Background')} borderColor={previewBorder} />
|
||||
<ColorSwatch color={previewSurface} label={t('events.branding.surface', 'Surface')} borderColor={previewBorder} />
|
||||
</XStack>
|
||||
<XStack marginTop="$2">
|
||||
<div
|
||||
@@ -1220,11 +1233,11 @@ function ColorField({
|
||||
);
|
||||
}
|
||||
|
||||
function ColorSwatch({ color, label }: { color: string; label: string }) {
|
||||
function ColorSwatch({ color, label, borderColor }: { color: string; label: string; borderColor?: string }) {
|
||||
const { border, muted } = useAdminTheme();
|
||||
return (
|
||||
<YStack alignItems="center" space="$1">
|
||||
<YStack width={40} height={40} borderRadius={12} borderWidth={1} borderColor={border} backgroundColor={color} />
|
||||
<YStack width={40} height={40} borderRadius={12} borderWidth={1} borderColor={borderColor ?? border} backgroundColor={color} />
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
@@ -34,6 +34,24 @@ describe('classifyPackageChange', () => {
|
||||
const candidate = { ...active, id: 4, max_photos: 200, gallery_days: 10, features: { advanced_analytics: false } } as any;
|
||||
expect(classifyPackageChange(candidate, active)).toEqual({ isUpgrade: false, isDowngrade: true });
|
||||
});
|
||||
|
||||
it('treats unlimited sharing as an upgrade over limited sharing', () => {
|
||||
const starter = { ...active, id: 5, features: ['limited_sharing'], max_photos: 100 } as any;
|
||||
const standard = { ...active, id: 6, features: ['unlimited_sharing'], max_photos: 100 } as any;
|
||||
expect(classifyPackageChange(standard, starter)).toEqual({ isUpgrade: true, isDowngrade: false });
|
||||
});
|
||||
|
||||
it('treats watermark removal as an upgrade over base watermark', () => {
|
||||
const base = { ...active, id: 7, watermark_allowed: false, features: [] } as any;
|
||||
const removal = { ...active, id: 8, watermark_allowed: true, features: ['no_watermark'] } as any;
|
||||
expect(classifyPackageChange(removal, base)).toEqual({ isUpgrade: true, isDowngrade: false });
|
||||
});
|
||||
|
||||
it('keeps watermark custom as higher than watermark removal', () => {
|
||||
const custom = { ...active, id: 9, watermark_allowed: true, features: [] } as any;
|
||||
const removal = { ...active, id: 10, watermark_allowed: true, features: ['no_watermark'] } as any;
|
||||
expect(classifyPackageChange(removal, custom)).toEqual({ isUpgrade: false, isDowngrade: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectRecommendedPackageId', () => {
|
||||
|
||||
@@ -63,6 +63,20 @@ function collectFeatures(pkg: Package | null): Set<string> {
|
||||
return new Set(getEnabledPackageFeatures(pkg));
|
||||
}
|
||||
|
||||
const FEATURE_COMPATIBILITY: Record<string, string[]> = {
|
||||
limited_sharing: ['limited_sharing', 'unlimited_sharing'],
|
||||
watermark_base: ['watermark_base', 'watermark_custom', 'no_watermark'],
|
||||
};
|
||||
|
||||
function featureSatisfied(feature: string, candidateFeatures: Set<string>): boolean {
|
||||
const compatible = FEATURE_COMPATIBILITY[feature];
|
||||
if (compatible) {
|
||||
return compatible.some((value) => candidateFeatures.has(value));
|
||||
}
|
||||
|
||||
return candidateFeatures.has(feature);
|
||||
}
|
||||
|
||||
function compareLimit(candidate: number | null, active: number | null): number {
|
||||
if (active === null) {
|
||||
return candidate === null ? 0 : -1;
|
||||
@@ -89,8 +103,8 @@ export function classifyPackageChange(pkg: Package, active: Package | null): Pac
|
||||
const activeFeatures = collectFeatures(active);
|
||||
const candidateFeatures = collectFeatures(pkg);
|
||||
|
||||
const hasFeatureUpgrade = Array.from(candidateFeatures).some((feature) => !activeFeatures.has(feature));
|
||||
const hasFeatureDowngrade = Array.from(activeFeatures).some((feature) => !candidateFeatures.has(feature));
|
||||
const hasFeatureUpgrade = Array.from(candidateFeatures).some((feature) => !featureSatisfied(feature, activeFeatures));
|
||||
const hasFeatureDowngrade = Array.from(activeFeatures).some((feature) => !featureSatisfied(feature, candidateFeatures));
|
||||
|
||||
const limitKeys: Array<keyof Package> = ['max_photos', 'max_guests', 'gallery_days'];
|
||||
let hasLimitUpgrade = false;
|
||||
|
||||
Reference in New Issue
Block a user