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

@@ -1,13 +1,14 @@
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Download, Loader2, Lock, Mail, Share2 } from 'lucide-react';
import { Loader2, Lock, Mail } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { adminPath, ADMIN_DEFAULT_AFTER_LOGIN_PATH, ADMIN_EVENTS_PATH } from '../constants';
import { useAuth } from '../auth/context';
import { resolveReturnTarget } from '../lib/returnTo';
import { useInstallPrompt } from './hooks/useInstallPrompt';
import { resolveInstallBannerState } from './lib/installBanner';
import { getInstallBannerDismissed, setInstallBannerDismissed, shouldShowInstallBanner } from './lib/installBanner';
import { MobileInstallBanner } from './components/MobileInstallBanner';
type LoginResponse = {
token: string;
@@ -45,7 +46,6 @@ async function performLogin(payload: { login: string; password: string; return_t
export default function MobileLoginPage() {
const { status, applyToken, abilities } = useAuth();
const { t } = useTranslation('auth');
const { t: tc } = useTranslation('common');
const location = useLocation();
const navigate = useNavigate();
const installPrompt = useInstallPrompt();
@@ -80,12 +80,16 @@ export default function MobileLoginPage() {
const [login, setLogin] = React.useState('');
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState<string | null>(null);
const installBanner = resolveInstallBannerState({
isInstalled: installPrompt.isInstalled,
isStandalone: installPrompt.isStandalone,
canInstall: installPrompt.canInstall,
isIos: installPrompt.isIos,
});
const [installBannerDismissed, setInstallBannerDismissedState] = React.useState(() => getInstallBannerDismissed());
const installBanner = shouldShowInstallBanner(
{
isInstalled: installPrompt.isInstalled,
isStandalone: installPrompt.isStandalone,
canInstall: installPrompt.canInstall,
isIos: installPrompt.isIos,
},
installBannerDismissed,
);
const mutation = useMutation({
mutationKey: ['tenantAdminLoginMobile'],
@@ -189,36 +193,15 @@ export default function MobileLoginPage() {
</button>
</form>
{installBanner ? (
<div className="rounded-2xl border border-white/10 bg-white/5 px-4 py-3">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/10">
{installBanner.variant === 'prompt' ? (
<Download className="h-5 w-5 text-white/80" />
) : (
<Share2 className="h-5 w-5 text-white/80" />
)}
</div>
<div className="flex-1 space-y-1">
<p className="text-sm font-semibold text-white">{tc('installBanner.title', 'Install Fotospiel Admin')}</p>
<p className="text-xs text-white/70">
{installBanner.variant === 'prompt'
? tc('installBanner.body', 'Add the app to your home screen for faster access and offline support.')
: tc('installBanner.iosHint', 'On iOS: Share → Add to Home Screen.')}
</p>
</div>
</div>
{installBanner.variant === 'prompt' ? (
<button
type="button"
onClick={() => void installPrompt.promptInstall()}
className="mt-3 inline-flex items-center justify-center rounded-full bg-white/10 px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/20"
>
{tc('installBanner.action', 'Install')}
</button>
) : null}
</div>
) : null}
<MobileInstallBanner
state={installBanner}
density="compact"
onInstall={installPrompt.canInstall ? () => void installPrompt.promptInstall() : undefined}
onDismiss={() => {
setInstallBannerDismissed(true);
setInstallBannerDismissedState(true);
}}
/>
<div className="text-center text-xs text-white/60">
{t('login.support', 'Fragen? Schreib uns an support@fotospiel.de oder antworte direkt auf deine Einladung.')}