Fix TypeScript typecheck errors
This commit is contained in:
@@ -16,9 +16,10 @@ export interface TenantProfile {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
role?: string | null;
|
||||
name?: string;
|
||||
name?: string | null;
|
||||
slug?: string;
|
||||
email?: string | null;
|
||||
avatar_url?: string | null;
|
||||
features?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Event-Admin",
|
||||
"pageTitle": "Login",
|
||||
"badge": "Fotospiel Event Admin",
|
||||
"hero_tagline": "Kontrolle behalten, entspannt bleiben",
|
||||
"hero_title": "Das Cockpit für dein Fotospiel Event",
|
||||
@@ -90,6 +91,9 @@
|
||||
"appearance_label": "Darstellung"
|
||||
},
|
||||
"redirecting": "Weiterleitung zum Login …",
|
||||
"logout": {
|
||||
"title": "Abmeldung wird vorbereitet …"
|
||||
},
|
||||
"processing": {
|
||||
"title": "Anmeldung wird verarbeitet …",
|
||||
"copy": "Einen Moment bitte, wir bereiten dein Dashboard vor."
|
||||
|
||||
@@ -983,6 +983,7 @@
|
||||
"layouts": "Druck-Layouts",
|
||||
"preview": "Anpassen & Exportieren",
|
||||
"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",
|
||||
"created": "Neuer QR-Link erstellt",
|
||||
"createFailed": "Link konnte nicht erstellt werden.",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"header": {
|
||||
"appName": "Event Admin",
|
||||
"documentTitle": "Fotospiel.App Event Admin",
|
||||
"selectEvent": "Wähle ein Event, um fortzufahren",
|
||||
"empty": "Lege dein erstes Event an, um zu starten",
|
||||
"eventSwitcher": "Event auswählen",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Event Admin",
|
||||
"pageTitle": "Login",
|
||||
"badge": "Fotospiel Event Admin",
|
||||
"hero_tagline": "Stay in control, stay relaxed",
|
||||
"hero_title": "Your cockpit for every Fotospiel event",
|
||||
@@ -90,6 +91,9 @@
|
||||
"appearance_label": "Appearance"
|
||||
},
|
||||
"redirecting": "Redirecting to login …",
|
||||
"logout": {
|
||||
"title": "Signing out …"
|
||||
},
|
||||
"processing": {
|
||||
"title": "Signing you in …",
|
||||
"copy": "One moment please while we prepare your dashboard."
|
||||
|
||||
@@ -979,6 +979,7 @@
|
||||
"layouts": "Print Layouts",
|
||||
"preview": "Customize & Export",
|
||||
"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",
|
||||
"created": "New QR link created",
|
||||
"createFailed": "Could not create link.",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"header": {
|
||||
"appName": "Event Admin",
|
||||
"documentTitle": "Fotospiel.App Event Admin",
|
||||
"selectEvent": "Select an event to continue",
|
||||
"empty": "Create your first event to get started",
|
||||
"eventSwitcher": "Choose an event",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAuth } from '../auth/context';
|
||||
import { ADMIN_DEFAULT_AFTER_LOGIN_PATH } from '../constants';
|
||||
import { decodeReturnTo, resolveReturnTarget } from '../lib/returnTo';
|
||||
import { useAdminTheme } from './theme';
|
||||
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||
|
||||
export default function AuthCallbackPage(): React.ReactElement {
|
||||
const { status } = useAuth();
|
||||
@@ -21,6 +22,8 @@ export default function AuthCallbackPage(): React.ReactElement {
|
||||
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 rawReturnTo = searchParams.get('return_to');
|
||||
|
||||
|
||||
@@ -1324,7 +1324,7 @@ function LabeledSlider({
|
||||
max={max}
|
||||
step={step}
|
||||
value={[value]}
|
||||
onValueChange={(next) => onChange(next[0] ?? value)}
|
||||
onValueChange={(next: number[]) => onChange(next[0] ?? value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Slider.Track height={6} borderRadius={999} backgroundColor={border}>
|
||||
|
||||
@@ -108,12 +108,13 @@ function SectionHeader({
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
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') },
|
||||
draft: { tone: 'warning', label: t('events.status.draft', 'Draft') },
|
||||
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>;
|
||||
}
|
||||
@@ -408,7 +409,7 @@ function LifecycleHero({
|
||||
<DashboardCard variant={cardVariant} padding={cardPadding}>
|
||||
<YStack space={isEmbedded ? '$2.5' : '$3'}>
|
||||
<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" />
|
||||
</YStack>
|
||||
<YStack>
|
||||
@@ -486,7 +487,7 @@ function LifecycleHero({
|
||||
</Text>
|
||||
<Switch
|
||||
checked={published}
|
||||
onCheckedChange={(checked) => handlePublishChange(Boolean(checked))}
|
||||
onCheckedChange={(checked: boolean) => handlePublishChange(Boolean(checked))}
|
||||
size="$2"
|
||||
disabled={isPublishing}
|
||||
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 slug = event?.slug;
|
||||
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 },
|
||||
!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('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,
|
||||
{ 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('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 },
|
||||
!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 },
|
||||
].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item));
|
||||
].filter(Boolean) as ToolItem[];
|
||||
|
||||
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 { t } = useTranslation('management');
|
||||
|
||||
if (!photos.length) return null;
|
||||
if (!photos.length || !slug) return null;
|
||||
|
||||
return (
|
||||
<DashboardCard>
|
||||
|
||||
@@ -1110,7 +1110,7 @@ export default function MobileEventControlRoomPage() {
|
||||
size="$3"
|
||||
checked={autoApproveHighlights}
|
||||
disabled={controlRoomSaving}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
saveControlRoomSettings({
|
||||
...controlRoomSettings,
|
||||
auto_approve_highlights: Boolean(checked),
|
||||
@@ -1139,7 +1139,7 @@ export default function MobileEventControlRoomPage() {
|
||||
size="$3"
|
||||
checked={autoAddApprovedToLive}
|
||||
disabled={controlRoomSaving || isImmediateUploads}
|
||||
onCheckedChange={(checked) => {
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
if (isImmediateUploads) {
|
||||
return;
|
||||
}
|
||||
@@ -1164,7 +1164,7 @@ export default function MobileEventControlRoomPage() {
|
||||
size="$3"
|
||||
checked={autoRemoveLiveOnHide}
|
||||
disabled={controlRoomSaving}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
saveControlRoomSettings({
|
||||
...controlRoomSettings,
|
||||
auto_remove_live_on_hide: Boolean(checked),
|
||||
@@ -1388,7 +1388,7 @@ export default function MobileEventControlRoomPage() {
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={moderationFilter}
|
||||
onValueChange={(value) => value && setModerationFilter(value as ModerationFilter)}
|
||||
onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)}
|
||||
>
|
||||
<XStack space="$1.5">
|
||||
{MODERATION_FILTERS.map((option) => {
|
||||
@@ -1576,7 +1576,7 @@ export default function MobileEventControlRoomPage() {
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={liveStatusFilter}
|
||||
onValueChange={(value) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
|
||||
onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
|
||||
>
|
||||
<XStack space="$1.5">
|
||||
{LIVE_STATUS_OPTIONS.map((option) => {
|
||||
|
||||
@@ -527,7 +527,7 @@ export default function MobileEventFormPage() {
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Switch
|
||||
checked={form.published}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setForm((prev) => ({ ...prev, published: Boolean(checked) }))
|
||||
}
|
||||
size="$3"
|
||||
@@ -546,7 +546,7 @@ export default function MobileEventFormPage() {
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Switch
|
||||
checked={form.tasksEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setForm((prev) => ({ ...prev, tasksEnabled: Boolean(checked) }))
|
||||
}
|
||||
size="$3"
|
||||
@@ -577,7 +577,7 @@ export default function MobileEventFormPage() {
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Switch
|
||||
checked={form.autoApproveUploads}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setForm((prev) => ({ ...prev, autoApproveUploads: Boolean(checked) }))
|
||||
}
|
||||
size="$3"
|
||||
|
||||
@@ -677,7 +677,7 @@ function EffectSlider({
|
||||
max={max}
|
||||
step={step}
|
||||
value={[value]}
|
||||
onValueChange={(next) => onChange(next[0] ?? value)}
|
||||
onValueChange={(next: number[]) => onChange(next[0] ?? value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Slider.Track height={6} borderRadius={999} backgroundColor={border}>
|
||||
|
||||
@@ -1158,7 +1158,7 @@ export default function MobileEventTasksPage() {
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as TaskSectionKey)}
|
||||
onValueChange={(value: string) => setActiveTab(value as TaskSectionKey)}
|
||||
flexDirection="column"
|
||||
alignItems="stretch"
|
||||
width="100%"
|
||||
@@ -1411,7 +1411,7 @@ export default function MobileEventTasksPage() {
|
||||
>
|
||||
<RadioGroup
|
||||
value={emotionFilter}
|
||||
onValueChange={(val) => {
|
||||
onValueChange={(val: string) => {
|
||||
setEmotionFilter(val);
|
||||
setShowEmotionFilterSheet(false);
|
||||
}}
|
||||
@@ -1441,7 +1441,7 @@ export default function MobileEventTasksPage() {
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(deleteCandidate)}
|
||||
onOpenChange={(open) => {
|
||||
onOpenChange={(open: boolean) => {
|
||||
if (!open) {
|
||||
setDeleteCandidate(null);
|
||||
}
|
||||
@@ -1496,7 +1496,7 @@ export default function MobileEventTasksPage() {
|
||||
|
||||
<AlertDialog
|
||||
open={bulkDeleteOpen}
|
||||
onOpenChange={(open) => {
|
||||
onOpenChange={(open: boolean) => {
|
||||
if (!open) {
|
||||
setBulkDeleteOpen(false);
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ function EventsList({
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={statusFilter}
|
||||
onValueChange={(value) => value && onStatusChange(value as EventStatusKey)}
|
||||
onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)}
|
||||
>
|
||||
<XStack space="$1.5">
|
||||
{filters.map((filter) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ADMIN_LOGIN_PATH } from '../constants';
|
||||
import { MobileCard } from './components/Primitives';
|
||||
import { MobileField, MobileInput } from './components/FormControls';
|
||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||
|
||||
type ResetResponse = {
|
||||
status: string;
|
||||
@@ -51,6 +52,8 @@ export default function ForgotPasswordPage() {
|
||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||
};
|
||||
|
||||
useDocumentTitle(t('login.forgot.title', 'Forgot your password?'));
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['tenantAdminForgotPassword'],
|
||||
mutationFn: requestPasswordReset,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Button } from '@tamagui/button';
|
||||
import { MobileCard } from './components/Primitives';
|
||||
import { MobileField, MobileInput } from './components/FormControls';
|
||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||
|
||||
type LoginResponse = {
|
||||
token: string;
|
||||
@@ -66,6 +67,8 @@ export default function MobileLoginPage() {
|
||||
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 rawReturnTo = searchParams.get('return_to');
|
||||
const oauthError = searchParams.get('error');
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SizableText as Text } from '@tamagui/text';
|
||||
import { Spinner } from 'tamagui';
|
||||
import { ADMIN_LOGIN_PATH } from '../constants';
|
||||
import { useAdminTheme } from './theme';
|
||||
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||
|
||||
export default function LoginStartPage(): React.ReactElement {
|
||||
const location = useLocation();
|
||||
@@ -19,6 +20,8 @@ export default function LoginStartPage(): React.ReactElement {
|
||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||
};
|
||||
|
||||
useDocumentTitle(t('redirecting', 'Redirecting to login …'));
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const returnTo = params.get('return_to');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from '@tamagui/card';
|
||||
import { YStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
@@ -6,15 +7,19 @@ import { Spinner } from 'tamagui';
|
||||
import { useAuth } from '../auth/context';
|
||||
import { ADMIN_PUBLIC_LANDING_PATH } from '../constants';
|
||||
import { useAdminTheme } from './theme';
|
||||
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||
|
||||
export default function LogoutPage() {
|
||||
const { logout } = useAuth();
|
||||
const { t } = useTranslation('auth');
|
||||
const { textStrong, muted, border, surface, shadow, appBackground } = useAdminTheme();
|
||||
const safeAreaStyle: React.CSSProperties = {
|
||||
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)',
|
||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||
};
|
||||
|
||||
useDocumentTitle(t('logout.title', 'Signing out …'));
|
||||
|
||||
React.useEffect(() => {
|
||||
logout({ redirect: ADMIN_PUBLIC_LANDING_PATH });
|
||||
}, [logout]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { Check, ChevronRight, ShieldCheck, Sparkles, X } from 'lucide-react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
@@ -395,7 +396,11 @@ function PackageShopCompareView({
|
||||
}
|
||||
}
|
||||
|
||||
if (row.type === 'feature') {
|
||||
return t(`shop.features.${row.featureKey}`, row.featureKey);
|
||||
}
|
||||
|
||||
return row.id;
|
||||
};
|
||||
|
||||
const formatLimitValue = (value: number | null) => {
|
||||
@@ -562,7 +567,7 @@ function getPackageStatusLabel({
|
||||
isActive,
|
||||
owned,
|
||||
}: {
|
||||
t: (key: string, fallback?: string, options?: Record<string, unknown>) => string;
|
||||
t: TFunction;
|
||||
isActive?: boolean;
|
||||
owned?: TenantPackageSummary;
|
||||
}): string | null {
|
||||
@@ -622,7 +627,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
||||
id="agb"
|
||||
size="$4"
|
||||
checked={agbAccepted}
|
||||
onCheckedChange={(checked) => setAgbAccepted(!!checked)}
|
||||
onCheckedChange={(checked: boolean) => setAgbAccepted(!!checked)}
|
||||
>
|
||||
<Checkbox.Indicator>
|
||||
<Check />
|
||||
@@ -638,7 +643,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
|
||||
id="withdrawal"
|
||||
size="$4"
|
||||
checked={withdrawalAccepted}
|
||||
onCheckedChange={(checked) => setWithdrawalAccepted(!!checked)}
|
||||
onCheckedChange={(checked: boolean) => setWithdrawalAccepted(!!checked)}
|
||||
>
|
||||
<Checkbox.Indicator>
|
||||
<Check />
|
||||
@@ -687,7 +692,7 @@ function aggregateOwnedEntries(entries: TenantPackageSummary[]): TenantPackageSu
|
||||
}
|
||||
|
||||
function resolveIncludedTierLabel(
|
||||
t: (key: string, fallback?: string, options?: Record<string, unknown>) => string,
|
||||
t: TFunction,
|
||||
slug: string | null
|
||||
): string | null {
|
||||
if (!slug) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from '@tamagui/button';
|
||||
import { ADMIN_LOGIN_PATH } from '../constants';
|
||||
import { MobileCard, CTAButton } from './components/Primitives';
|
||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||
|
||||
type FaqItem = {
|
||||
question: string;
|
||||
@@ -44,6 +45,8 @@ export default function PublicHelpPage() {
|
||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||
};
|
||||
|
||||
useDocumentTitle(t('login.help_title', 'Help for event admins'));
|
||||
|
||||
return (
|
||||
<YStack
|
||||
minHeight="100vh"
|
||||
|
||||
@@ -234,6 +234,16 @@ export default function MobileQrPrintPage() {
|
||||
label={t('events.qr.createLink', 'Neuen QR-Link erstellen')}
|
||||
onPress={async () => {
|
||||
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 {
|
||||
const invite = await createQrInvite(slug, { label: t('events.qr.mobileLinkLabel', 'Mobile link') });
|
||||
setQrUrl(invite.url);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ADMIN_LOGIN_PATH } from '../constants';
|
||||
import { MobileCard } from './components/Primitives';
|
||||
import { MobileField, MobileInput } from './components/FormControls';
|
||||
import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||
import { useDocumentTitle } from './hooks/useDocumentTitle';
|
||||
|
||||
type ResetResponse = {
|
||||
status: string;
|
||||
@@ -68,6 +69,8 @@ export default function ResetPasswordPage() {
|
||||
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)',
|
||||
};
|
||||
|
||||
useDocumentTitle(t('login.reset.title', 'Reset password'));
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['tenantAdminResetPassword'],
|
||||
mutationFn: resetPassword,
|
||||
|
||||
@@ -264,7 +264,7 @@ export default function MobileSettingsPage() {
|
||||
<Switch
|
||||
size="$4"
|
||||
checked={pushState.subscribed}
|
||||
onCheckedChange={(value) => {
|
||||
onCheckedChange={(value: boolean) => {
|
||||
if (value) {
|
||||
void pushState.enable();
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { ADMIN_EVENTS_PATH } from '../../constants';
|
||||
import type { TenantEvent } from '../../api';
|
||||
|
||||
const fixtures = vi.hoisted(() => ({
|
||||
event: {
|
||||
@@ -9,14 +10,17 @@ const fixtures = vi.hoisted(() => ({
|
||||
name: 'Demo Wedding',
|
||||
slug: 'demo-event',
|
||||
event_date: '2026-02-19',
|
||||
status: 'published' as const,
|
||||
settings: { location: 'Berlin' },
|
||||
event_type_id: null,
|
||||
event_type: null,
|
||||
status: 'published',
|
||||
engagement_mode: undefined,
|
||||
settings: { location: 'Berlin', guest_upload_visibility: 'immediate' },
|
||||
tasks_count: 4,
|
||||
photo_count: 12,
|
||||
active_invites_count: 3,
|
||||
total_invites_count: 5,
|
||||
member_permissions: ['photos:moderate', 'tasks:manage', 'join-tokens:manage'],
|
||||
},
|
||||
} as TenantEvent,
|
||||
activePackage: {
|
||||
id: 1,
|
||||
package_id: 1,
|
||||
|
||||
@@ -138,7 +138,9 @@ vi.mock('../components/Primitives', () => ({
|
||||
}));
|
||||
|
||||
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', () => ({
|
||||
|
||||
@@ -13,18 +13,36 @@ const fixtures = vi.hoisted(() => ({
|
||||
invites: [
|
||||
{
|
||||
id: 1,
|
||||
token: 'invite-token',
|
||||
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: [
|
||||
{
|
||||
id: 'layout-1',
|
||||
name: 'Poster Layout',
|
||||
description: 'Layout description',
|
||||
subtitle: 'Layout subtitle',
|
||||
paper: 'a4',
|
||||
orientation: 'portrait',
|
||||
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 { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const fixtures = vi.hoisted(() => ({
|
||||
event: {
|
||||
@@ -74,7 +75,11 @@ vi.mock('../components/MobileShell', () => ({
|
||||
|
||||
vi.mock('../components/Primitives', () => ({
|
||||
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>,
|
||||
}));
|
||||
|
||||
@@ -116,6 +121,7 @@ vi.mock('../theme', () => ({
|
||||
}));
|
||||
|
||||
import MobileQrPrintPage from '../QrPrintPage';
|
||||
import { createQrInvite } from '../../api';
|
||||
|
||||
describe('MobileQrPrintPage', () => {
|
||||
it('renders QR overview content', async () => {
|
||||
@@ -125,4 +131,19 @@ describe('MobileQrPrintPage', () => {
|
||||
expect(screen.getByText('Schritt 1: Format wählen')).toBeInTheDocument();
|
||||
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;
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -184,7 +184,7 @@ export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPro
|
||||
{...props}
|
||||
{...({ type } as any)}
|
||||
secureTextEntry={isPassword}
|
||||
onChangeText={(value) => {
|
||||
onChangeText={(value: string) => {
|
||||
onChange?.({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
|
||||
}}
|
||||
size={compact ? '$3' : '$4'}
|
||||
@@ -222,7 +222,7 @@ export const MobileTextArea = React.forwardRef<
|
||||
ref={ref as React.Ref<any>}
|
||||
{...props}
|
||||
{...({ minHeight: compact ? 72 : 96 } as any)}
|
||||
onChangeText={(value) => {
|
||||
onChangeText={(value: string) => {
|
||||
onChange?.({ target: { value } } as React.ChangeEvent<HTMLTextAreaElement>);
|
||||
}}
|
||||
size={compact ? '$3' : '$4'}
|
||||
@@ -292,7 +292,7 @@ export function MobileSelect({
|
||||
<Select
|
||||
value={selectValue}
|
||||
defaultValue={selectValue === undefined ? selectDefault : undefined}
|
||||
onValueChange={(next) => {
|
||||
onValueChange={(next: string) => {
|
||||
props.onChange?.({ target: { value: next } } as React.ChangeEvent<HTMLSelectElement>);
|
||||
}}
|
||||
size={compact ? '$3' : '$4'}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function LegalConsentSheet({
|
||||
id="legal-terms"
|
||||
size="$4"
|
||||
checked={acceptedTerms}
|
||||
onCheckedChange={(checked) => setAcceptedTerms(Boolean(checked))}
|
||||
onCheckedChange={(checked: boolean) => setAcceptedTerms(Boolean(checked))}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
@@ -135,7 +135,7 @@ export function LegalConsentSheet({
|
||||
id="legal-waiver"
|
||||
size="$4"
|
||||
checked={acceptedWaiver}
|
||||
onCheckedChange={(checked) => setAcceptedWaiver(Boolean(checked))}
|
||||
onCheckedChange={(checked: boolean) => setAcceptedWaiver(Boolean(checked))}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { TenantEvent, getEvents } from '../../api';
|
||||
import { setTabHistory } from '../lib/tabHistory';
|
||||
import { loadPhotoQueue } from '../lib/photoModerationQueue';
|
||||
import { countQueuedPhotoActions } from '../lib/queueStatus';
|
||||
import { useDocumentTitle } from '../hooks/useDocumentTitle';
|
||||
import { useAdminTheme } from '../theme';
|
||||
import { useAuth } from '../../auth/context';
|
||||
import { EventSwitcherSheet } from './EventSwitcherSheet';
|
||||
@@ -25,13 +26,14 @@ import { UserMenuSheet } from './UserMenuSheet';
|
||||
|
||||
type MobileShellProps = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
activeTab: NavKey;
|
||||
onBack?: () => void;
|
||||
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 { user } = useAuth();
|
||||
const { go } = useMobileNav(activeEvent?.slug, activeTab);
|
||||
@@ -41,6 +43,8 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions
|
||||
const { count: notificationCount } = useNotificationsBadge();
|
||||
const online = useOnlineStatus();
|
||||
|
||||
useDocumentTitle(title);
|
||||
|
||||
const theme = useAdminTheme();
|
||||
|
||||
const backgroundColor = theme.background;
|
||||
@@ -360,6 +364,18 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions
|
||||
) : null}
|
||||
</MobileCard>
|
||||
) : 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}
|
||||
</YStack>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAdminTheme } from '../theme';
|
||||
import { useDocumentTitle } from '../hooks/useDocumentTitle';
|
||||
|
||||
type OnboardingShellProps = {
|
||||
eyebrow?: string;
|
||||
@@ -34,6 +35,8 @@ export function OnboardingShell({
|
||||
const resolvedBackLabel = backLabel ?? t('layout.back', 'Back');
|
||||
const resolvedSkipLabel = skipLabel ?? t('layout.skip', 'Skip');
|
||||
|
||||
useDocumentTitle(title);
|
||||
|
||||
return (
|
||||
<YStack minHeight="100vh" backgroundColor={background} alignItems="center">
|
||||
<YStack
|
||||
|
||||
@@ -98,7 +98,7 @@ export function CTAButton({
|
||||
iconRight,
|
||||
}: {
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
onPress?: () => void;
|
||||
tone?: 'primary' | 'ghost' | 'danger';
|
||||
fullWidth?: boolean;
|
||||
disabled?: boolean;
|
||||
@@ -110,7 +110,7 @@ export function CTAButton({
|
||||
const { primary, surface, border, text, danger, glassSurfaceStrong } = useAdminTheme();
|
||||
const isPrimary = tone === 'primary';
|
||||
const isDanger = tone === 'danger';
|
||||
const isDisabled = disabled || loading;
|
||||
const isDisabled = disabled || loading || !onPress;
|
||||
const backgroundColor = isDanger ? danger : isPrimary ? primary : glassSurfaceStrong ?? surface;
|
||||
const borderColor = isPrimary || isDanger ? 'transparent' : border;
|
||||
const labelColor = isPrimary || isDanger ? 'white' : text;
|
||||
|
||||
@@ -66,8 +66,7 @@ export function SetupChecklist({
|
||||
height={6}
|
||||
>
|
||||
<Progress.Indicator
|
||||
backgroundColor={isAllComplete ? theme.success : theme.primary}
|
||||
animation="bouncy"
|
||||
backgroundColor={isAllComplete ? theme.successText : theme.primary}
|
||||
/>
|
||||
</Progress>
|
||||
</YStack>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { MobileSelect } from './FormControls';
|
||||
type UserMenuSheetProps = {
|
||||
open: boolean;
|
||||
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;
|
||||
navigate: (path: string) => void;
|
||||
};
|
||||
@@ -245,7 +245,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
||||
<Switch
|
||||
size="$2"
|
||||
checked={isDark}
|
||||
onCheckedChange={(next) => updateAppearance(next ? 'dark' : 'light')}
|
||||
onCheckedChange={(next: boolean) => updateAppearance(next ? 'dark' : 'light')}
|
||||
aria-label={t('mobileProfile.theme', 'Dark Mode')}
|
||||
>
|
||||
<Switch.Thumb />
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { EventContextValue } from '../../../context/EventContext';
|
||||
import type { TenantEvent } from '../../../api';
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
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" />,
|
||||
}));
|
||||
|
||||
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: [],
|
||||
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,
|
||||
hasEvents: true,
|
||||
selectEvent: vi.fn(),
|
||||
refetch: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('../../../context/EventContext', () => ({
|
||||
@@ -129,9 +146,25 @@ describe('MobileShell', () => {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
});
|
||||
document.title = '';
|
||||
eventContext.events = [];
|
||||
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 () => {
|
||||
@@ -171,8 +204,14 @@ describe('MobileShell', () => {
|
||||
|
||||
it('shows the event switcher when multiple events are available', async () => {
|
||||
eventContext.events = [
|
||||
{ slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} },
|
||||
{ slug: 'event-2', name: 'Second Event', event_date: '2024-02-01', status: 'active', settings: {} },
|
||||
{ ...baseEvent },
|
||||
{
|
||||
...baseEvent,
|
||||
id: 2,
|
||||
slug: 'event-2',
|
||||
name: 'Second Event',
|
||||
event_date: '2024-02-01',
|
||||
},
|
||||
];
|
||||
|
||||
await act(async () => {
|
||||
@@ -190,8 +229,14 @@ describe('MobileShell', () => {
|
||||
|
||||
it('hides the event switcher on the events list page', async () => {
|
||||
eventContext.events = [
|
||||
{ slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} },
|
||||
{ slug: 'event-2', name: 'Second Event', event_date: '2024-02-01', status: 'active', settings: {} },
|
||||
{ ...baseEvent },
|
||||
{
|
||||
...baseEvent,
|
||||
id: 2,
|
||||
slug: 'event-2',
|
||||
name: 'Second Event',
|
||||
event_date: '2024-02-01',
|
||||
},
|
||||
];
|
||||
|
||||
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 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 hasLimitDowngrade = false;
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Sparkles, Flame, UserRound, Camera } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { useTranslation } from '../i18n/useTranslation';
|
||||
|
||||
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 = [
|
||||
{ value: 'latest', labelKey: 'galleryPage.filters.latest', icon: <Sparkles className="h-4 w-4" aria-hidden /> },
|
||||
{ value: 'popular', labelKey: 'galleryPage.filters.popular', icon: <Flame className="h-4 w-4" aria-hidden /> },
|
||||
{ value: 'mine', labelKey: 'galleryPage.filters.mine', icon: <UserRound className="h-4 w-4" aria-hidden /> },
|
||||
{ value: 'latest', labelKey: 'galleryPage.filters.latest', icon: Sparkles },
|
||||
{ value: 'popular', labelKey: 'galleryPage.filters.popular', icon: Flame },
|
||||
{ value: 'mine', labelKey: 'galleryPage.filters.mine', icon: UserRound },
|
||||
];
|
||||
|
||||
export default function FiltersBar({
|
||||
@@ -29,7 +30,7 @@ export default function FiltersBar({
|
||||
const { t } = useTranslation();
|
||||
const filters: FilterConfig = React.useMemo(
|
||||
() => (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),
|
||||
[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">
|
||||
{filters.map((filter, index) => {
|
||||
const isActive = value === filter.value;
|
||||
const Icon = filter.icon;
|
||||
return (
|
||||
<div key={filter.value} className="flex items-center">
|
||||
<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',
|
||||
)}
|
||||
>
|
||||
{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>
|
||||
</button>
|
||||
{index < filters.length - 1 && (
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function TaskPickerPage() {
|
||||
|
||||
if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.');
|
||||
const payload = await response.json();
|
||||
const taskList = Array.isArray(payload)
|
||||
const taskList: Task[] = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray(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 };
|
||||
}
|
||||
|
||||
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' {
|
||||
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' {
|
||||
export const Pressable: any;
|
||||
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/*' {
|
||||
const mod: any;
|
||||
export = mod;
|
||||
|
||||
Reference in New Issue
Block a user