onboarding tracking is now wired, the tour can be replayed from Settings, install‑banner reset is included, and empty states in Tasks/Members/Guest Messages now have guided CTAs.

What changed:
  - Onboarding tracking: admin_app_opened on first authenticated dashboard load; event_created, branding_configured,
    and invite_created on their respective actions.
  - Tour replay: Settings now has an “Experience” section to replay the tour (clears tour seen flag and opens via ?tour=1).
  - Empty states: Tasks, Members, and Guest Messages now include richer copy + quick actions.
  - New helpers + copy: Tour storage helpers, new translations, and related UI wiring.
This commit is contained in:
Codex Agent
2025-12-28 18:59:12 +01:00
parent d5f038d098
commit 718c129a8d
16 changed files with 454 additions and 91 deletions

View File

@@ -8,7 +8,7 @@ import { Pressable } from '@tamagui/react-native-web-lite';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
import { MobileField, MobileInput, MobileSelect } from './components/FormControls';
import { EventMember, getEventMembers, inviteEventMember, removeEventMember } from '../api';
import { EventMember, getEventMembers, inviteEventMember, removeEventMember, trackOnboarding } from '../api';
import { isAuthError } from '../auth/tokens';
import { getApiErrorMessage } from '../lib/apiError';
import toast from 'react-hot-toast';
@@ -26,6 +26,7 @@ export default function MobileEventMembersPage() {
const [invite, setInvite] = React.useState({ name: '', email: '', role: 'member' as EventMember['role'] });
const [saving, setSaving] = React.useState(false);
const [inviteLink, setInviteLink] = React.useState<string | null>(null);
const emailInputRef = React.useRef<HTMLInputElement | null>(null);
const [search, setSearch] = React.useState('');
const [confirmRemove, setConfirmRemove] = React.useState<EventMember | null>(null);
@@ -67,6 +68,7 @@ export default function MobileEventMembersPage() {
});
setMembers((prev) => [member, ...prev]);
setInvite({ name: '', email: '', role: 'member' });
void trackOnboarding('invite_created', { event_slug: slug });
toast.success(t('events.members.inviteSuccess', 'Einladung gesendet'));
} catch (err) {
if (!isAuthError(err)) {
@@ -130,6 +132,7 @@ export default function MobileEventMembersPage() {
value={invite.email}
onChange={(e) => setInvite((prev) => ({ ...prev, email: e.target.value }))}
placeholder="alex@example.com"
ref={emailInputRef}
/>
</MobileField>
<MobileField label={t('events.members.role', 'Role')}>
@@ -188,9 +191,19 @@ export default function MobileEventMembersPage() {
))}
</YStack>
) : members.length === 0 ? (
<Text fontSize="$sm" color="#4b5563">
{t('events.members.empty', 'Noch keine Einladungen.')}
</Text>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
{t('events.members.emptyTitle', 'Invite your team')}
</Text>
<Text fontSize="$xs" color="#4b5563">
{t('events.members.emptyBody', 'Send the first invite so helpers can access the event.')}
</Text>
<CTAButton
label={t('events.members.emptyAction', 'Send first invite')}
onPress={() => emailInputRef.current?.focus()}
fullWidth={false}
/>
</YStack>
) : (
<YStack space="$2">
{members