die tenant admin oauth authentifizierung wurde implementiert und funktioniert jetzt. Zudem wurde das marketing frontend dashboard implementiert.

This commit is contained in:
Codex Agent
2025-11-04 16:14:07 +01:00
parent 55c606bdd4
commit 92e64c361a
11 changed files with 407 additions and 156 deletions

View File

@@ -3,25 +3,36 @@ import { NavLink, useParams, useLocation } from 'react-router-dom';
import { CheckSquare, GalleryHorizontal, Home, Trophy } from 'lucide-react';
import { useEventData } from '../hooks/useEventData';
import { useTranslation } from '../i18n/useTranslation';
import { useEventBranding } from '../context/EventBrandingContext';
function TabLink({
to,
children,
isActive,
accentColor,
}: {
to: string;
children: React.ReactNode;
isActive: boolean;
accentColor: string;
}) {
const activeStyle = isActive
? {
background: `linear-gradient(135deg, ${accentColor}, ${accentColor}cc)`,
color: '#ffffff',
boxShadow: `0 12px 30px ${accentColor}33`,
}
: undefined;
return (
<NavLink
to={to}
className={`
flex flex-col items-center gap-1 h-14 p-2 transition-all duration-200 rounded-lg backdrop-blur-md
${isActive
? 'bg-gradient-to-t from-pink-500/90 to-pink-400/90 text-white shadow-lg scale-105 border border-white/30'
: 'text-gray-300 hover:bg-white/10 hover:text-pink-300 hover:scale-105 hover:border-white/20 border border-transparent'
}`}
flex h-14 flex-col items-center justify-center gap-1 rounded-lg border border-transparent p-2 text-xs font-medium transition-all duration-200 ease-out
touch-manipulation backdrop-blur-md
${isActive ? 'scale-[1.04]' : 'text-white/70 hover:text-white'}
`}
style={activeStyle}
>
{children}
</NavLink>
@@ -33,10 +44,12 @@ export default function BottomNav() {
const location = useLocation();
const { event, status } = useEventData();
const { t } = useTranslation();
const { branding } = useEventBranding();
const isReady = status === 'ready' && !!event;
if (!token || !isReady) return null; // Only show bottom nav within event context
if (!token || !isReady) return null;
const base = `/e/${encodeURIComponent(token)}`;
const currentPath = location.pathname;
@@ -47,33 +60,36 @@ export default function BottomNav() {
gallery: t('navigation.gallery'),
};
// Improved active state logic
const isHomeActive = currentPath === base || currentPath === `/${token}`;
const isTasksActive = currentPath.startsWith(`${base}/tasks`) || currentPath === `${base}/upload`;
const isAchievementsActive = currentPath.startsWith(`${base}/achievements`);
const isGalleryActive = currentPath.startsWith(`${base}/gallery`) || currentPath.startsWith(`${base}/photos`);
return (
<div className="fixed inset-x-0 bottom-0 z-30 border-t border-white/30 bg-white/70 px-2 py-2 shadow-xl backdrop-blur-xl dark:border-white/10 dark:bg-gradient-to-t dark:from-gray-950/85 dark:via-gray-900/70 dark:to-gray-900/35 dark:backdrop-blur-2xl dark:shadow-[0_18px_60px_rgba(15,15,30,0.6)]">
<div className="mx-auto flex max-w-sm items-center justify-around">
<TabLink to={`${base}`} isActive={isHomeActive}>
<div className="fixed inset-x-0 bottom-0 z-30 border-t border-white/20 bg-gradient-to-t from-black/30 via-black/10 to-transparent px-3 py-2 shadow-xl backdrop-blur-xl dark:border-white/10 dark:from-gray-950/85 dark:via-gray-900/70 dark:to-gray-900/35 dark:backdrop-blur-2xl dark:shadow-[0_18px_60px_rgba(15,15,30,0.6)]">
<div className="mx-auto flex max-w-sm items-center justify-around gap-2">
<TabLink to={`${base}`} isActive={isHomeActive} accentColor={branding.primaryColor}>
<div className="flex flex-col items-center gap-1">
<Home className="h-5 w-5" /> <span className="text-xs">{labels.home}</span>
<Home className="h-5 w-5" aria-hidden />
<span>{labels.home}</span>
</div>
</TabLink>
<TabLink to={`${base}/tasks`} isActive={isTasksActive}>
<TabLink to={`${base}/tasks`} isActive={isTasksActive} accentColor={branding.primaryColor}>
<div className="flex flex-col items-center gap-1">
<CheckSquare className="h-5 w-5" /> <span className="text-xs">{labels.tasks}</span>
<CheckSquare className="h-5 w-5" aria-hidden />
<span>{labels.tasks}</span>
</div>
</TabLink>
<TabLink to={`${base}/achievements`} isActive={isAchievementsActive}>
<TabLink to={`${base}/achievements`} isActive={isAchievementsActive} accentColor={branding.primaryColor}>
<div className="flex flex-col items-center gap-1">
<Trophy className="h-5 w-5" /> <span className="text-xs">{labels.achievements}</span>
<Trophy className="h-5 w-5" aria-hidden />
<span>{labels.achievements}</span>
</div>
</TabLink>
<TabLink to={`${base}/gallery`} isActive={isGalleryActive}>
<TabLink to={`${base}/gallery`} isActive={isGalleryActive} accentColor={branding.primaryColor}>
<div className="flex flex-col items-center gap-1">
<GalleryHorizontal className="h-5 w-5" /> <span className="text-xs">{labels.gallery}</span>
<GalleryHorizontal className="h-5 w-5" aria-hidden />
<span>{labels.gallery}</span>
</div>
</TabLink>
</div>

View File

@@ -6,6 +6,7 @@ import { useOptionalEventStats } from '../context/EventStatsContext';
import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
import { SettingsSheet } from './settings-sheet';
import { useTranslation } from '../i18n/useTranslation';
import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '../context/EventBrandingContext';
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
heart: Heart,
@@ -36,7 +37,7 @@ function getInitials(name: string): string {
return name.substring(0, 2).toUpperCase();
}
function renderEventAvatar(name: string, icon: unknown) {
function renderEventAvatar(name: string, icon: unknown, accentColor: string, textColor: string) {
if (typeof icon === 'string') {
const trimmed = icon.trim();
if (trimmed) {
@@ -44,7 +45,10 @@ function renderEventAvatar(name: string, icon: unknown) {
const IconComponent = EVENT_ICON_COMPONENTS[normalized];
if (IconComponent) {
return (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-pink-100 text-pink-600">
<div
className="flex h-10 w-10 items-center justify-center rounded-full shadow-sm"
style={{ backgroundColor: accentColor, color: textColor }}
>
<IconComponent className="h-5 w-5" aria-hidden />
</div>
);
@@ -52,7 +56,10 @@ function renderEventAvatar(name: string, icon: unknown) {
if (isLikelyEmoji(trimmed)) {
return (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-pink-100 text-pink-600 text-xl">
<div
className="flex h-10 w-10 items-center justify-center rounded-full text-xl shadow-sm"
style={{ backgroundColor: accentColor, color: textColor }}
>
<span aria-hidden>{trimmed}</span>
<span className="sr-only">{name}</span>
</div>
@@ -62,7 +69,10 @@ function renderEventAvatar(name: string, icon: unknown) {
}
return (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-pink-100 text-pink-600 font-semibold text-sm">
<div
className="flex h-10 w-10 items-center justify-center rounded-full font-semibold text-sm shadow-sm"
style={{ backgroundColor: accentColor, color: textColor }}
>
{getInitials(name)}
</div>
);
@@ -72,11 +82,18 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
const statsContext = useOptionalEventStats();
const identity = useOptionalGuestIdentity();
const { t } = useTranslation();
const brandingContext = useOptionalEventBranding();
const branding = brandingContext?.branding ?? DEFAULT_EVENT_BRANDING;
const primaryForeground = '#ffffff';
const { event, status } = useEventData();
if (!eventToken) {
const guestName = identity?.name && identity?.hydrated ? identity.name : null;
return (
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40">
<div
className="sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40"
style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}
>
<div className="flex flex-col">
<div className="font-semibold">{title}</div>
{guestName && (
@@ -93,14 +110,21 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
);
}
const { event, status } = useEventData();
const guestName =
identity && identity.eventKey === eventToken && identity.hydrated && identity.name ? identity.name : null;
const headerStyle: React.CSSProperties = {
background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`,
color: primaryForeground,
fontFamily: branding.fontFamily ?? undefined,
};
const accentColor = branding.secondaryColor;
if (status === 'loading') {
return (
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40">
<div className="font-semibold">{t('header.loading')}</div>
<div className="sticky top-0 z-20 flex items-center justify-between border-b border-white/10 px-4 py-2 text-white shadow-sm backdrop-blur" style={headerStyle}>
<div className="font-semibold" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>{t('header.loading')}</div>
<div className="flex items-center gap-2">
<AppearanceToggleDropdown />
<SettingsSheet />
@@ -117,17 +141,20 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
statsContext && statsContext.eventKey === eventToken ? statsContext : undefined;
return (
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40">
<div
className="sticky top-0 z-20 flex items-center justify-between border-b border-white/10 px-4 py-3 text-white shadow-sm backdrop-blur"
style={headerStyle}
>
<div className="flex items-center gap-3">
{renderEventAvatar(event.name, event.type?.icon)}
<div className="flex flex-col">
{renderEventAvatar(event.name, event.type?.icon, accentColor, primaryForeground)}
<div className="flex flex-col" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>
<div className="font-semibold text-base">{event.name}</div>
{guestName && (
<span className="text-xs text-muted-foreground">
<span className="text-xs text-white/80">
{`${t('common.hi')} ${guestName}`}
</span>
)}
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<div className="flex items-center gap-2 text-xs text-white/70">
{stats && (
<>
<span className="flex items-center gap-1">