198 lines
13 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|