Files
anwar-pi-website/web/src/app/(frontend)/vip-onboarding/onboarding-wizard.tsx
T
2026-10-09 16:20:55 +03:00

198 lines
13 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
/* 4-step VIP onboarding wizard — port of vip-onboarding.html step logic
(radio choice-grid, next/back, final summary + recommendation).
Selections persist in localStorage (faithful to the simulated original). */
const GOALS = [
{ value: 'بناء مشروع ذكاء اصطناعي', copy: 'حوّل فكرة إلى نموذج يعمل ويمكن اختباره مع مستخدمين.' },
{ value: 'تطوير مساري المهني', copy: 'ابنِ خبرة عملية ومشروعات تشرح قيمتك بوضوح.' },
{ value: 'أتمتة العمل', copy: 'حدد مهمة متكررة وابنِ لها سير عمل يمكن مراقبته.' },
{ value: 'فهم الوكلاء بعمق', copy: 'صمم التخطيط والتنفيذ والتحقق وحدود التوقف بأمان.' },
]
const LEVELS = [
{ value: 'مبتدئ', label: 'أبدأ من الأساسيات', copy: 'استخدمت أدوات الذكاء الاصطناعي، لكنني لم أبنِ مشروعاً بعد.' },
{ value: 'متوسط', label: 'بنيت تجارب بسيطة', copy: 'جرّبت واجهات برمجة التطبيقات (API) أو أتمتة أو تطبيقاً صغيراً.' },
{ value: 'متقدم', label: 'أعمل على أنظمة فعلية', copy: 'أحتاج أنماطاً أعمق للقياس والموثوقية والتشغيل.' },
]
const LABELS = ['الخطوة 1 من 4', 'الخطوة 2 من 4', 'الخطوة 3 من 4', 'الخطوة 4 من 4']
const PERCENTAGES = ['25٪', '50٪', '75٪', '100٪']
const STEP_TITLES = ['هدفك الآن', 'مستواك العملي', 'رسائلك', 'ابدأ مسارك']
type State = {
goal: string
level: string
weekly: boolean
trackUpdates: boolean
replies: boolean
step: number
}
function recommendation(goal: string): [string, string, string] {
if (goal === 'أتمتة العمل') return ['راقب قبل أن تؤتمت', 'ابدأ بتحديد إشارات النجاح والفشل قبل بناء سير العمل، ثم استخدم قائمة المراقبة في أول تجربة.', '/articles?topic=automation']
if (goal === 'بناء مشروع ذكاء اصطناعي') return ['من الفكرة إلى نموذج قابل للاختبار', 'اختر مشكلة واحدة ومستخدمًا واحدًا، ثم حوّل الفرضية إلى نموذج صغير يمكن اختباره هذا الأسبوع.', '/posts/prototype-to-production']
if (goal === 'تطوير مساري المهني') return ['تشريح وكيل ناجح', 'ابنِ نموذجاً صغيراً يوضح التخطيط والتنفيذ والتحقق، ثم وثّق قراراتك كدراسة حالة.', '/posts/agent-loop-stop-condition']
return ['تشريح وكيل ناجح', 'ابدأ بفصل التخطيط عن التنفيذ والتحقق، ثم استخدم قالب حالة الوكيل في مشروع صغير.', '/posts/agent-loop-stop-condition']
}
export default function OnboardingWizard({ viewerId }: { viewerId: number | null }) {
const [state, setState] = useState<State>({ goal: '', level: '', weekly: true, trackUpdates: true, replies: true, step: 0 })
const [goalStatus, setGoalStatus] = useState('')
const [levelStatus, setLevelStatus] = useState('')
const [restored, setRestored] = useState(false)
/* Backend persistence — goal/level/prefs live on the user record. */
useEffect(() => {
if (!viewerId || !state.goal || !state.level) return
fetch(`/api/users/${viewerId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
goal: state.goal,
level: state.level,
prefs: { weekly: state.weekly, trackUpdates: state.trackUpdates, replies: state.replies },
}),
}).catch(() => null)
}, [viewerId, state.goal, state.level, state.weekly, state.trackUpdates, state.replies])
useEffect(() => {
try {
const raw = localStorage.getItem('anwarpi:vip-onboarding')
if (raw) {
const s = JSON.parse(raw) as Partial<State>
setState(prev => ({
...prev,
goal: s.goal || '',
level: s.level && LEVELS.some(l => l.value === s.level) ? s.level : '',
weekly: typeof s.weekly === 'boolean' ? s.weekly : true,
trackUpdates: typeof s.trackUpdates === 'boolean' ? s.trackUpdates : true,
replies: typeof s.replies === 'boolean' ? s.replies : true,
step: Math.max(0, Math.min(3, Number(s.step) || 0)),
}))
}
} catch { /* ignore */ }
setRestored(true)
}, [])
useEffect(() => {
if (!restored) return
try { localStorage.setItem('anwarpi:vip-onboarding', JSON.stringify(state)) } catch { /* ignore */ }
}, [state, restored])
function next() {
if (state.step === 0 && !state.goal) { setGoalStatus('اختر هدفاً واحداً للمتابعة.'); return }
if (state.step === 1 && !state.level) { setLevelStatus('اختر مستوى واحداً للمتابعة.'); return }
setState(s => ({ ...s, step: Math.min(3, s.step + 1) }))
}
const notices = [
state.weekly && 'خلاصة الأحد',
state.trackUpdates && 'تحديثات المسار',
state.replies && 'الردود',
].filter(Boolean) as string[]
const rec = recommendation(state.goal)
return (
<>
<section className="onboarding-head" data-od-id="onboarding-hero">
<div className="container">
<p className="eyebrow">عضو جديد · إعداد سريع</p>
<h1 data-od-id="onboarding-title">مرحباً بك. لنبدأ بما يخدم هدفك.</h1>
<p className="lead">أجب عن سؤالين وحدد رسائلك المفضلة. سنقترح نقطة بداية واضحة بدلاً من تركك أمام مكتبة كبيرة.</p>
<div className="progress-wrap" aria-label="تقدم الإعداد">
<div className="progress-copy"><span id="progressLabel">{LABELS[state.step]}</span><span id="progressPercent">{PERCENTAGES[state.step]}</span></div>
<div className="progress-track"><div className="progress-bar" id="progressBar" style={{ width: `${(state.step + 1) * 25}%` }}></div></div>
</div>
</div>
</section>
<section className="container onboarding-layout" data-od-id="onboarding-flow">
<nav className="step-nav" aria-label="خطوات إعداد العضوية" data-od-id="onboarding-steps">
{STEP_TITLES.map((title, i) => (
<button type="button" key={i} aria-current={state.step === i ? 'step' : undefined} disabled className={i < state.step ? 'done' : ''}>
<span className="step-num">{'01020304'.slice(i * 2, i * 2 + 2)}</span><span>{title}</span>
</button>
))}
</nav>
<form id="onboardingForm" noValidate onSubmit={e => e.preventDefault()}>
<section className={`step-panel${state.step === 0 ? ' active' : ''}`} data-step={0} data-od-id="step-goal">
<h2>ما النتيجة التي تريدها أولاً؟</h2>
<p className="step-intro">اختر هدفاً واحداً الآن. يمكنك تغييره لاحقاً من إعدادات الحساب.</p>
<div className="choice-grid" role="radiogroup" aria-label="هدفك">
{GOALS.map(g => (
<label className="choice" key={g.value}>
<input type="radio" name="goal" value={g.value} required checked={state.goal === g.value} onChange={() => { setState(s => ({ ...s, goal: g.value })); setGoalStatus('') }} />
<span className="choice-body"><span className="choice-title">{g.value}</span><span className="choice-copy">{g.copy}</span><span className="choice-mark" aria-hidden="true"></span></span>
</label>
))}
</div>
<p className="status" id="goalStatus" role="status" aria-live="polite">{goalStatus}</p>
<div className="actions"><button className="btn btn-primary" type="button" onClick={next}>تابع إلى مستواك</button></div>
</section>
<section className={`step-panel${state.step === 1 ? ' active' : ''}`} data-step={1} data-od-id="step-level">
<h2>ما مستوى خبرتك العملية؟</h2>
<p className="step-intro">لا نختبرك. الإجابة تضبط ترتيب المواد وكمية الشرح في اقتراح البداية.</p>
<div className="choice-grid" role="radiogroup" aria-label="مستواك">
{LEVELS.map(l => (
<label className="choice" key={l.value}>
<input type="radio" name="level" value={l.value} required checked={state.level === l.value} onChange={() => { setState(s => ({ ...s, level: l.value })); setLevelStatus('') }} />
<span className="choice-body"><span className="choice-title">{l.label}</span><span className="choice-copy">{l.copy}</span><span className="choice-mark" aria-hidden="true"></span></span>
</label>
))}
</div>
<p className="status" id="levelStatus" role="status" aria-live="polite">{levelStatus}</p>
<div className="actions">
<button className="btn btn-secondary" type="button" onClick={() => setState(s => ({ ...s, step: 0 }))}>رجوع</button>
<button className="btn btn-primary" type="button" onClick={next}>تابع إلى الرسائل</button>
</div>
</section>
<section className={`step-panel${state.step === 2 ? ' active' : ''}`} data-step={2} data-od-id="step-notifications">
<h2>اختر ما يستحق الوصول إلى بريدك.</h2>
<p className="step-intro">كل الخيارات قابلة للتغيير. لا يؤثر إيقافها على وصولك إلى محتوى العضوية.</p>
<div className="notice-card">
<div className="notice-row"><div><strong>خلاصة الأحد</strong><p>أفضل ما نُشر هذا الأسبوع مع نقطة تطبيق واحدة.</p></div>
<label className="switch"><input type="checkbox" name="weekly" checked={state.weekly} onChange={e => setState(s => ({ ...s, weekly: e.target.checked }))} aria-label="تفعيل خلاصة الأحد" /><span className="track"></span></label></div>
<div className="notice-row"><div><strong>تحديثات المسار</strong><p>رسالة عندما نضيف جزءاً جديداً إلى المسار الذي تتبعه.</p></div>
<label className="switch"><input type="checkbox" name="trackUpdates" checked={state.trackUpdates} onChange={e => setState(s => ({ ...s, trackUpdates: e.target.checked }))} aria-label="تفعيل تحديثات المسار" /><span className="track"></span></label></div>
<div className="notice-row"><div><strong>الردود على تعليقاتك</strong><p>تنبيه فقط عندما يرد عضو أو فريق أنور على سؤالك.</p></div>
<label className="switch"><input type="checkbox" name="replies" checked={state.replies} onChange={e => setState(s => ({ ...s, replies: e.target.checked }))} aria-label="تفعيل إشعارات الردود" /><span className="track"></span></label></div>
</div>
<div className="actions">
<button className="btn btn-secondary" type="button" onClick={() => setState(s => ({ ...s, step: 1 }))}>رجوع</button>
<button className="btn btn-primary" type="button" onClick={() => setState(s => ({ ...s, step: 3 }))}>اعرض نقطة البداية</button>
</div>
</section>
<section className={`step-panel${state.step === 3 ? ' active' : ''}`} data-step={3} data-od-id="step-start">
<h2>مسارك جاهز.</h2>
<p className="step-intro">ابدأ بمادة واحدة، طبّق ملفها العملي، ثم عد إلى المكتبة. لا حاجة لفتح كل شيء اليوم.</p>
<dl className="summary">
<div className="summary-row"><dt>هدفك</dt><dd id="summaryGoal">{state.goal || '—'}</dd></div>
<div className="summary-row"><dt>مستواك</dt><dd id="summaryLevel">{state.level || '—'}</dd></div>
<div className="summary-row"><dt>الرسائل</dt><dd id="summaryNotices">{notices.length ? notices.join('، ') : 'لا رسائل بريدية'}</dd></div>
</dl>
<article className="start-card" data-od-id="recommended-first-guide">
<p className="eyebrow">نقطة البداية المقترحة</p>
<h3 id="recommendationTitle">{rec[0]}</h3>
<p id="recommendationCopy">{rec[1]}</p>
<Link className="btn btn-primary" href={rec[2]} id="startGuide" data-od-id="start-first-guide">ابدأ الدليل الأول</Link>
</article>
<div className="actions">
<button className="btn btn-secondary" type="button" onClick={() => setState(s => ({ ...s, step: 2 }))}>تعديل الاختيارات</button>
<Link className="btn btn-secondary" href="/articles">اذهب إلى المكتبة</Link>
</div>
</section>
</form>
</section>
</>
)
}