Files
fotospiel-app/resources/js/guest-v2/components/EventLogo.tsx
Codex Agent 298a8375b6
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Update guest v2 branding and theming
2026-02-03 15:18:44 +01:00

168 lines
4.9 KiB
TypeScript

import React from 'react';
import { YStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Camera, Heart, PartyPopper, Users } from 'lucide-react';
import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '@/guest/context/EventBrandingContext';
import { getContrastingTextColor } from '@/guest/lib/color';
import type { EventBranding } from '@/guest/types/event-branding';
type LogoSize = 's' | 'm' | 'l';
type EventLogoProps = {
name: string;
icon?: string | null;
logo?: EventBranding['logo'] | null;
size?: LogoSize;
};
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ size?: number; color?: string }>> = {
heart: Heart,
guests: Users,
party: PartyPopper,
camera: Camera,
};
const LOGO_SIZE_MAP: Record<LogoSize, { container: number; image: number; emoji: number; icon: number; text: number }> = {
s: { container: 32, image: 24, emoji: 16, icon: 14, text: 12 },
m: { container: 40, image: 30, emoji: 18, icon: 18, text: 14 },
l: { container: 48, image: 38, emoji: 22, icon: 22, text: 16 },
};
function isLikelyEmoji(value: string): boolean {
if (!value) {
return false;
}
const characters = Array.from(value.trim());
if (characters.length === 0 || characters.length > 2) {
return false;
}
return characters.some((char) => {
const codePoint = char.codePointAt(0) ?? 0;
return codePoint > 0x2600;
});
}
function getInitials(name: string): string {
const words = name.split(' ').filter(Boolean);
if (words.length >= 2) {
return `${words[0][0]}${words[1][0]}`.toUpperCase();
}
return name.substring(0, 2).toUpperCase();
}
export default function EventLogo({ name, icon, logo, size }: EventLogoProps) {
const brandingContext = useOptionalEventBranding();
const branding = brandingContext?.branding ?? DEFAULT_EVENT_BRANDING;
const resolvedLogo = logo ?? branding.logo;
const logoMode = resolvedLogo?.mode ?? (branding.logoUrl ? 'upload' : 'emoticon');
const logoValue = resolvedLogo?.value ?? branding.logoUrl ?? null;
const logoSize = size ?? resolvedLogo?.size ?? 'm';
const sizes = LOGO_SIZE_MAP[logoSize];
const accentColor = branding.secondaryColor || DEFAULT_EVENT_BRANDING.secondaryColor;
const textColor = getContrastingTextColor(accentColor, '#ffffff', '#0f172a');
const [logoFailed, setLogoFailed] = React.useState(false);
React.useEffect(() => {
setLogoFailed(false);
}, [logoValue]);
if (logoMode === 'upload' && logoValue && !logoFailed) {
return (
<YStack
width={sizes.container}
height={sizes.container}
borderRadius={sizes.container}
alignItems="center"
justifyContent="center"
backgroundColor="#ffffff"
borderWidth={1}
borderColor="rgba(15, 23, 42, 0.08)"
overflow="hidden"
>
<img
src={logoValue}
alt={name}
style={{
width: sizes.image,
height: sizes.image,
borderRadius: sizes.image,
objectFit: 'cover',
}}
onError={() => setLogoFailed(true)}
/>
</YStack>
);
}
if (logoMode === 'emoticon' && logoValue && isLikelyEmoji(logoValue)) {
return (
<YStack
width={sizes.container}
height={sizes.container}
borderRadius={sizes.container}
alignItems="center"
justifyContent="center"
backgroundColor={accentColor}
>
<Text fontSize={sizes.emoji} color={textColor}>
{logoValue}
</Text>
</YStack>
);
}
if (typeof icon === 'string') {
const trimmed = icon.trim();
if (trimmed) {
const normalized = trimmed.toLowerCase();
const IconComponent = EVENT_ICON_COMPONENTS[normalized];
if (IconComponent) {
return (
<YStack
width={sizes.container}
height={sizes.container}
borderRadius={sizes.container}
alignItems="center"
justifyContent="center"
backgroundColor={accentColor}
>
<IconComponent size={sizes.icon} color={textColor} />
</YStack>
);
}
if (isLikelyEmoji(trimmed)) {
return (
<YStack
width={sizes.container}
height={sizes.container}
borderRadius={sizes.container}
alignItems="center"
justifyContent="center"
backgroundColor={accentColor}
>
<Text fontSize={sizes.emoji} color={textColor}>
{trimmed}
</Text>
</YStack>
);
}
}
}
return (
<YStack
width={sizes.container}
height={sizes.container}
borderRadius={sizes.container}
alignItems="center"
justifyContent="center"
backgroundColor={accentColor}
>
<Text fontSize={sizes.text} color={textColor} fontWeight="$7">
{getInitials(name)}
</Text>
</YStack>
);
}