Skip to content

Commit b6cf4a6

Browse files
committed
feat(onboarding): API /api/onboarding stato tour + OnboardingTour.tsx 4 step con tooltip e overlay
2 parents 1fc28de + 8d087f2 commit b6cf4a6

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

web/app/api/onboarding/route.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import fs from 'fs'
3+
import path from 'path'
4+
import os from 'os'
5+
6+
export const dynamic = 'force-dynamic'
7+
8+
const STATE_PATH = path.join(os.homedir(), '.jht', 'onboarding.json')
9+
10+
type OnboardingState = {
11+
completed: boolean
12+
completedAt?: number
13+
skipped: boolean
14+
stepsCompleted: string[]
15+
}
16+
17+
function load(): OnboardingState {
18+
try {
19+
return JSON.parse(fs.readFileSync(STATE_PATH, 'utf-8')) as OnboardingState
20+
} catch {
21+
return { completed: false, skipped: false, stepsCompleted: [] }
22+
}
23+
}
24+
25+
function save(state: OnboardingState): void {
26+
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true })
27+
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf-8')
28+
}
29+
30+
export async function GET() {
31+
return NextResponse.json(load())
32+
}
33+
34+
export async function POST(req: NextRequest) {
35+
let body: Record<string, unknown>
36+
try { body = await req.json() }
37+
catch { return NextResponse.json({ error: 'body non valido' }, { status: 400 }) }
38+
39+
const state = load()
40+
41+
if (body.skip === true) {
42+
state.skipped = true
43+
state.completed = true
44+
state.completedAt = Date.now()
45+
save(state)
46+
return NextResponse.json(state)
47+
}
48+
49+
if (body.stepId && typeof body.stepId === 'string') {
50+
if (!state.stepsCompleted.includes(body.stepId)) {
51+
state.stepsCompleted.push(body.stepId)
52+
}
53+
save(state)
54+
return NextResponse.json(state)
55+
}
56+
57+
if (body.complete === true) {
58+
state.completed = true
59+
state.completedAt = Date.now()
60+
save(state)
61+
return NextResponse.json(state)
62+
}
63+
64+
if (body.reset === true) {
65+
const fresh: OnboardingState = { completed: false, skipped: false, stepsCompleted: [] }
66+
save(fresh)
67+
return NextResponse.json(fresh)
68+
}
69+
70+
return NextResponse.json({ error: 'azione non valida' }, { status: 400 })
71+
}

web/components/OnboardingTour.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use client'
2+
3+
import { useEffect, useState, useCallback } from 'react'
4+
5+
type Step = {
6+
id: string
7+
title: string
8+
description: string
9+
selector?: string
10+
position: 'top' | 'bottom' | 'right' | 'left'
11+
}
12+
13+
const STEPS: Step[] = [
14+
{
15+
id: 'sidebar',
16+
title: 'Navigazione',
17+
description: 'Usa la sidebar per accedere a tutte le sezioni: agenti, sessioni, analytics e impostazioni.',
18+
selector: 'nav[aria-label="sidebar"]',
19+
position: 'right',
20+
},
21+
{
22+
id: 'dashboard',
23+
title: 'Overview',
24+
description: 'La dashboard mostra lo stato del team in tempo reale — agenti attivi, token, costi e plugin.',
25+
selector: '[data-tour="overview"]',
26+
position: 'bottom',
27+
},
28+
{
29+
id: 'search',
30+
title: 'Ricerca globale',
31+
description: 'Premi Cmd+K per cercare agenti, sessioni, task e pagine ovunque nella dashboard.',
32+
selector: '[data-tour="search"]',
33+
position: 'bottom',
34+
},
35+
{
36+
id: 'settings',
37+
title: 'Impostazioni',
38+
description: 'Configura provider AI, API key, bot Telegram e cron job dalla pagina impostazioni.',
39+
selector: '[data-tour="settings"]',
40+
position: 'top',
41+
},
42+
]
43+
44+
type TooltipProps = { step: Step; index: number; total: number; onNext: () => void; onSkip: () => void }
45+
46+
function Tooltip({ step, index, total, onNext, onSkip }: TooltipProps) {
47+
return (
48+
<div
49+
className="fixed z-[9999] w-72 rounded-xl p-4 flex flex-col gap-3"
50+
style={{
51+
bottom: step.position === 'bottom' ? 'auto' : '2rem',
52+
top: step.position === 'bottom' ? '4rem' : 'auto',
53+
right: step.position === 'right' ? 'auto' : '1.5rem',
54+
left: step.position === 'right' ? '14rem' : 'auto',
55+
background: 'var(--color-panel)',
56+
border: '1px solid var(--color-green)',
57+
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
58+
}}
59+
>
60+
<div className="flex items-center justify-between">
61+
<p className="text-[9px] font-semibold tracking-[0.2em] uppercase" style={{ color: 'var(--color-green)' }}>
62+
{index + 1} / {total}
63+
</p>
64+
<button
65+
onClick={onSkip}
66+
className="text-[10px] cursor-pointer"
67+
style={{ color: 'var(--color-dim)', background: 'none', border: 'none' }}
68+
>
69+
salta
70+
</button>
71+
</div>
72+
<p className="text-[13px] font-bold" style={{ color: 'var(--color-white)' }}>{step.title}</p>
73+
<p className="text-[11px] leading-relaxed" style={{ color: 'var(--color-muted)' }}>{step.description}</p>
74+
<button
75+
onClick={onNext}
76+
className="py-2 rounded text-[11px] font-bold cursor-pointer"
77+
style={{ background: 'var(--color-green)', color: 'var(--color-bg)' }}
78+
>
79+
{index < STEPS.length - 1 ? 'Avanti →' : 'Fine'}
80+
</button>
81+
</div>
82+
)
83+
}
84+
85+
export default function OnboardingTour() {
86+
const [active, setActive] = useState(false)
87+
const [step, setStep] = useState(0)
88+
89+
useEffect(() => {
90+
fetch('/api/onboarding')
91+
.then(r => r.json())
92+
.then(s => { if (!s.completed) setActive(true) })
93+
.catch(() => {})
94+
}, [])
95+
96+
const complete = useCallback(() => {
97+
fetch('/api/onboarding', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ complete: true }) }).catch(() => {})
98+
setActive(false)
99+
}, [])
100+
101+
const skip = useCallback(() => {
102+
fetch('/api/onboarding', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ skip: true }) }).catch(() => {})
103+
setActive(false)
104+
}, [])
105+
106+
const next = useCallback(() => {
107+
const s = STEPS[step]
108+
fetch('/api/onboarding', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stepId: s.id }) }).catch(() => {})
109+
if (step < STEPS.length - 1) setStep(i => i + 1)
110+
else complete()
111+
}, [step, complete])
112+
113+
if (!active) return null
114+
115+
return (
116+
<>
117+
<div className="fixed inset-0 z-[9998]" style={{ background: 'rgba(0,0,0,0.45)', pointerEvents: 'none' }} />
118+
<Tooltip step={STEPS[step]} index={step} total={STEPS.length} onNext={next} onSkip={skip} />
119+
</>
120+
)
121+
}

0 commit comments

Comments
 (0)