events werden nun erfolgreich gespeichert, branding wird nun erfolgreich gespeichert, emotionen können nun angelegt werden. Task Ansicht im Event admin verbessert, Buttons in FAB umgewandelt und vereinheitlicht. Teilen-Link Guest PWA schicker gemacht, SynGoogleFonts ausgebaut (mit Einzel-Family-Download).
This commit is contained in:
@@ -178,7 +178,7 @@ export function CommandShelf() {
|
||||
},
|
||||
{
|
||||
key: 'invites',
|
||||
label: t('commandShelf.actions.invites.label', 'QR & Einladungen'),
|
||||
label: t('commandShelf.actions.invites.label', 'QR-Codes'),
|
||||
description: t('commandShelf.actions.invites.desc', 'Layouts exportieren oder Links kopieren.'),
|
||||
icon: QrCode,
|
||||
href: ADMIN_EVENT_INVITES_PATH(slug),
|
||||
@@ -220,7 +220,7 @@ export function CommandShelf() {
|
||||
},
|
||||
{
|
||||
key: 'invites',
|
||||
label: t('commandShelf.metrics.invites', 'Einladungen'),
|
||||
label: t('commandShelf.metrics.invites', 'QR-Codes'),
|
||||
value: activeEvent.active_invites_count ?? activeEvent.total_invites_count,
|
||||
hint: t('commandShelf.metrics.invitesHint', 'live'),
|
||||
},
|
||||
@@ -373,7 +373,7 @@ export function CommandShelf() {
|
||||
{t('commandShelf.mobile.sheetTitle', 'Schnellaktionen')}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
{t('commandShelf.mobile.sheetDescription', 'Moderation, Aufgaben und Einladungen an einem Ort.')}
|
||||
{t('commandShelf.mobile.sheetDescription', 'Moderation, Aufgaben und QR-Codes an einem Ort.')}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-wrap gap-2 px-4 text-xs text-slate-500 dark:text-slate-300">
|
||||
|
||||
@@ -36,7 +36,7 @@ function buildEventLinks(slug: string, t: ReturnType<typeof useTranslation>['t']
|
||||
{ key: 'photobooth', label: t('eventMenu.photobooth', 'Photobooth'), href: ADMIN_EVENT_PHOTOBOOTH_PATH(slug) },
|
||||
{ key: 'guests', label: t('eventMenu.guests', 'Team & Gäste'), href: ADMIN_EVENT_MEMBERS_PATH(slug) },
|
||||
{ key: 'tasks', label: t('eventMenu.tasks', 'Aufgaben'), href: ADMIN_EVENT_TASKS_PATH(slug) },
|
||||
{ key: 'invites', label: t('eventMenu.invites', 'Einladungen'), href: ADMIN_EVENT_INVITES_PATH(slug) },
|
||||
{ key: 'invites', label: t('eventMenu.invites', 'QR-Codes'), href: ADMIN_EVENT_INVITES_PATH(slug) },
|
||||
{ key: 'branding', label: t('eventMenu.branding', 'Branding & Fonts'), href: ADMIN_EVENT_BRANDING_PATH(slug) },
|
||||
];
|
||||
}
|
||||
|
||||
70
resources/js/admin/components/FloatingActionBar.tsx
Normal file
70
resources/js/admin/components/FloatingActionBar.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type ActionTone = 'primary' | 'secondary' | 'danger' | 'neutral';
|
||||
|
||||
export type FloatingAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
onClick: () => void;
|
||||
tone?: ActionTone;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export function FloatingActionBar({ actions, className }: { actions: FloatingAction[]; className?: string }): React.ReactElement | null {
|
||||
if (!actions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toneClasses: Record<ActionTone, string> = {
|
||||
primary: 'bg-primary text-primary-foreground shadow-primary/25 hover:bg-primary/90 focus-visible:ring-primary/70 border border-primary/20',
|
||||
secondary: 'bg-[var(--tenant-surface-strong)] text-[var(--tenant-foreground)] shadow-slate-300/60 hover:bg-[var(--tenant-surface)] focus-visible:ring-slate-200 border border-[var(--tenant-border-strong)]',
|
||||
neutral: 'bg-white/90 text-slate-900 shadow-slate-200/80 hover:bg-white focus-visible:ring-slate-200 border border-slate-200 dark:bg-slate-800/80 dark:text-white dark:border-slate-700',
|
||||
danger: 'bg-rose-500 text-white shadow-rose-300/50 hover:bg-rose-600 focus-visible:ring-rose-200 border border-rose-400/80',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none fixed inset-x-4 bottom-[calc(env(safe-area-inset-bottom,0px)+72px)] z-50 sm:inset-auto sm:right-6 sm:bottom-6',
|
||||
className
|
||||
)}
|
||||
style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}
|
||||
>
|
||||
<div className="pointer-events-auto flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
|
||||
{actions.map((action) => {
|
||||
const Icon = action.icon;
|
||||
const tone = action.tone ?? 'primary';
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={action.key}
|
||||
size="lg"
|
||||
className={cn(
|
||||
'group flex h-11 w-11 items-center justify-center gap-0 rounded-full p-0 text-sm font-semibold shadow-lg transition-all duration-150 focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-auto sm:w-auto sm:gap-2 sm:px-4 sm:py-2',
|
||||
toneClasses[tone]
|
||||
)}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled || action.loading}
|
||||
aria-label={action.ariaLabel ?? action.label}
|
||||
>
|
||||
{action.loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Icon className="h-4 w-4" />
|
||||
)}
|
||||
<span className="hidden sm:inline">{action.label}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export function DashboardEventFocusCard({
|
||||
},
|
||||
{
|
||||
key: 'invites',
|
||||
label: t('stats.invites', 'Einladungen live'),
|
||||
label: t('stats.invites', 'QR-Codes live'),
|
||||
value: Number(event.active_invites_count ?? event.total_invites_count ?? 0).toLocaleString(),
|
||||
},
|
||||
];
|
||||
@@ -110,7 +110,7 @@ export function DashboardEventFocusCard({
|
||||
},
|
||||
{
|
||||
key: 'invites',
|
||||
label: t('actions.invites', 'QR & Einladungen'),
|
||||
label: t('actions.invites', 'QR-Codes'),
|
||||
description: t('actions.invitesHint', 'Layouts exportieren oder Links kopieren.'),
|
||||
icon: QrCode,
|
||||
handler: onOpenInvites,
|
||||
|
||||
Reference in New Issue
Block a user