feat(client): Onboarding-Tour um Statistik und Feedback erweitern
Neue Tour-Schritte für Statistik-Dashboard und Feedback-Formular, Hinweis zum Löschen der Demo-Einträge und Landung auf Statistik nach Abschluss. Rollenauflösung bei geteilten Logbüchern fail-closed bis die Rolle bekannt ist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3401,3 +3401,11 @@ body.app-tour-active .app-tour-target-active {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.app-tour-active .disclaimer-modal-overlay.feedback-modal-overlay--tour {
|
||||||
|
z-index: 9990;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.app-tour-active .feedback-modal-overlay--tour .disclaimer-modal-panel {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|||||||
+46
-11
@@ -77,7 +77,7 @@ function App() {
|
|||||||
[activeLogbookId]
|
[activeLogbookId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [activeAccessRole, setActiveAccessRole] = useState<LogbookAccessRole>('OWNER')
|
const [activeAccessRole, setActiveAccessRole] = useState<LogbookAccessRole | null>('OWNER')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeLogbookId) {
|
if (!activeLogbookId) {
|
||||||
@@ -91,15 +91,24 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cachedRole = activeLogbookRecord.collaborationRole
|
const cachedRole = activeLogbookRecord.collaborationRole
|
||||||
|
// Fail-closed for write UI until role is known: do not assume WRITE
|
||||||
setActiveAccessRole(
|
setActiveAccessRole(
|
||||||
cachedRole
|
cachedRole ? parseCollaborationRole(cachedRole, `logbook ${activeLogbookId}`) : null
|
||||||
? parseCollaborationRole(cachedRole, `logbook ${activeLogbookId}`)
|
|
||||||
: 'WRITE'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
getLogbookAccess(activeLogbookId).then((access) => {
|
let cancelled = false
|
||||||
if (access) setActiveAccessRole(access.role)
|
getLogbookAccess(activeLogbookId)
|
||||||
|
.then((access) => {
|
||||||
|
if (cancelled || !access) return
|
||||||
|
setActiveAccessRole(access.role)
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn('Failed to resolve logbook access role:', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
}, [activeLogbookId, activeLogbookRecord])
|
}, [activeLogbookId, activeLogbookRecord])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -188,12 +197,20 @@ function App() {
|
|||||||
`${cleanUrl.pathname}${cleanUrl.search}${cleanUrl.hash}`
|
`${cleanUrl.pathname}${cleanUrl.search}${cleanUrl.hash}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
void (async () => {
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
const session = await checkServerSession()
|
const session = await checkServerSession()
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
if (session.authenticated && session.userId) {
|
if (session.authenticated && session.userId) {
|
||||||
localStorage.setItem('active_userid', session.userId)
|
localStorage.setItem('active_userid', session.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedUser = localStorage.getItem('active_username')
|
const savedUser = localStorage.getItem('active_username')
|
||||||
const key = getActiveMasterKey()
|
const key = getActiveMasterKey()
|
||||||
if (session.authenticated && savedUser && key) {
|
if (session.authenticated && savedUser && key) {
|
||||||
@@ -205,7 +222,16 @@ function App() {
|
|||||||
setActiveLogbookTitle(savedLogbookTitle)
|
setActiveLogbookTitle(savedLogbookTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.warn('Session restore failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -224,7 +250,8 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerNavigation({
|
registerNavigation({
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setSelectedEntryId: setTourSelectedEntryId
|
setSelectedEntryId: setTourSelectedEntryId,
|
||||||
|
setFeedbackOpen: setTourFeedbackOpen
|
||||||
})
|
})
|
||||||
}, [registerNavigation])
|
}, [registerNavigation])
|
||||||
|
|
||||||
@@ -391,7 +418,12 @@ function App() {
|
|||||||
|
|
||||||
const pwaInstallBanner = <PwaInstallPrompt variant="banner" />
|
const pwaInstallBanner = <PwaInstallPrompt variant="banner" />
|
||||||
|
|
||||||
const logbookReadOnly = activeAccessRole === 'READ'
|
const sharedLogbook =
|
||||||
|
activeLogbookRecord === undefined ? null : activeLogbookRecord.isShared === 1
|
||||||
|
const logbookReadOnly =
|
||||||
|
activeLogbookId != null &&
|
||||||
|
(sharedLogbook === null || sharedLogbook) &&
|
||||||
|
activeAccessRole !== 'WRITE'
|
||||||
|
|
||||||
if (!activeLogbookId) {
|
if (!activeLogbookId) {
|
||||||
return (
|
return (
|
||||||
@@ -420,12 +452,12 @@ function App() {
|
|||||||
<div className="app-title-area">
|
<div className="app-title-area">
|
||||||
<div className="app-title-row">
|
<div className="app-title-row">
|
||||||
<h2>{activeLogbookTitle}</h2>
|
<h2>{activeLogbookTitle}</h2>
|
||||||
{activeAccessRole !== 'OWNER' && (
|
{activeAccessRole && activeAccessRole !== 'OWNER' && (
|
||||||
<LogbookRoleBadge role={activeAccessRole} />
|
<LogbookRoleBadge role={activeAccessRole} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="app-subtitle">
|
<p className="app-subtitle">
|
||||||
{activeAccessRole !== 'OWNER'
|
{activeAccessRole && activeAccessRole !== 'OWNER'
|
||||||
? t('dashboard.section_shared_hint')
|
? t('dashboard.section_shared_hint')
|
||||||
: `${t('app.name')} / ${activeLogbookId?.substring(0, 8)}...`}
|
: `${t('app.name')} / ${activeLogbookId?.substring(0, 8)}...`}
|
||||||
</p>
|
</p>
|
||||||
@@ -454,6 +486,9 @@ function App() {
|
|||||||
<FeedbackHeaderButton
|
<FeedbackHeaderButton
|
||||||
logbookId={activeLogbookId}
|
logbookId={activeLogbookId}
|
||||||
logbookTitle={activeLogbookTitle}
|
logbookTitle={activeLogbookTitle}
|
||||||
|
tourOpen={tourFeedbackOpen}
|
||||||
|
onTourOpenChange={setTourFeedbackOpen}
|
||||||
|
tourHighlight={isActive && currentStepId === 'nav_feedback'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button className="btn-icon logout" onClick={handleLogout} title={t('dashboard.logout')}>
|
<button className="btn-icon logout" onClick={handleLogout} title={t('dashboard.logout')}>
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export default function DemoViewer({ onExit }: DemoViewerProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerNavigation({
|
registerNavigation({
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setSelectedEntryId: setTourSelectedEntryId
|
setSelectedEntryId: setTourSelectedEntryId,
|
||||||
|
setFeedbackOpen: () => {}
|
||||||
})
|
})
|
||||||
registerDemoTourContext({ firstEntryId: fixture.firstEntryId })
|
registerDemoTourContext({ firstEntryId: fixture.firstEntryId })
|
||||||
|
|
||||||
|
|||||||
@@ -6,31 +6,45 @@ import FeedbackModal from './FeedbackModal.tsx'
|
|||||||
interface FeedbackHeaderButtonProps {
|
interface FeedbackHeaderButtonProps {
|
||||||
logbookId?: string | null
|
logbookId?: string | null
|
||||||
logbookTitle?: string | null
|
logbookTitle?: string | null
|
||||||
|
tourOpen?: boolean
|
||||||
|
onTourOpenChange?: (open: boolean) => void
|
||||||
|
tourHighlight?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeedbackHeaderButton({
|
export default function FeedbackHeaderButton({
|
||||||
logbookId,
|
logbookId,
|
||||||
logbookTitle
|
logbookTitle,
|
||||||
|
tourOpen = false,
|
||||||
|
onTourOpenChange,
|
||||||
|
tourHighlight = false
|
||||||
}: FeedbackHeaderButtonProps) {
|
}: FeedbackHeaderButtonProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [userOpen, setUserOpen] = useState(false)
|
||||||
|
const open = tourOpen || userOpen
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setUserOpen(false)
|
||||||
|
onTourOpenChange?.(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn-icon"
|
className="btn-icon"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setUserOpen(true)}
|
||||||
title={t('feedback.button_title')}
|
title={t('feedback.button_title')}
|
||||||
aria-label={t('feedback.button_title')}
|
aria-label={t('feedback.button_title')}
|
||||||
|
data-tour="feedback-button"
|
||||||
>
|
>
|
||||||
<MessageSquarePlus size={18} />
|
<MessageSquarePlus size={18} />
|
||||||
</button>
|
</button>
|
||||||
<FeedbackModal
|
<FeedbackModal
|
||||||
open={open}
|
open={open}
|
||||||
onClose={() => setOpen(false)}
|
onClose={handleClose}
|
||||||
logbookId={logbookId}
|
logbookId={logbookId}
|
||||||
logbookTitle={logbookTitle}
|
logbookTitle={logbookTitle}
|
||||||
|
tourMode={tourHighlight}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface FeedbackModalProps {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
logbookId?: string | null
|
logbookId?: string | null
|
||||||
logbookTitle?: string | null
|
logbookTitle?: string | null
|
||||||
|
tourMode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubmitState = 'idle' | 'submitting' | 'success' | 'error'
|
type SubmitState = 'idle' | 'submitting' | 'success' | 'error'
|
||||||
@@ -18,7 +19,8 @@ export default function FeedbackModal({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
logbookId,
|
logbookId,
|
||||||
logbookTitle
|
logbookTitle,
|
||||||
|
tourMode = false
|
||||||
}: FeedbackModalProps) {
|
}: FeedbackModalProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [category, setCategory] = useState<FeedbackCategory>('general')
|
const [category, setCategory] = useState<FeedbackCategory>('general')
|
||||||
@@ -97,14 +99,20 @@ export default function FeedbackModal({
|
|||||||
if (!open) return null
|
if (!open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="disclaimer-modal-overlay" onClick={isBusy ? undefined : onClose}>
|
<div
|
||||||
|
className={`disclaimer-modal-overlay${tourMode ? ' feedback-modal-overlay--tour' : ''}`}
|
||||||
|
onClick={isBusy || tourMode ? undefined : onClose}
|
||||||
|
>
|
||||||
<div className="disclaimer-modal-panel" onClick={(event) => event.stopPropagation()}>
|
<div className="disclaimer-modal-panel" onClick={(event) => event.stopPropagation()}>
|
||||||
<div className="auth-card glass registration-disclaimer registration-disclaimer--modal feedback-modal">
|
<div
|
||||||
|
className="auth-card glass registration-disclaimer registration-disclaimer--modal feedback-modal"
|
||||||
|
data-tour="feedback-form"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="registration-disclaimer__close feedback-modal__close"
|
className="registration-disclaimer__close feedback-modal__close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={isBusy}
|
disabled={isBusy || tourMode}
|
||||||
aria-label={t('feedback.cancel')}
|
aria-label={t('feedback.cancel')}
|
||||||
>
|
>
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
@@ -187,7 +195,7 @@ export default function FeedbackModal({
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn secondary"
|
className="btn secondary"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={submitState === 'submitting'}
|
disabled={submitState === 'submitting' || tourMode}
|
||||||
>
|
>
|
||||||
{t('feedback.cancel')}
|
{t('feedback.cancel')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ export default function StatsDashboard({ logbookId, logbookTitle }: StatsDashboa
|
|||||||
}, [accountStats])
|
}, [accountStats])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-card">
|
<div className="form-card" data-tour="stats-dashboard">
|
||||||
<div className="form-header">
|
<div className="form-header">
|
||||||
<BarChart2 size={24} className="form-icon" />
|
<BarChart2 size={24} className="form-icon" />
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -27,11 +27,14 @@ export type TourStepId =
|
|||||||
| 'entry_track'
|
| 'entry_track'
|
||||||
| 'nav_vessel'
|
| 'nav_vessel'
|
||||||
| 'nav_crew'
|
| 'nav_crew'
|
||||||
|
| 'nav_stats'
|
||||||
|
| 'nav_feedback'
|
||||||
| 'finish'
|
| 'finish'
|
||||||
|
|
||||||
interface TourNavigation {
|
interface TourNavigation {
|
||||||
setActiveTab: (tab: AppTab) => void
|
setActiveTab: (tab: AppTab) => void
|
||||||
setSelectedEntryId: (entryId: string | null) => void
|
setSelectedEntryId: (entryId: string | null) => void
|
||||||
|
setFeedbackOpen: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DemoTourContext {
|
interface DemoTourContext {
|
||||||
@@ -55,7 +58,7 @@ interface AppTourContextValue {
|
|||||||
requestStartAfterLogin: () => void
|
requestStartAfterLogin: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_ORDER: TourStepId[] = [
|
const FULL_STEP_ORDER: TourStepId[] = [
|
||||||
'welcome',
|
'welcome',
|
||||||
'nav_logs',
|
'nav_logs',
|
||||||
'entry_list',
|
'entry_list',
|
||||||
@@ -63,16 +66,28 @@ const STEP_ORDER: TourStepId[] = [
|
|||||||
'entry_track',
|
'entry_track',
|
||||||
'nav_vessel',
|
'nav_vessel',
|
||||||
'nav_crew',
|
'nav_crew',
|
||||||
|
'nav_stats',
|
||||||
|
'nav_feedback',
|
||||||
'finish'
|
'finish'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** Public demo has no stats/feedback UI — skip those steps. */
|
||||||
|
const DEMO_EXCLUDED_STEPS: TourStepId[] = ['nav_stats', 'nav_feedback']
|
||||||
|
const DEMO_STEP_ORDER: TourStepId[] = FULL_STEP_ORDER.filter((id) => !DEMO_EXCLUDED_STEPS.includes(id))
|
||||||
|
|
||||||
|
function getStepOrder(demoMode: boolean): TourStepId[] {
|
||||||
|
return demoMode ? DEMO_STEP_ORDER : FULL_STEP_ORDER
|
||||||
|
}
|
||||||
|
|
||||||
const TARGET_BY_STEP: Partial<Record<TourStepId, string>> = {
|
const TARGET_BY_STEP: Partial<Record<TourStepId, string>> = {
|
||||||
nav_logs: '[data-tour="nav-logs"]',
|
nav_logs: '[data-tour="nav-logs"]',
|
||||||
entry_list: '[data-tour="entry-list"]',
|
entry_list: '[data-tour="entry-list"]',
|
||||||
entry_open: '[data-tour="entry-first"]',
|
entry_open: '[data-tour="entry-first"]',
|
||||||
entry_track: '[data-tour="entry-track"]',
|
entry_track: '[data-tour="entry-track"]',
|
||||||
nav_vessel: '[data-tour="nav-vessel"]',
|
nav_vessel: '[data-tour="nav-vessel"]',
|
||||||
nav_crew: '[data-tour="nav-crew"]'
|
nav_crew: '[data-tour="nav-crew"]',
|
||||||
|
nav_stats: '[data-tour="stats-dashboard"]',
|
||||||
|
nav_feedback: '[data-tour="feedback-form"]'
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppTourContext = createContext<AppTourContextValue | null>(null)
|
const AppTourContext = createContext<AppTourContextValue | null>(null)
|
||||||
@@ -86,7 +101,8 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
const demoContextRef = useRef<DemoTourContext | null>(null)
|
const demoContextRef = useRef<DemoTourContext | null>(null)
|
||||||
const tourModeRef = useRef<{ demoMode: boolean }>({ demoMode: false })
|
const tourModeRef = useRef<{ demoMode: boolean }>({ demoMode: false })
|
||||||
|
|
||||||
const currentStepId = isActive ? STEP_ORDER[stepIndex] ?? null : null
|
const stepOrder = getStepOrder(isDemoTour)
|
||||||
|
const currentStepId = isActive ? stepOrder[stepIndex] ?? null : null
|
||||||
|
|
||||||
const resolveFirstEntryId = useCallback((): string | null => {
|
const resolveFirstEntryId = useCallback((): string | null => {
|
||||||
return demoContextRef.current?.firstEntryId ?? getStoredDemoFirstEntryId()
|
return demoContextRef.current?.firstEntryId ?? getStoredDemoFirstEntryId()
|
||||||
@@ -111,16 +127,29 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
nav.setSelectedEntryId(null)
|
nav.setSelectedEntryId(null)
|
||||||
nav.setActiveTab('crew')
|
nav.setActiveTab('crew')
|
||||||
}
|
}
|
||||||
|
if (stepId === 'nav_stats') {
|
||||||
|
nav.setSelectedEntryId(null)
|
||||||
|
nav.setActiveTab('stats')
|
||||||
|
}
|
||||||
|
if (stepId === 'nav_feedback') {
|
||||||
|
nav.setSelectedEntryId(null)
|
||||||
|
nav.setFeedbackOpen(true)
|
||||||
|
} else {
|
||||||
|
nav.setFeedbackOpen(false)
|
||||||
|
}
|
||||||
}, [resolveFirstEntryId])
|
}, [resolveFirstEntryId])
|
||||||
|
|
||||||
const scrollToCurrentTarget = useCallback((stepId: TourStepId | null) => {
|
const scrollToCurrentTarget = useCallback((stepId: TourStepId | null) => {
|
||||||
if (!stepId) return
|
if (!stepId) return
|
||||||
const selector = TARGET_BY_STEP[stepId]
|
const selector = TARGET_BY_STEP[stepId]
|
||||||
if (!selector) return
|
if (!selector) return
|
||||||
|
const delayMs = stepId === 'nav_feedback' ? 180 : 0
|
||||||
|
window.setTimeout(() => {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
const el = document.querySelector(selector)
|
const el = document.querySelector(selector)
|
||||||
el?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' })
|
el?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' })
|
||||||
})
|
})
|
||||||
|
}, delayMs)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const startTour = useCallback((options?: { force?: boolean; demoMode?: boolean }) => {
|
const startTour = useCallback((options?: { force?: boolean; demoMode?: boolean }) => {
|
||||||
@@ -142,12 +171,18 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
const tourProps = tourModeRef.current.demoMode ? { mode: 'demo' } : undefined
|
const tourProps = tourModeRef.current.demoMode ? { mode: 'demo' } : undefined
|
||||||
if (outcome === 'completed') {
|
if (outcome === 'completed') {
|
||||||
trackPlausibleEvent(PlausibleEvents.ONBOARDING_TOUR_COMPLETED, tourProps)
|
trackPlausibleEvent(PlausibleEvents.ONBOARDING_TOUR_COMPLETED, tourProps)
|
||||||
|
const nav = navigationRef.current
|
||||||
|
if (nav && !tourModeRef.current.demoMode) {
|
||||||
|
nav.setSelectedEntryId(null)
|
||||||
|
nav.setActiveTab('stats')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const step = STEP_ORDER[stepIndexAtDismiss] ?? 'welcome'
|
const step = getStepOrder(tourModeRef.current.demoMode)[stepIndexAtDismiss] ?? 'welcome'
|
||||||
trackPlausibleEvent(PlausibleEvents.ONBOARDING_TOUR_SKIPPED, { step, ...tourProps })
|
trackPlausibleEvent(PlausibleEvents.ONBOARDING_TOUR_SKIPPED, { step, ...tourProps })
|
||||||
}
|
}
|
||||||
|
|
||||||
tourModeRef.current = { demoMode: false }
|
tourModeRef.current = { demoMode: false }
|
||||||
|
navigationRef.current?.setFeedbackOpen(false)
|
||||||
setIsDemoTour(false)
|
setIsDemoTour(false)
|
||||||
setIsActive(false)
|
setIsActive(false)
|
||||||
setStepIndex(0)
|
setStepIndex(0)
|
||||||
@@ -162,12 +197,13 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [dismissTour, stepIndex])
|
}, [dismissTour, stepIndex])
|
||||||
|
|
||||||
const nextStep = useCallback(() => {
|
const nextStep = useCallback(() => {
|
||||||
if (stepIndex + 1 >= STEP_ORDER.length) {
|
const order = getStepOrder(isDemoTour)
|
||||||
|
if (stepIndex + 1 >= order.length) {
|
||||||
dismissTour('completed', stepIndex)
|
dismissTour('completed', stepIndex)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStepIndex(stepIndex + 1)
|
setStepIndex(stepIndex + 1)
|
||||||
}, [dismissTour, stepIndex])
|
}, [dismissTour, isDemoTour, stepIndex])
|
||||||
|
|
||||||
const prevStep = useCallback(() => {
|
const prevStep = useCallback(() => {
|
||||||
setStepIndex((current) => Math.max(0, current - 1))
|
setStepIndex((current) => Math.max(0, current - 1))
|
||||||
@@ -175,11 +211,11 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive) return
|
if (!isActive) return
|
||||||
const stepId = STEP_ORDER[stepIndex]
|
const stepId = getStepOrder(isDemoTour)[stepIndex]
|
||||||
if (!stepId) return
|
if (!stepId) return
|
||||||
applyStepSideEffects(stepId)
|
applyStepSideEffects(stepId)
|
||||||
scrollToCurrentTarget(stepId)
|
scrollToCurrentTarget(stepId)
|
||||||
}, [isActive, stepIndex, applyStepSideEffects, scrollToCurrentTarget])
|
}, [isActive, isDemoTour, stepIndex, applyStepSideEffects, scrollToCurrentTarget])
|
||||||
|
|
||||||
const restartTour = useCallback(() => {
|
const restartTour = useCallback(() => {
|
||||||
const userId = localStorage.getItem('active_userid')
|
const userId = localStorage.getItem('active_userid')
|
||||||
@@ -220,7 +256,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
isDemoTour,
|
isDemoTour,
|
||||||
currentStepId,
|
currentStepId,
|
||||||
currentStepIndex: stepIndex,
|
currentStepIndex: stepIndex,
|
||||||
totalSteps: STEP_ORDER.length,
|
totalSteps: stepOrder.length,
|
||||||
startTour,
|
startTour,
|
||||||
stopTour,
|
stopTour,
|
||||||
restartTour,
|
restartTour,
|
||||||
@@ -244,6 +280,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
skipTour,
|
skipTour,
|
||||||
startTour,
|
startTour,
|
||||||
stepIndex,
|
stepIndex,
|
||||||
|
stepOrder.length,
|
||||||
stopTour
|
stopTour
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -467,7 +467,7 @@
|
|||||||
"steps": {
|
"steps": {
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "Willkommen an Bord!",
|
"title": "Willkommen an Bord!",
|
||||||
"body": "Wir haben ein Demo-Logbuch mit drei Reisetagen in der Kieler Förde für Sie angelegt. Diese kurze Tour zeigt Ihnen die wichtigsten Funktionen."
|
"body": "Wir haben ein Demo-Logbuch mit drei Reisetagen in der Kieler Förde für Sie angelegt. Die Beispieleinträge können Sie jederzeit löschen, wenn Sie mit dem eigenen Logbuch starten möchten. Diese kurze Tour zeigt Ihnen die wichtigsten Funktionen."
|
||||||
},
|
},
|
||||||
"welcome_public": {
|
"welcome_public": {
|
||||||
"title": "Willkommen an Bord!",
|
"title": "Willkommen an Bord!",
|
||||||
@@ -497,9 +497,17 @@
|
|||||||
"title": "Crew-Liste",
|
"title": "Crew-Liste",
|
||||||
"body": "Verwalten Sie Besatzungsmitglieder und weisen Sie sie später Reisetagen zu."
|
"body": "Verwalten Sie Besatzungsmitglieder und weisen Sie sie später Reisetagen zu."
|
||||||
},
|
},
|
||||||
|
"nav_stats": {
|
||||||
|
"title": "Statistik-Dashboard",
|
||||||
|
"body": "Hier sehen Sie Reisedistanzen, Verbrauch, Routenkarten und Antriebsanteile – automatisch aus Ihren Logbucheinträgen berechnet."
|
||||||
|
},
|
||||||
|
"nav_feedback": {
|
||||||
|
"title": "Feedback senden",
|
||||||
|
"body": "Über dieses Formular können Sie Fehler, Ideen oder allgemeines Feedback direkt an das Projektteam schicken – auch nach der Tour jederzeit über das Symbol oben rechts."
|
||||||
|
},
|
||||||
"finish": {
|
"finish": {
|
||||||
"title": "Alles klar!",
|
"title": "Alles klar!",
|
||||||
"body": "Sie können die Tour jederzeit unter Einstellungen erneut starten. Gute Fahrt!"
|
"body": "Sie landen gleich im Statistik-Dashboard. Die Tour können Sie jederzeit unter Einstellungen erneut starten. Gute Fahrt!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -467,7 +467,7 @@
|
|||||||
"steps": {
|
"steps": {
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "Welcome aboard!",
|
"title": "Welcome aboard!",
|
||||||
"body": "We created a demo logbook with three travel days in Kiel Bay. This short tour shows you the key features."
|
"body": "We created a demo logbook with three travel days in Kiel Bay. You can delete the sample entries anytime when you're ready to start your own logbook. This short tour shows you the key features."
|
||||||
},
|
},
|
||||||
"welcome_public": {
|
"welcome_public": {
|
||||||
"title": "Welcome aboard!",
|
"title": "Welcome aboard!",
|
||||||
@@ -497,9 +497,17 @@
|
|||||||
"title": "Crew list",
|
"title": "Crew list",
|
||||||
"body": "Manage crew members and assign them to travel days later."
|
"body": "Manage crew members and assign them to travel days later."
|
||||||
},
|
},
|
||||||
|
"nav_stats": {
|
||||||
|
"title": "Statistics dashboard",
|
||||||
|
"body": "View travel distances, consumption, route maps, and propulsion breakdown — calculated automatically from your log entries."
|
||||||
|
},
|
||||||
|
"nav_feedback": {
|
||||||
|
"title": "Send feedback",
|
||||||
|
"body": "Use this form to report bugs, ideas, or general feedback to the project team — you can also open it anytime later via the icon in the top right."
|
||||||
|
},
|
||||||
"finish": {
|
"finish": {
|
||||||
"title": "You're all set!",
|
"title": "You're all set!",
|
||||||
"body": "You can restart the tour anytime in Settings. Fair winds!"
|
"body": "You'll land on the statistics dashboard next. You can restart the tour anytime in Settings. Fair winds!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user