Fix TypeScript typecheck errors
This commit is contained in:
@@ -16,9 +16,10 @@ export interface TenantProfile {
|
|||||||
id: number;
|
id: number;
|
||||||
tenant_id: number;
|
tenant_id: number;
|
||||||
role?: string | null;
|
role?: string | null;
|
||||||
name?: string;
|
name?: string | null;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
avatar_url?: string | null;
|
||||||
features?: Record<string, unknown>;
|
features?: Record<string, unknown>;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Event-Admin",
|
"title": "Event-Admin",
|
||||||
|
"pageTitle": "Login",
|
||||||
"badge": "Fotospiel Event Admin",
|
"badge": "Fotospiel Event Admin",
|
||||||
"hero_tagline": "Kontrolle behalten, entspannt bleiben",
|
"hero_tagline": "Kontrolle behalten, entspannt bleiben",
|
||||||
"hero_title": "Das Cockpit für dein Fotospiel Event",
|
"hero_title": "Das Cockpit für dein Fotospiel Event",
|
||||||
@@ -90,6 +91,9 @@
|
|||||||
"appearance_label": "Darstellung"
|
"appearance_label": "Darstellung"
|
||||||
},
|
},
|
||||||
"redirecting": "Weiterleitung zum Login …",
|
"redirecting": "Weiterleitung zum Login …",
|
||||||
|
"logout": {
|
||||||
|
"title": "Abmeldung wird vorbereitet …"
|
||||||
|
},
|
||||||
"processing": {
|
"processing": {
|
||||||
"title": "Anmeldung wird verarbeitet …",
|
"title": "Anmeldung wird verarbeitet …",
|
||||||
"copy": "Einen Moment bitte, wir bereiten dein Dashboard vor."
|
"copy": "Einen Moment bitte, wir bereiten dein Dashboard vor."
|
||||||
|
|||||||
@@ -983,6 +983,7 @@
|
|||||||
"layouts": "Druck-Layouts",
|
"layouts": "Druck-Layouts",
|
||||||
"preview": "Anpassen & Exportieren",
|
"preview": "Anpassen & Exportieren",
|
||||||
"createLink": "Neuen QR-Link erstellen",
|
"createLink": "Neuen QR-Link erstellen",
|
||||||
|
"createLinkConfirm": "Neuen QR-Link erstellen? Dadurch werden alle bisherigen Ausdrucke ungültig und alle Personen mit dem alten Link verlieren den Zugang.",
|
||||||
"mobileLinkLabel": "Mobiler Link",
|
"mobileLinkLabel": "Mobiler Link",
|
||||||
"created": "Neuer QR-Link erstellt",
|
"created": "Neuer QR-Link erstellt",
|
||||||
"createFailed": "Link konnte nicht erstellt werden.",
|
"createFailed": "Link konnte nicht erstellt werden.",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"appName": "Event Admin",
|
"appName": "Event Admin",
|
||||||
|
"documentTitle": "Fotospiel.App Event Admin",
|
||||||
"selectEvent": "Wähle ein Event, um fortzufahren",
|
"selectEvent": "Wähle ein Event, um fortzufahren",
|
||||||
"empty": "Lege dein erstes Event an, um zu starten",
|
"empty": "Lege dein erstes Event an, um zu starten",
|
||||||
"eventSwitcher": "Event auswählen",
|
"eventSwitcher": "Event auswählen",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Event Admin",
|
"title": "Event Admin",
|
||||||
|
"pageTitle": "Login",
|
||||||
"badge": "Fotospiel Event Admin",
|
"badge": "Fotospiel Event Admin",
|
||||||
"hero_tagline": "Stay in control, stay relaxed",
|
"hero_tagline": "Stay in control, stay relaxed",
|
||||||
"hero_title": "Your cockpit for every Fotospiel event",
|
"hero_title": "Your cockpit for every Fotospiel event",
|
||||||
@@ -90,6 +91,9 @@
|
|||||||
"appearance_label": "Appearance"
|
"appearance_label": "Appearance"
|
||||||
},
|
},
|
||||||
"redirecting": "Redirecting to login …",
|
"redirecting": "Redirecting to login …",
|
||||||
|
"logout": {
|
||||||
|
"title": "Signing out …"
|
||||||
|
},
|
||||||
"processing": {
|
"processing": {
|
||||||
"title": "Signing you in …",
|
"title": "Signing you in …",
|
||||||
"copy": "One moment please while we prepare your dashboard."
|
"copy": "One moment please while we prepare your dashboard."
|
||||||
|
|||||||
@@ -979,6 +979,7 @@
|
|||||||
"layouts": "Print Layouts",
|
"layouts": "Print Layouts",
|
||||||
"preview": "Customize & Export",
|
"preview": "Customize & Export",
|
||||||
"createLink": "Create new QR link",
|
"createLink": "Create new QR link",
|
||||||
|
"createLinkConfirm": "Create a new QR link? This will invalidate all printed materials and everyone with the old link will lose access.",
|
||||||
"mobileLinkLabel": "Mobile link",
|
"mobileLinkLabel": "Mobile link",
|
||||||
"created": "New QR link created",
|
"created": "New QR link created",
|
||||||
"createFailed": "Could not create link.",
|
"createFailed": "Could not create link.",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"appName": "Event Admin",
|
"appName": "Event Admin",
|
||||||
|
"documentTitle": "Fotospiel.App Event Admin",
|
||||||
"selectEvent": "Select an event to continue",
|
"selectEvent": "Select an event to continue",
|
||||||
"empty": "Create your first event to get started",
|
"empty": "Create your first event to get started",
|
||||||
"eventSwitcher": "Choose an event",
|
"eventSwitcher": "Choose an event",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useAuth } from '../auth/context';
|
|||||||
import { ADMIN_DEFAULT_AFTER_LOGIN_PATH } from '../constants';
|
import { ADMIN_DEFAULT_AFTER_LOGIN_PATH } from '../constants';
|
||||||
import { decodeReturnTo, resolveReturnTarget } from '../lib/returnTo';
|
import { decodeReturnTo, resolveReturnTarget } from '../lib/returnTo';
|
||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
|
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||||
|
|
||||||
export default function AuthCallbackPage(): React.ReactElement {
|
export default function AuthCallbackPage(): React.ReactElement {
|
||||||
const { status } = useAuth();
|
const { status } = useAuth();
|
||||||
@@ -21,6 +22,8 @@ export default function AuthCallbackPage(): React.ReactElement {
|
|||||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useDocumentTitle(t('processing.title', 'Signing you in …'));
|
||||||
|
|
||||||
const searchParams = React.useMemo(() => new URLSearchParams(window.location.search), []);
|
const searchParams = React.useMemo(() => new URLSearchParams(window.location.search), []);
|
||||||
const rawReturnTo = searchParams.get('return_to');
|
const rawReturnTo = searchParams.get('return_to');
|
||||||
|
|
||||||
|
|||||||
@@ -1324,7 +1324,7 @@ function LabeledSlider({
|
|||||||
max={max}
|
max={max}
|
||||||
step={step}
|
step={step}
|
||||||
value={[value]}
|
value={[value]}
|
||||||
onValueChange={(next) => onChange(next[0] ?? value)}
|
onValueChange={(next: number[]) => onChange(next[0] ?? value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Slider.Track height={6} borderRadius={999} backgroundColor={border}>
|
<Slider.Track height={6} borderRadius={999} backgroundColor={border}>
|
||||||
|
|||||||
@@ -108,12 +108,13 @@ function SectionHeader({
|
|||||||
|
|
||||||
function StatusBadge({ status }: { status: string }) {
|
function StatusBadge({ status }: { status: string }) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const config =
|
type StatusTone = 'success' | 'warning' | 'danger' | 'muted';
|
||||||
{
|
const statuses: Record<string, { tone: StatusTone; label: string }> = {
|
||||||
published: { tone: 'success', label: t('events.status.published', 'Live') },
|
published: { tone: 'success', label: t('events.status.published', 'Live') },
|
||||||
draft: { tone: 'warning', label: t('events.status.draft', 'Draft') },
|
draft: { tone: 'warning', label: t('events.status.draft', 'Draft') },
|
||||||
archived: { tone: 'muted', label: t('events.status.archived', 'Archived') },
|
archived: { tone: 'muted', label: t('events.status.archived', 'Archived') },
|
||||||
}[status] || { tone: 'muted', label: status };
|
};
|
||||||
|
const config = statuses[status] ?? { tone: 'muted', label: status };
|
||||||
|
|
||||||
return <PillBadge tone={config.tone}>{config.label}</PillBadge>;
|
return <PillBadge tone={config.tone}>{config.label}</PillBadge>;
|
||||||
}
|
}
|
||||||
@@ -408,7 +409,7 @@ function LifecycleHero({
|
|||||||
<DashboardCard variant={cardVariant} padding={cardPadding}>
|
<DashboardCard variant={cardVariant} padding={cardPadding}>
|
||||||
<YStack space={isEmbedded ? '$2.5' : '$3'}>
|
<YStack space={isEmbedded ? '$2.5' : '$3'}>
|
||||||
<XStack alignItems="center" space="$2.5">
|
<XStack alignItems="center" space="$2.5">
|
||||||
<YStack width={40} height={40} borderRadius={20} backgroundColor={theme.success} alignItems="center" justifyContent="center">
|
<YStack width={40} height={40} borderRadius={20} backgroundColor={theme.successText} alignItems="center" justifyContent="center">
|
||||||
<CheckCircle2 size={20} color="white" />
|
<CheckCircle2 size={20} color="white" />
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack>
|
<YStack>
|
||||||
@@ -486,7 +487,7 @@ function LifecycleHero({
|
|||||||
</Text>
|
</Text>
|
||||||
<Switch
|
<Switch
|
||||||
checked={published}
|
checked={published}
|
||||||
onCheckedChange={(checked) => handlePublishChange(Boolean(checked))}
|
onCheckedChange={(checked: boolean) => handlePublishChange(Boolean(checked))}
|
||||||
size="$2"
|
size="$2"
|
||||||
disabled={isPublishing}
|
disabled={isPublishing}
|
||||||
aria-label={t('eventForm.fields.publish.label', 'Publish immediately')}
|
aria-label={t('eventForm.fields.publish.label', 'Publish immediately')}
|
||||||
@@ -625,26 +626,27 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
|
|||||||
const { t } = useTranslation(['management', 'dashboard']);
|
const { t } = useTranslation(['management', 'dashboard']);
|
||||||
const slug = event?.slug;
|
const slug = event?.slug;
|
||||||
if (!slug) return null;
|
if (!slug) return null;
|
||||||
|
type ToolItem = { label: string; icon: any; path: string; color?: string };
|
||||||
|
|
||||||
const experienceItems = [
|
const experienceItems: ToolItem[] = [
|
||||||
{ label: t('management:photos.gallery.title', 'Photos'), icon: ImageIcon, path: `/mobile/events/${slug}/control-room`, color: theme.primary },
|
{ label: t('management:photos.gallery.title', 'Photos'), icon: ImageIcon, path: `/mobile/events/${slug}/control-room`, color: theme.primary },
|
||||||
!isCompleted ? { label: t('management:events.quick.liveShowSettings', 'Slide Show'), icon: Tv, path: `/mobile/events/${slug}/live-show/settings`, color: '#F59E0B' } : null,
|
!isCompleted ? { label: t('management:events.quick.liveShowSettings', 'Slide Show'), icon: Tv, path: `/mobile/events/${slug}/live-show/settings`, color: '#F59E0B' } : null,
|
||||||
!isCompleted ? { label: t('events.tasks.badge', 'Photo tasks'), icon: ListTodo, path: `/mobile/events/${slug}/tasks`, color: theme.accent } : null,
|
!isCompleted ? { label: t('events.tasks.badge', 'Photo tasks'), icon: ListTodo, path: `/mobile/events/${slug}/tasks`, color: theme.accent } : null,
|
||||||
!isCompleted ? { label: t('management:events.quick.photobooth', 'Photobooth'), icon: Camera, path: `/mobile/events/${slug}/photobooth`, color: '#8B5CF6' } : null,
|
!isCompleted ? { label: t('management:events.quick.photobooth', 'Photobooth'), icon: Camera, path: `/mobile/events/${slug}/photobooth`, color: '#8B5CF6' } : null,
|
||||||
].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item));
|
].filter(Boolean) as ToolItem[];
|
||||||
|
|
||||||
const operationsItems = [
|
const operationsItems: ToolItem[] = [
|
||||||
!isCompleted ? { label: t('management:invites.badge', 'QR Codes'), icon: QrCode, path: `/mobile/events/${slug}/qr`, color: '#10B981' } : null,
|
!isCompleted ? { label: t('management:invites.badge', 'QR Codes'), icon: QrCode, path: `/mobile/events/${slug}/qr`, color: '#10B981' } : null,
|
||||||
{ label: t('management:events.quick.guests', 'Guests'), icon: Users, path: `/mobile/events/${slug}/members`, color: ADMIN_ACTION_COLORS.guests },
|
{ label: t('management:events.quick.guests', 'Guests'), icon: Users, path: `/mobile/events/${slug}/members`, color: ADMIN_ACTION_COLORS.guests },
|
||||||
!isCompleted ? { label: t('management:events.quick.guestMessages', 'Messages'), icon: Megaphone, path: `/mobile/events/${slug}/guest-notifications`, color: ADMIN_ACTION_COLORS.guestMessages } : null,
|
!isCompleted ? { label: t('management:events.quick.guestMessages', 'Messages'), icon: Megaphone, path: `/mobile/events/${slug}/guest-notifications`, color: ADMIN_ACTION_COLORS.guestMessages } : null,
|
||||||
!isCompleted ? { label: t('events.branding.titleShort', 'Branding'), icon: Layout, path: `/mobile/events/${slug}/branding`, color: ADMIN_ACTION_COLORS.branding } : null,
|
!isCompleted ? { label: t('events.branding.titleShort', 'Branding'), icon: Layout, path: `/mobile/events/${slug}/branding`, color: ADMIN_ACTION_COLORS.branding } : null,
|
||||||
].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item));
|
].filter(Boolean) as ToolItem[];
|
||||||
|
|
||||||
const adminItems = [
|
const adminItems: ToolItem[] = [
|
||||||
{ label: t('management:mobileDashboard.shortcutAnalytics', 'Analytics'), icon: TrendingUp, path: `/mobile/events/${slug}/analytics`, color: ADMIN_ACTION_COLORS.analytics },
|
{ label: t('management:mobileDashboard.shortcutAnalytics', 'Analytics'), icon: TrendingUp, path: `/mobile/events/${slug}/analytics`, color: ADMIN_ACTION_COLORS.analytics },
|
||||||
!isCompleted ? { label: t('events.recap.exportTitleShort', 'Exports'), icon: Download, path: `/mobile/exports`, color: ADMIN_ACTION_COLORS.recap } : null,
|
!isCompleted ? { label: t('events.recap.exportTitleShort', 'Exports'), icon: Download, path: `/mobile/exports`, color: ADMIN_ACTION_COLORS.recap } : null,
|
||||||
{ label: t('management:mobileProfile.settings', 'Settings'), icon: Settings, path: `/mobile/events/${slug}/edit`, color: ADMIN_ACTION_COLORS.settings },
|
{ label: t('management:mobileProfile.settings', 'Settings'), icon: Settings, path: `/mobile/events/${slug}/edit`, color: ADMIN_ACTION_COLORS.settings },
|
||||||
].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item));
|
].filter(Boolean) as ToolItem[];
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{
|
||||||
@@ -719,11 +721,11 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[], navigate: any, slug: string }) {
|
function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]; navigate: any; slug?: string }) {
|
||||||
const theme = useAdminTheme();
|
const theme = useAdminTheme();
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
|
|
||||||
if (!photos.length) return null;
|
if (!photos.length || !slug) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardCard>
|
<DashboardCard>
|
||||||
|
|||||||
@@ -1110,7 +1110,7 @@ export default function MobileEventControlRoomPage() {
|
|||||||
size="$3"
|
size="$3"
|
||||||
checked={autoApproveHighlights}
|
checked={autoApproveHighlights}
|
||||||
disabled={controlRoomSaving}
|
disabled={controlRoomSaving}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked: boolean) =>
|
||||||
saveControlRoomSettings({
|
saveControlRoomSettings({
|
||||||
...controlRoomSettings,
|
...controlRoomSettings,
|
||||||
auto_approve_highlights: Boolean(checked),
|
auto_approve_highlights: Boolean(checked),
|
||||||
@@ -1139,7 +1139,7 @@ export default function MobileEventControlRoomPage() {
|
|||||||
size="$3"
|
size="$3"
|
||||||
checked={autoAddApprovedToLive}
|
checked={autoAddApprovedToLive}
|
||||||
disabled={controlRoomSaving || isImmediateUploads}
|
disabled={controlRoomSaving || isImmediateUploads}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked: boolean) => {
|
||||||
if (isImmediateUploads) {
|
if (isImmediateUploads) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1164,7 +1164,7 @@ export default function MobileEventControlRoomPage() {
|
|||||||
size="$3"
|
size="$3"
|
||||||
checked={autoRemoveLiveOnHide}
|
checked={autoRemoveLiveOnHide}
|
||||||
disabled={controlRoomSaving}
|
disabled={controlRoomSaving}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked: boolean) =>
|
||||||
saveControlRoomSettings({
|
saveControlRoomSettings({
|
||||||
...controlRoomSettings,
|
...controlRoomSettings,
|
||||||
auto_remove_live_on_hide: Boolean(checked),
|
auto_remove_live_on_hide: Boolean(checked),
|
||||||
@@ -1388,7 +1388,7 @@ export default function MobileEventControlRoomPage() {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
value={moderationFilter}
|
value={moderationFilter}
|
||||||
onValueChange={(value) => value && setModerationFilter(value as ModerationFilter)}
|
onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)}
|
||||||
>
|
>
|
||||||
<XStack space="$1.5">
|
<XStack space="$1.5">
|
||||||
{MODERATION_FILTERS.map((option) => {
|
{MODERATION_FILTERS.map((option) => {
|
||||||
@@ -1576,7 +1576,7 @@ export default function MobileEventControlRoomPage() {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
value={liveStatusFilter}
|
value={liveStatusFilter}
|
||||||
onValueChange={(value) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
|
onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
|
||||||
>
|
>
|
||||||
<XStack space="$1.5">
|
<XStack space="$1.5">
|
||||||
{LIVE_STATUS_OPTIONS.map((option) => {
|
{LIVE_STATUS_OPTIONS.map((option) => {
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ export default function MobileEventFormPage() {
|
|||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={form.published}
|
checked={form.published}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked: boolean) =>
|
||||||
setForm((prev) => ({ ...prev, published: Boolean(checked) }))
|
setForm((prev) => ({ ...prev, published: Boolean(checked) }))
|
||||||
}
|
}
|
||||||
size="$3"
|
size="$3"
|
||||||
@@ -546,7 +546,7 @@ export default function MobileEventFormPage() {
|
|||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={form.tasksEnabled}
|
checked={form.tasksEnabled}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked: boolean) =>
|
||||||
setForm((prev) => ({ ...prev, tasksEnabled: Boolean(checked) }))
|
setForm((prev) => ({ ...prev, tasksEnabled: Boolean(checked) }))
|
||||||
}
|
}
|
||||||
size="$3"
|
size="$3"
|
||||||
@@ -577,7 +577,7 @@ export default function MobileEventFormPage() {
|
|||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={form.autoApproveUploads}
|
checked={form.autoApproveUploads}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked: boolean) =>
|
||||||
setForm((prev) => ({ ...prev, autoApproveUploads: Boolean(checked) }))
|
setForm((prev) => ({ ...prev, autoApproveUploads: Boolean(checked) }))
|
||||||
}
|
}
|
||||||
size="$3"
|
size="$3"
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ function EffectSlider({
|
|||||||
max={max}
|
max={max}
|
||||||
step={step}
|
step={step}
|
||||||
value={[value]}
|
value={[value]}
|
||||||
onValueChange={(next) => onChange(next[0] ?? value)}
|
onValueChange={(next: number[]) => onChange(next[0] ?? value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Slider.Track height={6} borderRadius={999} backgroundColor={border}>
|
<Slider.Track height={6} borderRadius={999} backgroundColor={border}>
|
||||||
|
|||||||
@@ -1158,7 +1158,7 @@ export default function MobileEventTasksPage() {
|
|||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onValueChange={(value) => setActiveTab(value as TaskSectionKey)}
|
onValueChange={(value: string) => setActiveTab(value as TaskSectionKey)}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -1411,7 +1411,7 @@ export default function MobileEventTasksPage() {
|
|||||||
>
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={emotionFilter}
|
value={emotionFilter}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val: string) => {
|
||||||
setEmotionFilter(val);
|
setEmotionFilter(val);
|
||||||
setShowEmotionFilterSheet(false);
|
setShowEmotionFilterSheet(false);
|
||||||
}}
|
}}
|
||||||
@@ -1441,7 +1441,7 @@ export default function MobileEventTasksPage() {
|
|||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={Boolean(deleteCandidate)}
|
open={Boolean(deleteCandidate)}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setDeleteCandidate(null);
|
setDeleteCandidate(null);
|
||||||
}
|
}
|
||||||
@@ -1496,7 +1496,7 @@ export default function MobileEventTasksPage() {
|
|||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={bulkDeleteOpen}
|
open={bulkDeleteOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setBulkDeleteOpen(false);
|
setBulkDeleteOpen(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ function EventsList({
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onValueChange={(value) => value && onStatusChange(value as EventStatusKey)}
|
onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)}
|
||||||
>
|
>
|
||||||
<XStack space="$1.5">
|
<XStack space="$1.5">
|
||||||
{filters.map((filter) => {
|
{filters.map((filter) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ADMIN_LOGIN_PATH } from '../constants';
|
|||||||
import { MobileCard } from './components/Primitives';
|
import { MobileCard } from './components/Primitives';
|
||||||
import { MobileField, MobileInput } from './components/FormControls';
|
import { MobileField, MobileInput } from './components/FormControls';
|
||||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||||
|
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||||
|
|
||||||
type ResetResponse = {
|
type ResetResponse = {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -51,6 +52,8 @@ export default function ForgotPasswordPage() {
|
|||||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useDocumentTitle(t('login.forgot.title', 'Forgot your password?'));
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: ['tenantAdminForgotPassword'],
|
mutationKey: ['tenantAdminForgotPassword'],
|
||||||
mutationFn: requestPasswordReset,
|
mutationFn: requestPasswordReset,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { Button } from '@tamagui/button';
|
|||||||
import { MobileCard } from './components/Primitives';
|
import { MobileCard } from './components/Primitives';
|
||||||
import { MobileField, MobileInput } from './components/FormControls';
|
import { MobileField, MobileInput } from './components/FormControls';
|
||||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||||
|
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||||
|
|
||||||
type LoginResponse = {
|
type LoginResponse = {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -66,6 +67,8 @@ export default function MobileLoginPage() {
|
|||||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useDocumentTitle(t('login.pageTitle', 'Login'));
|
||||||
|
|
||||||
const searchParams = React.useMemo(() => new URLSearchParams(location.search), [location.search]);
|
const searchParams = React.useMemo(() => new URLSearchParams(location.search), [location.search]);
|
||||||
const rawReturnTo = searchParams.get('return_to');
|
const rawReturnTo = searchParams.get('return_to');
|
||||||
const oauthError = searchParams.get('error');
|
const oauthError = searchParams.get('error');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SizableText as Text } from '@tamagui/text';
|
|||||||
import { Spinner } from 'tamagui';
|
import { Spinner } from 'tamagui';
|
||||||
import { ADMIN_LOGIN_PATH } from '../constants';
|
import { ADMIN_LOGIN_PATH } from '../constants';
|
||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
|
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||||
|
|
||||||
export default function LoginStartPage(): React.ReactElement {
|
export default function LoginStartPage(): React.ReactElement {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -19,6 +20,8 @@ export default function LoginStartPage(): React.ReactElement {
|
|||||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useDocumentTitle(t('redirecting', 'Redirecting to login …'));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const returnTo = params.get('return_to');
|
const returnTo = params.get('return_to');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from '@tamagui/card';
|
import { Card } from '@tamagui/card';
|
||||||
import { YStack } from '@tamagui/stacks';
|
import { YStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
@@ -6,15 +7,19 @@ import { Spinner } from 'tamagui';
|
|||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { ADMIN_PUBLIC_LANDING_PATH } from '../constants';
|
import { ADMIN_PUBLIC_LANDING_PATH } from '../constants';
|
||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
|
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||||
|
|
||||||
export default function LogoutPage() {
|
export default function LogoutPage() {
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
|
const { t } = useTranslation('auth');
|
||||||
const { textStrong, muted, border, surface, shadow, appBackground } = useAdminTheme();
|
const { textStrong, muted, border, surface, shadow, appBackground } = useAdminTheme();
|
||||||
const safeAreaStyle: React.CSSProperties = {
|
const safeAreaStyle: React.CSSProperties = {
|
||||||
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)',
|
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)',
|
||||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useDocumentTitle(t('logout.title', 'Signing out …'));
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
logout({ redirect: ADMIN_PUBLIC_LANDING_PATH });
|
logout({ redirect: ADMIN_PUBLIC_LANDING_PATH });
|
||||||
}, [logout]);
|
}, [logout]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { TFunction } from 'i18next';
|
||||||
import { Check, ChevronRight, ShieldCheck, Sparkles, X } from 'lucide-react';
|
import { Check, ChevronRight, ShieldCheck, Sparkles, X } from 'lucide-react';
|
||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
@@ -395,7 +396,11 @@ function PackageShopCompareView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return t(`shop.features.${row.featureKey}`, row.featureKey);
|
if (row.type === 'feature') {
|
||||||
|
return t(`shop.features.${row.featureKey}`, row.featureKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLimitValue = (value: number | null) => {
|
const formatLimitValue = (value: number | null) => {
|
||||||
@@ -562,7 +567,7 @@ function getPackageStatusLabel({
|
|||||||
isActive,
|
isActive,
|
||||||
owned,
|
owned,
|
||||||
}: {
|
}: {
|
||||||
t: (key: string, fallback?: string, options?: Record<string, unknown>) => string;
|
t: TFunction;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
owned?: TenantPackageSummary;
|
owned?: TenantPackageSummary;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
@@ -622,7 +627,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
|||||||
id="agb"
|
id="agb"
|
||||||
size="$4"
|
size="$4"
|
||||||
checked={agbAccepted}
|
checked={agbAccepted}
|
||||||
onCheckedChange={(checked) => setAgbAccepted(!!checked)}
|
onCheckedChange={(checked: boolean) => setAgbAccepted(!!checked)}
|
||||||
>
|
>
|
||||||
<Checkbox.Indicator>
|
<Checkbox.Indicator>
|
||||||
<Check />
|
<Check />
|
||||||
@@ -638,7 +643,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
|||||||
id="withdrawal"
|
id="withdrawal"
|
||||||
size="$4"
|
size="$4"
|
||||||
checked={withdrawalAccepted}
|
checked={withdrawalAccepted}
|
||||||
onCheckedChange={(checked) => setWithdrawalAccepted(!!checked)}
|
onCheckedChange={(checked: boolean) => setWithdrawalAccepted(!!checked)}
|
||||||
>
|
>
|
||||||
<Checkbox.Indicator>
|
<Checkbox.Indicator>
|
||||||
<Check />
|
<Check />
|
||||||
@@ -687,7 +692,7 @@ function aggregateOwnedEntries(entries: TenantPackageSummary[]): TenantPackageSu
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveIncludedTierLabel(
|
function resolveIncludedTierLabel(
|
||||||
t: (key: string, fallback?: string, options?: Record<string, unknown>) => string,
|
t: TFunction,
|
||||||
slug: string | null
|
slug: string | null
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Button } from '@tamagui/button';
|
|||||||
import { ADMIN_LOGIN_PATH } from '../constants';
|
import { ADMIN_LOGIN_PATH } from '../constants';
|
||||||
import { MobileCard, CTAButton } from './components/Primitives';
|
import { MobileCard, CTAButton } from './components/Primitives';
|
||||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||||
|
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||||
|
|
||||||
type FaqItem = {
|
type FaqItem = {
|
||||||
question: string;
|
question: string;
|
||||||
@@ -44,6 +45,8 @@ export default function PublicHelpPage() {
|
|||||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useDocumentTitle(t('login.help_title', 'Help for event admins'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack
|
<YStack
|
||||||
minHeight="100vh"
|
minHeight="100vh"
|
||||||
|
|||||||
@@ -234,6 +234,16 @@ export default function MobileQrPrintPage() {
|
|||||||
label={t('events.qr.createLink', 'Neuen QR-Link erstellen')}
|
label={t('events.qr.createLink', 'Neuen QR-Link erstellen')}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (!slug) return;
|
if (!slug) return;
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
t(
|
||||||
|
'events.qr.createLinkConfirm',
|
||||||
|
'Neuen QR-Link erstellen? Dadurch werden alle bisherigen Ausdrucke ungültig und alle Personen mit dem alten Link verlieren den Zugang.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const invite = await createQrInvite(slug, { label: t('events.qr.mobileLinkLabel', 'Mobile link') });
|
const invite = await createQrInvite(slug, { label: t('events.qr.mobileLinkLabel', 'Mobile link') });
|
||||||
setQrUrl(invite.url);
|
setQrUrl(invite.url);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ADMIN_LOGIN_PATH } from '../constants';
|
|||||||
import { MobileCard } from './components/Primitives';
|
import { MobileCard } from './components/Primitives';
|
||||||
import { MobileField, MobileInput } from './components/FormControls';
|
import { MobileField, MobileInput } from './components/FormControls';
|
||||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||||
|
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||||
|
|
||||||
type ResetResponse = {
|
type ResetResponse = {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -68,6 +69,8 @@ export default function ResetPasswordPage() {
|
|||||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useDocumentTitle(t('login.reset.title', 'Reset password'));
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: ['tenantAdminResetPassword'],
|
mutationKey: ['tenantAdminResetPassword'],
|
||||||
mutationFn: resetPassword,
|
mutationFn: resetPassword,
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export default function MobileSettingsPage() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="$4"
|
size="$4"
|
||||||
checked={pushState.subscribed}
|
checked={pushState.subscribed}
|
||||||
onCheckedChange={(value) => {
|
onCheckedChange={(value: boolean) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
void pushState.enable();
|
void pushState.enable();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||||
import { ADMIN_EVENTS_PATH } from '../../constants';
|
import { ADMIN_EVENTS_PATH } from '../../constants';
|
||||||
|
import type { TenantEvent } from '../../api';
|
||||||
|
|
||||||
const fixtures = vi.hoisted(() => ({
|
const fixtures = vi.hoisted(() => ({
|
||||||
event: {
|
event: {
|
||||||
@@ -9,14 +10,17 @@ const fixtures = vi.hoisted(() => ({
|
|||||||
name: 'Demo Wedding',
|
name: 'Demo Wedding',
|
||||||
slug: 'demo-event',
|
slug: 'demo-event',
|
||||||
event_date: '2026-02-19',
|
event_date: '2026-02-19',
|
||||||
status: 'published' as const,
|
event_type_id: null,
|
||||||
settings: { location: 'Berlin' },
|
event_type: null,
|
||||||
|
status: 'published',
|
||||||
|
engagement_mode: undefined,
|
||||||
|
settings: { location: 'Berlin', guest_upload_visibility: 'immediate' },
|
||||||
tasks_count: 4,
|
tasks_count: 4,
|
||||||
photo_count: 12,
|
photo_count: 12,
|
||||||
active_invites_count: 3,
|
active_invites_count: 3,
|
||||||
total_invites_count: 5,
|
total_invites_count: 5,
|
||||||
member_permissions: ['photos:moderate', 'tasks:manage', 'join-tokens:manage'],
|
member_permissions: ['photos:moderate', 'tasks:manage', 'join-tokens:manage'],
|
||||||
},
|
} as TenantEvent,
|
||||||
activePackage: {
|
activePackage: {
|
||||||
id: 1,
|
id: 1,
|
||||||
package_id: 1,
|
package_id: 1,
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ vi.mock('../components/Primitives', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../components/FormControls', () => ({
|
vi.mock('../components/FormControls', () => ({
|
||||||
MobileInput: ({ compact: _compact, ...props }: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
MobileInput: ({ compact: _compact, ...props }: { compact?: boolean } & React.InputHTMLAttributes<HTMLInputElement>) => (
|
||||||
|
<input {...props} />
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@tamagui/card', () => ({
|
vi.mock('@tamagui/card', () => ({
|
||||||
|
|||||||
@@ -13,18 +13,36 @@ const fixtures = vi.hoisted(() => ({
|
|||||||
invites: [
|
invites: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
token: 'invite-token',
|
||||||
url: 'https://example.test/guest/demo-event',
|
url: 'https://example.test/guest/demo-event',
|
||||||
qr_code_data_url: '',
|
label: null,
|
||||||
|
qr_code_data_url: null,
|
||||||
|
usage_limit: null,
|
||||||
|
usage_count: 0,
|
||||||
|
expires_at: null,
|
||||||
|
revoked_at: null,
|
||||||
|
is_active: true,
|
||||||
|
created_at: '2026-01-15T12:00:00Z',
|
||||||
|
metadata: {},
|
||||||
layouts: [
|
layouts: [
|
||||||
{
|
{
|
||||||
id: 'layout-1',
|
id: 'layout-1',
|
||||||
name: 'Poster Layout',
|
name: 'Poster Layout',
|
||||||
description: 'Layout description',
|
description: 'Layout description',
|
||||||
|
subtitle: 'Layout subtitle',
|
||||||
paper: 'a4',
|
paper: 'a4',
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
panel_mode: 'single',
|
panel_mode: 'single',
|
||||||
|
preview: {
|
||||||
|
background: null,
|
||||||
|
background_gradient: null,
|
||||||
|
accent: null,
|
||||||
|
text: null,
|
||||||
|
},
|
||||||
|
formats: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
layouts_url: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
const fixtures = vi.hoisted(() => ({
|
const fixtures = vi.hoisted(() => ({
|
||||||
event: {
|
event: {
|
||||||
@@ -74,7 +75,11 @@ vi.mock('../components/MobileShell', () => ({
|
|||||||
|
|
||||||
vi.mock('../components/Primitives', () => ({
|
vi.mock('../components/Primitives', () => ({
|
||||||
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
|
CTAButton: ({ label, onPress, disabled }: { label: string; onPress?: () => void; disabled?: boolean }) => (
|
||||||
|
<button type="button" onClick={onPress} disabled={disabled}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -116,6 +121,7 @@ vi.mock('../theme', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import MobileQrPrintPage from '../QrPrintPage';
|
import MobileQrPrintPage from '../QrPrintPage';
|
||||||
|
import { createQrInvite } from '../../api';
|
||||||
|
|
||||||
describe('MobileQrPrintPage', () => {
|
describe('MobileQrPrintPage', () => {
|
||||||
it('renders QR overview content', async () => {
|
it('renders QR overview content', async () => {
|
||||||
@@ -125,4 +131,19 @@ describe('MobileQrPrintPage', () => {
|
|||||||
expect(screen.getByText('Schritt 1: Format wählen')).toBeInTheDocument();
|
expect(screen.getByText('Schritt 1: Format wählen')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('Neuen QR-Link erstellen').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('Neuen QR-Link erstellen').length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires confirmation before creating a new QR link', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const confirmSpy = vi.fn().mockReturnValue(false);
|
||||||
|
window.confirm = confirmSpy;
|
||||||
|
|
||||||
|
render(<MobileQrPrintPage />);
|
||||||
|
|
||||||
|
const createButton = await screen.findByRole('button', { name: 'Neuen QR-Link erstellen' });
|
||||||
|
await user.click(createButton);
|
||||||
|
|
||||||
|
expect(confirmSpy).toHaveBeenCalled();
|
||||||
|
expect(confirmSpy).toHaveBeenCalledWith(expect.stringContaining('Ausdrucke'));
|
||||||
|
expect(createQrInvite).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe('selectRecommendedPackageId', () => {
|
|||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
it('returns null when no feature is requested', () => {
|
it('returns null when no feature is requested', () => {
|
||||||
expect(selectRecommendedPackageId(packages, null, 100)).toBeNull();
|
expect(selectRecommendedPackageId(packages, null, null)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects the cheapest upgrade with the feature', () => {
|
it('selects the cheapest upgrade with the feature', () => {
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPro
|
|||||||
{...props}
|
{...props}
|
||||||
{...({ type } as any)}
|
{...({ type } as any)}
|
||||||
secureTextEntry={isPassword}
|
secureTextEntry={isPassword}
|
||||||
onChangeText={(value) => {
|
onChangeText={(value: string) => {
|
||||||
onChange?.({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
|
onChange?.({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
|
||||||
}}
|
}}
|
||||||
size={compact ? '$3' : '$4'}
|
size={compact ? '$3' : '$4'}
|
||||||
@@ -222,7 +222,7 @@ export const MobileTextArea = React.forwardRef<
|
|||||||
ref={ref as React.Ref<any>}
|
ref={ref as React.Ref<any>}
|
||||||
{...props}
|
{...props}
|
||||||
{...({ minHeight: compact ? 72 : 96 } as any)}
|
{...({ minHeight: compact ? 72 : 96 } as any)}
|
||||||
onChangeText={(value) => {
|
onChangeText={(value: string) => {
|
||||||
onChange?.({ target: { value } } as React.ChangeEvent<HTMLTextAreaElement>);
|
onChange?.({ target: { value } } as React.ChangeEvent<HTMLTextAreaElement>);
|
||||||
}}
|
}}
|
||||||
size={compact ? '$3' : '$4'}
|
size={compact ? '$3' : '$4'}
|
||||||
@@ -292,7 +292,7 @@ export function MobileSelect({
|
|||||||
<Select
|
<Select
|
||||||
value={selectValue}
|
value={selectValue}
|
||||||
defaultValue={selectValue === undefined ? selectDefault : undefined}
|
defaultValue={selectValue === undefined ? selectDefault : undefined}
|
||||||
onValueChange={(next) => {
|
onValueChange={(next: string) => {
|
||||||
props.onChange?.({ target: { value: next } } as React.ChangeEvent<HTMLSelectElement>);
|
props.onChange?.({ target: { value: next } } as React.ChangeEvent<HTMLSelectElement>);
|
||||||
}}
|
}}
|
||||||
size={compact ? '$3' : '$4'}
|
size={compact ? '$3' : '$4'}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function LegalConsentSheet({
|
|||||||
id="legal-terms"
|
id="legal-terms"
|
||||||
size="$4"
|
size="$4"
|
||||||
checked={acceptedTerms}
|
checked={acceptedTerms}
|
||||||
onCheckedChange={(checked) => setAcceptedTerms(Boolean(checked))}
|
onCheckedChange={(checked: boolean) => setAcceptedTerms(Boolean(checked))}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={border}
|
borderColor={border}
|
||||||
backgroundColor={surface}
|
backgroundColor={surface}
|
||||||
@@ -135,7 +135,7 @@ export function LegalConsentSheet({
|
|||||||
id="legal-waiver"
|
id="legal-waiver"
|
||||||
size="$4"
|
size="$4"
|
||||||
checked={acceptedWaiver}
|
checked={acceptedWaiver}
|
||||||
onCheckedChange={(checked) => setAcceptedWaiver(Boolean(checked))}
|
onCheckedChange={(checked: boolean) => setAcceptedWaiver(Boolean(checked))}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={border}
|
borderColor={border}
|
||||||
backgroundColor={surface}
|
backgroundColor={surface}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { TenantEvent, getEvents } from '../../api';
|
|||||||
import { setTabHistory } from '../lib/tabHistory';
|
import { setTabHistory } from '../lib/tabHistory';
|
||||||
import { loadPhotoQueue } from '../lib/photoModerationQueue';
|
import { loadPhotoQueue } from '../lib/photoModerationQueue';
|
||||||
import { countQueuedPhotoActions } from '../lib/queueStatus';
|
import { countQueuedPhotoActions } from '../lib/queueStatus';
|
||||||
|
import { useDocumentTitle } from '../hooks/useDocumentTitle';
|
||||||
import { useAdminTheme } from '../theme';
|
import { useAdminTheme } from '../theme';
|
||||||
import { useAuth } from '../../auth/context';
|
import { useAuth } from '../../auth/context';
|
||||||
import { EventSwitcherSheet } from './EventSwitcherSheet';
|
import { EventSwitcherSheet } from './EventSwitcherSheet';
|
||||||
@@ -25,13 +26,14 @@ import { UserMenuSheet } from './UserMenuSheet';
|
|||||||
|
|
||||||
type MobileShellProps = {
|
type MobileShellProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
activeTab: NavKey;
|
activeTab: NavKey;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
headerActions?: React.ReactNode;
|
headerActions?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobileShell({ title, children, activeTab, onBack, headerActions }: MobileShellProps) {
|
export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) {
|
||||||
const { events, activeEvent, selectEvent } = useEventContext();
|
const { events, activeEvent, selectEvent } = useEventContext();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { go } = useMobileNav(activeEvent?.slug, activeTab);
|
const { go } = useMobileNav(activeEvent?.slug, activeTab);
|
||||||
@@ -41,6 +43,8 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions
|
|||||||
const { count: notificationCount } = useNotificationsBadge();
|
const { count: notificationCount } = useNotificationsBadge();
|
||||||
const online = useOnlineStatus();
|
const online = useOnlineStatus();
|
||||||
|
|
||||||
|
useDocumentTitle(title);
|
||||||
|
|
||||||
const theme = useAdminTheme();
|
const theme = useAdminTheme();
|
||||||
|
|
||||||
const backgroundColor = theme.background;
|
const backgroundColor = theme.background;
|
||||||
@@ -360,6 +364,18 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions
|
|||||||
) : null}
|
) : null}
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
) : null}
|
) : null}
|
||||||
|
{subtitle ? (
|
||||||
|
<YStack space="$1">
|
||||||
|
{title ? (
|
||||||
|
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text fontSize="$sm" color={theme.muted}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { SizableText as Text } from '@tamagui/text';
|
|||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAdminTheme } from '../theme';
|
import { useAdminTheme } from '../theme';
|
||||||
|
import { useDocumentTitle } from '../hooks/useDocumentTitle';
|
||||||
|
|
||||||
type OnboardingShellProps = {
|
type OnboardingShellProps = {
|
||||||
eyebrow?: string;
|
eyebrow?: string;
|
||||||
@@ -34,6 +35,8 @@ export function OnboardingShell({
|
|||||||
const resolvedBackLabel = backLabel ?? t('layout.back', 'Back');
|
const resolvedBackLabel = backLabel ?? t('layout.back', 'Back');
|
||||||
const resolvedSkipLabel = skipLabel ?? t('layout.skip', 'Skip');
|
const resolvedSkipLabel = skipLabel ?? t('layout.skip', 'Skip');
|
||||||
|
|
||||||
|
useDocumentTitle(title);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack minHeight="100vh" backgroundColor={background} alignItems="center">
|
<YStack minHeight="100vh" backgroundColor={background} alignItems="center">
|
||||||
<YStack
|
<YStack
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function CTAButton({
|
|||||||
iconRight,
|
iconRight,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
onPress: () => void;
|
onPress?: () => void;
|
||||||
tone?: 'primary' | 'ghost' | 'danger';
|
tone?: 'primary' | 'ghost' | 'danger';
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -110,7 +110,7 @@ export function CTAButton({
|
|||||||
const { primary, surface, border, text, danger, glassSurfaceStrong } = useAdminTheme();
|
const { primary, surface, border, text, danger, glassSurfaceStrong } = useAdminTheme();
|
||||||
const isPrimary = tone === 'primary';
|
const isPrimary = tone === 'primary';
|
||||||
const isDanger = tone === 'danger';
|
const isDanger = tone === 'danger';
|
||||||
const isDisabled = disabled || loading;
|
const isDisabled = disabled || loading || !onPress;
|
||||||
const backgroundColor = isDanger ? danger : isPrimary ? primary : glassSurfaceStrong ?? surface;
|
const backgroundColor = isDanger ? danger : isPrimary ? primary : glassSurfaceStrong ?? surface;
|
||||||
const borderColor = isPrimary || isDanger ? 'transparent' : border;
|
const borderColor = isPrimary || isDanger ? 'transparent' : border;
|
||||||
const labelColor = isPrimary || isDanger ? 'white' : text;
|
const labelColor = isPrimary || isDanger ? 'white' : text;
|
||||||
|
|||||||
@@ -66,8 +66,7 @@ export function SetupChecklist({
|
|||||||
height={6}
|
height={6}
|
||||||
>
|
>
|
||||||
<Progress.Indicator
|
<Progress.Indicator
|
||||||
backgroundColor={isAllComplete ? theme.success : theme.primary}
|
backgroundColor={isAllComplete ? theme.successText : theme.primary}
|
||||||
animation="bouncy"
|
|
||||||
/>
|
/>
|
||||||
</Progress>
|
</Progress>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { MobileSelect } from './FormControls';
|
|||||||
type UserMenuSheetProps = {
|
type UserMenuSheetProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
user?: { name?: string | null; email?: string | null; avatar_url?: string | null };
|
user?: { name?: string | null; email?: string | null; avatar_url?: string | null } | null;
|
||||||
isMember: boolean;
|
isMember: boolean;
|
||||||
navigate: (path: string) => void;
|
navigate: (path: string) => void;
|
||||||
};
|
};
|
||||||
@@ -245,7 +245,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
|||||||
<Switch
|
<Switch
|
||||||
size="$2"
|
size="$2"
|
||||||
checked={isDark}
|
checked={isDark}
|
||||||
onCheckedChange={(next) => updateAppearance(next ? 'dark' : 'light')}
|
onCheckedChange={(next: boolean) => updateAppearance(next ? 'dark' : 'light')}
|
||||||
aria-label={t('mobileProfile.theme', 'Dark Mode')}
|
aria-label={t('mobileProfile.theme', 'Dark Mode')}
|
||||||
>
|
>
|
||||||
<Switch.Thumb />
|
<Switch.Thumb />
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen } from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import type { EventContextValue } from '../../../context/EventContext';
|
||||||
|
import type { TenantEvent } from '../../../api';
|
||||||
|
|
||||||
vi.mock('react-i18next', () => ({
|
vi.mock('react-i18next', () => ({
|
||||||
useTranslation: () => ({ t: (key: string, fallback?: string) => fallback ?? key, i18n: { language: 'en-GB' } }),
|
useTranslation: () => ({ t: (key: string, fallback?: string) => fallback ?? key, i18n: { language: 'en-GB' } }),
|
||||||
@@ -55,12 +57,27 @@ vi.mock('../UserMenuSheet', () => ({
|
|||||||
UserMenuSheet: () => <div data-testid="user-menu-sheet" />,
|
UserMenuSheet: () => <div data-testid="user-menu-sheet" />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const eventContext = {
|
const baseEvent: TenantEvent = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Test Event',
|
||||||
|
slug: 'event-1',
|
||||||
|
event_date: '2024-01-01',
|
||||||
|
event_type_id: null,
|
||||||
|
event_type: null,
|
||||||
|
status: 'published',
|
||||||
|
settings: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventContext: EventContextValue = {
|
||||||
events: [],
|
events: [],
|
||||||
activeEvent: { slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} },
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
activeEvent: baseEvent,
|
||||||
|
activeSlug: baseEvent.slug,
|
||||||
hasMultipleEvents: false,
|
hasMultipleEvents: false,
|
||||||
hasEvents: true,
|
hasEvents: true,
|
||||||
selectEvent: vi.fn(),
|
selectEvent: vi.fn(),
|
||||||
|
refetch: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock('../../../context/EventContext', () => ({
|
vi.mock('../../../context/EventContext', () => ({
|
||||||
@@ -129,9 +146,25 @@ describe('MobileShell', () => {
|
|||||||
addEventListener: vi.fn(),
|
addEventListener: vi.fn(),
|
||||||
removeEventListener: vi.fn(),
|
removeEventListener: vi.fn(),
|
||||||
});
|
});
|
||||||
|
document.title = '';
|
||||||
eventContext.events = [];
|
eventContext.events = [];
|
||||||
eventContext.hasMultipleEvents = false;
|
eventContext.hasMultipleEvents = false;
|
||||||
eventContext.activeEvent = { slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} };
|
eventContext.activeEvent = { ...baseEvent };
|
||||||
|
eventContext.activeSlug = baseEvent.slug;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the document title with the app prefix', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MobileShell activeTab="home" title="Dashboard">
|
||||||
|
<div>Body</div>
|
||||||
|
</MobileShell>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.title).toBe('Fotospiel.App Event Admin · Dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders quick QR as icon-only button', async () => {
|
it('renders quick QR as icon-only button', async () => {
|
||||||
@@ -171,8 +204,14 @@ describe('MobileShell', () => {
|
|||||||
|
|
||||||
it('shows the event switcher when multiple events are available', async () => {
|
it('shows the event switcher when multiple events are available', async () => {
|
||||||
eventContext.events = [
|
eventContext.events = [
|
||||||
{ slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} },
|
{ ...baseEvent },
|
||||||
{ slug: 'event-2', name: 'Second Event', event_date: '2024-02-01', status: 'active', settings: {} },
|
{
|
||||||
|
...baseEvent,
|
||||||
|
id: 2,
|
||||||
|
slug: 'event-2',
|
||||||
|
name: 'Second Event',
|
||||||
|
event_date: '2024-02-01',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -190,8 +229,14 @@ describe('MobileShell', () => {
|
|||||||
|
|
||||||
it('hides the event switcher on the events list page', async () => {
|
it('hides the event switcher on the events list page', async () => {
|
||||||
eventContext.events = [
|
eventContext.events = [
|
||||||
{ slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} },
|
{ ...baseEvent },
|
||||||
{ slug: 'event-2', name: 'Second Event', event_date: '2024-02-01', status: 'active', settings: {} },
|
{
|
||||||
|
...baseEvent,
|
||||||
|
id: 2,
|
||||||
|
slug: 'event-2',
|
||||||
|
name: 'Second Event',
|
||||||
|
event_date: '2024-02-01',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
19
resources/js/admin/mobile/hooks/useDocumentTitle.ts
Normal file
19
resources/js/admin/mobile/hooks/useDocumentTitle.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const TITLE_SEPARATOR = ' · ';
|
||||||
|
|
||||||
|
export function useDocumentTitle(title?: string | null) {
|
||||||
|
const { t, i18n } = useTranslation('mobile');
|
||||||
|
const language = i18n?.language;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseTitle = t('header.documentTitle', 'Fotospiel.App Event Admin');
|
||||||
|
const resolvedTitle = typeof title === 'string' ? title.trim() : '';
|
||||||
|
document.title = resolvedTitle ? `${baseTitle}${TITLE_SEPARATOR}${resolvedTitle}` : baseTitle;
|
||||||
|
}, [language, t, title]);
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ export function classifyPackageChange(pkg: Package, active: Package | null): Pac
|
|||||||
const hasFeatureUpgrade = Array.from(candidateFeatures).some((feature) => !featureSatisfied(feature, activeFeatures));
|
const hasFeatureUpgrade = Array.from(candidateFeatures).some((feature) => !featureSatisfied(feature, activeFeatures));
|
||||||
const hasFeatureDowngrade = Array.from(activeFeatures).some((feature) => !featureSatisfied(feature, candidateFeatures));
|
const hasFeatureDowngrade = Array.from(activeFeatures).some((feature) => !featureSatisfied(feature, candidateFeatures));
|
||||||
|
|
||||||
const limitKeys: Array<keyof Package> = ['max_photos', 'max_guests', 'gallery_days'];
|
const limitKeys: Array<'max_photos' | 'max_guests' | 'gallery_days'> = ['max_photos', 'max_guests', 'gallery_days'];
|
||||||
let hasLimitUpgrade = false;
|
let hasLimitUpgrade = false;
|
||||||
let hasLimitDowngrade = false;
|
let hasLimitDowngrade = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Sparkles, Flame, UserRound, Camera } from 'lucide-react';
|
import { Sparkles, Flame, UserRound, Camera } from 'lucide-react';
|
||||||
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import { useTranslation } from '../i18n/useTranslation';
|
import { useTranslation } from '../i18n/useTranslation';
|
||||||
|
|
||||||
export type GalleryFilter = 'latest' | 'popular' | 'mine' | 'photobooth';
|
export type GalleryFilter = 'latest' | 'popular' | 'mine' | 'photobooth';
|
||||||
|
|
||||||
type FilterConfig = Array<{ value: GalleryFilter; labelKey: string; icon: React.ReactNode }>;
|
type FilterConfig = Array<{ value: GalleryFilter; labelKey: string; icon: LucideIcon }>;
|
||||||
|
|
||||||
const baseFilters: FilterConfig = [
|
const baseFilters: FilterConfig = [
|
||||||
{ value: 'latest', labelKey: 'galleryPage.filters.latest', icon: <Sparkles className="h-4 w-4" aria-hidden /> },
|
{ value: 'latest', labelKey: 'galleryPage.filters.latest', icon: Sparkles },
|
||||||
{ value: 'popular', labelKey: 'galleryPage.filters.popular', icon: <Flame className="h-4 w-4" aria-hidden /> },
|
{ value: 'popular', labelKey: 'galleryPage.filters.popular', icon: Flame },
|
||||||
{ value: 'mine', labelKey: 'galleryPage.filters.mine', icon: <UserRound className="h-4 w-4" aria-hidden /> },
|
{ value: 'mine', labelKey: 'galleryPage.filters.mine', icon: UserRound },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function FiltersBar({
|
export default function FiltersBar({
|
||||||
@@ -29,7 +30,7 @@ export default function FiltersBar({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const filters: FilterConfig = React.useMemo(
|
const filters: FilterConfig = React.useMemo(
|
||||||
() => (showPhotobooth
|
() => (showPhotobooth
|
||||||
? [...baseFilters, { value: 'photobooth', labelKey: 'galleryPage.filters.photobooth', icon: <Camera className="h-4 w-4" aria-hidden /> }]
|
? [...baseFilters, { value: 'photobooth', labelKey: 'galleryPage.filters.photobooth', icon: Camera }]
|
||||||
: baseFilters),
|
: baseFilters),
|
||||||
[showPhotobooth],
|
[showPhotobooth],
|
||||||
);
|
);
|
||||||
@@ -45,6 +46,7 @@ export default function FiltersBar({
|
|||||||
<div className="inline-flex items-center rounded-full border border-border/70 bg-white/80 p-1 shadow-sm backdrop-blur dark:border-white/10 dark:bg-slate-950/70">
|
<div className="inline-flex items-center rounded-full border border-border/70 bg-white/80 p-1 shadow-sm backdrop-blur dark:border-white/10 dark:bg-slate-950/70">
|
||||||
{filters.map((filter, index) => {
|
{filters.map((filter, index) => {
|
||||||
const isActive = value === filter.value;
|
const isActive = value === filter.value;
|
||||||
|
const Icon = filter.icon;
|
||||||
return (
|
return (
|
||||||
<div key={filter.value} className="flex items-center">
|
<div key={filter.value} className="flex items-center">
|
||||||
<button
|
<button
|
||||||
@@ -57,7 +59,7 @@ export default function FiltersBar({
|
|||||||
: 'text-muted-foreground hover:bg-pink-50 hover:text-pink-600 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white',
|
: 'text-muted-foreground hover:bg-pink-50 hover:text-pink-600 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{React.cloneElement(filter.icon as React.ReactElement, { className: 'h-3.5 w-3.5' })}
|
<Icon className="h-3.5 w-3.5" aria-hidden />
|
||||||
<span className="whitespace-nowrap">{t(filter.labelKey)}</span>
|
<span className="whitespace-nowrap">{t(filter.labelKey)}</span>
|
||||||
</button>
|
</button>
|
||||||
{index < filters.length - 1 && (
|
{index < filters.length - 1 && (
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default function TaskPickerPage() {
|
|||||||
|
|
||||||
if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.');
|
if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.');
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
const taskList = Array.isArray(payload)
|
const taskList: Task[] = Array.isArray(payload)
|
||||||
? payload
|
? payload
|
||||||
: Array.isArray(payload?.data)
|
: Array.isArray(payload?.data)
|
||||||
? payload.data
|
? payload.data
|
||||||
|
|||||||
70
resources/js/types/shims.d.ts
vendored
70
resources/js/types/shims.d.ts
vendored
@@ -15,15 +15,85 @@ declare module '@tamagui/button' {
|
|||||||
export { Button };
|
export { Button };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/card' {
|
||||||
|
export const Card: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/group' {
|
||||||
|
export const XGroup: any;
|
||||||
|
export const YGroup: any;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@tamagui/list-item' {
|
declare module '@tamagui/list-item' {
|
||||||
export const ListItem: any;
|
export const ListItem: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/checkbox' {
|
||||||
|
export const Checkbox: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/switch' {
|
||||||
|
export const Switch: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/accordion' {
|
||||||
|
export const Accordion: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/scroll-view' {
|
||||||
|
export const ScrollView: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/toggle-group' {
|
||||||
|
export const ToggleGroup: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/alert-dialog' {
|
||||||
|
export const AlertDialog: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/select' {
|
||||||
|
export const Select: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/sheet' {
|
||||||
|
export const Sheet: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/avatar' {
|
||||||
|
export const Avatar: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/radio-group' {
|
||||||
|
export const RadioGroup: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/progress' {
|
||||||
|
export const Progress: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/slider' {
|
||||||
|
export const Slider: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tamagui/image' {
|
||||||
|
export const Image: any;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@tamagui/react-native-web-lite' {
|
declare module '@tamagui/react-native-web-lite' {
|
||||||
export const Pressable: any;
|
export const Pressable: any;
|
||||||
export * from 'react-native';
|
export * from 'react-native';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'tamagui' {
|
||||||
|
export const Tabs: any;
|
||||||
|
export const Separator: any;
|
||||||
|
export const Slider: any;
|
||||||
|
export const Input: any;
|
||||||
|
export const TextArea: any;
|
||||||
|
export const Spinner: any;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@/actions/*' {
|
declare module '@/actions/*' {
|
||||||
const mod: any;
|
const mod: any;
|
||||||
export = mod;
|
export = mod;
|
||||||
|
|||||||
Reference in New Issue
Block a user