Audit fixes: VIP self-grant, draft leak, onboarding flag, hero personalization, gated downloads
Deploy GitHub Pages / deploy (push) Canceled after 0s
Deploy GitHub Pages / deploy (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { safeNext } from '@/lib/next'
|
||||
|
||||
type Viewer = { id: number; email: string; interest: string | null; level: string | null; onboarded: boolean } | null
|
||||
|
||||
const COPY = ['الخطوة 1 من 3', 'الخطوة 2 من 3', 'الخطوة 3 من 3']
|
||||
const PERCENTS = ['33٪', '67٪', '100٪']
|
||||
|
||||
/* Wizard markup/steps from member-onboarding.html. Interest + level PATCH the
|
||||
user record when leaving each step (falls back to localStorage if no session). */
|
||||
export default function OnboardingWizard({ viewer }: { viewer: Viewer }) {
|
||||
const [step, setStep] = useState(0)
|
||||
const [interest, setInterest] = useState(viewer?.interest ?? '')
|
||||
const [level, setLevel] = useState(viewer?.level ?? '')
|
||||
const [status, setStatus] = useState('')
|
||||
const [nextPath, setNextPath] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
try { setNextPath(safeNext(sessionStorage.getItem('anwarpi:signup-next'))) } catch { /* private mode */ }
|
||||
}, [])
|
||||
|
||||
function show(next: number) {
|
||||
setStep(Math.max(0, Math.min(2, next)))
|
||||
}
|
||||
|
||||
async function persist(fields: Record<string, unknown>) {
|
||||
try {
|
||||
localStorage.setItem('anwarpi:member-onboarding', JSON.stringify({ interest, experience: level }))
|
||||
} catch { /* private mode */ }
|
||||
if (!viewer) return
|
||||
await fetch(`/api/users/${viewer.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(fields),
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
async function next() {
|
||||
if (step === 0) {
|
||||
if (!interest) { setStatus('اختر مساراً واحداً للمتابعة.'); return }
|
||||
setStatus('')
|
||||
await persist({ interest })
|
||||
show(1)
|
||||
return
|
||||
}
|
||||
if (step === 1) {
|
||||
if (!level) { setStatus('اختر مستوى واحداً للمتابعة.'); return }
|
||||
setStatus('')
|
||||
await persist({ level, onboarded: true })
|
||||
show(2)
|
||||
}
|
||||
}
|
||||
|
||||
const summary = interest && level ? `اخترت مسار ${interest} بمستوى ${level}. حسابك المجاني جاهز، ويمكنك الترقية الآن أو لاحقاً.` : 'حسابك المجاني جاهز. يمكنك الترقية الآن أو لاحقاً من صفحة العضوية.'
|
||||
|
||||
useEffect(() => {
|
||||
const bar = document.getElementById('progressBar')
|
||||
if (bar) bar.style.width = `${((step + 1) / 3) * 100}%`
|
||||
const label = document.getElementById('progressLabel')
|
||||
if (label) label.textContent = COPY[step]
|
||||
const percent = document.getElementById('progressPercent')
|
||||
if (percent) percent.textContent = PERCENTS[step]
|
||||
}, [step])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="container flow" data-od-id="member-onboarding-flow">
|
||||
<nav className="step-nav" aria-label="خطوات إعداد الحساب" data-od-id="onboarding-steps">
|
||||
{[0, 1, 2].map(i => (
|
||||
<div key={i} className={i === step ? 'active' : i < step ? 'done' : ''} data-step-label={i}>
|
||||
<span>{['01', '02', '03'][i]}</span><span>{['اهتمامك', 'مستواك', 'خيارات العضوية'][i]}</span>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<form id="memberOnboarding" noValidate onSubmit={e => e.preventDefault()}>
|
||||
<section className={`panel${step === 0 ? ' active' : ''}`} data-step="0" data-od-id="step-interest">
|
||||
<h2>أي مسار تريد أن يظهر أولاً؟</h2>
|
||||
<p className="intro">اختر مساراً واحداً الآن. ستظل بقية المكتبة متاحة لك.</p>
|
||||
<div className="choices">
|
||||
{[
|
||||
['الوكلاء', 'الوكلاء وتنسيقهم', 'التخطيط والتنفيذ والتحقق وتنسيق عدة وكلاء.'],
|
||||
['RAG', 'التوليد المعزّز بالاسترجاع (RAG)', 'اختيار المصادر وقياس جودة الاسترجاع والإجابة.'],
|
||||
['الأتمتة', 'الأتمتة للأعمال', 'دمج الذكاء الاصطناعي في تدفقات عمل قابلة للمراقبة.'],
|
||||
['النماذج المفتوحة', 'النماذج المفتوحة', 'التشغيل المحلي والضبط الدقيق واختيار النموذج المناسب.'],
|
||||
].map(([value, label, desc]) => (
|
||||
<label className="choice" key={value}>
|
||||
<input type="radio" name="interest" value={value} checked={interest === value} onChange={() => { setInterest(value); setStatus('') }} />
|
||||
<span className="choice-body"><strong>{label}</strong><small>{desc}</small><span className="choice-mark" aria-hidden="true"></span></span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="status" id="interestStatus" role="status" aria-live="polite">{step === 0 ? status : ''}</p>
|
||||
<div className="actions">
|
||||
<button className="btn btn-primary" type="button" data-next onClick={next}>تابع إلى مستواك</button>
|
||||
</div>
|
||||
</section>
|
||||
<section className={`panel${step === 1 ? ' active' : ''}`} data-step="1" data-od-id="step-experience">
|
||||
<h2>ما مستوى خبرتك العملية؟</h2>
|
||||
<p className="intro">سنستخدمه فقط لترتيب نقطة البداية، لا لإخفاء أي محتوى مجاني.</p>
|
||||
<div className="choices">
|
||||
{[
|
||||
['مبتدئ', 'أبدأ من الأساسيات', 'أستخدم أدوات الذكاء الاصطناعي ولم أبنِ مشروعاً بعد.'],
|
||||
['متوسط', 'بنيت تجارب بسيطة', 'جرّبت واجهات برمجة التطبيقات (API) أو أتمتة أو تطبيقاً صغيراً.'],
|
||||
['متقدم', 'أعمل على أنظمة فعلية', 'أبحث عن أنماط أعمق للقياس والموثوقية والتشغيل.'],
|
||||
].map(([value, label, desc]) => (
|
||||
<label className="choice" key={value}>
|
||||
<input type="radio" name="experience" value={value} checked={level === value} onChange={() => { setLevel(value); setStatus('') }} />
|
||||
<span className="choice-body"><strong>{label}</strong><small>{desc}</small><span className="choice-mark" aria-hidden="true"></span></span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="status" id="experienceStatus" role="status" aria-live="polite">{step === 1 ? status : ''}</p>
|
||||
<div className="actions">
|
||||
<button className="btn btn-secondary" type="button" data-back onClick={() => show(0)}>رجوع</button>
|
||||
<button className="btn btn-primary" type="button" data-next onClick={next}>شاهد خيارات الوصول</button>
|
||||
</div>
|
||||
</section>
|
||||
<section className={`panel${step === 2 ? ' active' : ''}`} data-step="2" data-od-id="step-access">
|
||||
<p className="eyebrow">اختيارك، بلا ضغط</p>
|
||||
<h2>ابدأ مجاناً أو افتح محتوى العضوية الكامل.</h2>
|
||||
<p className="intro" id="summaryCopy">{summary}</p>
|
||||
<div className="benefits" data-od-id="vip-benefits">
|
||||
<article className="benefit"><span className="benefit-num">01</span><div><h3>الأدلة المتقدمة كاملة</h3><p>اقرأ ما بعد المعاينة في أدلة الوكلاء وRAG والأتمتة والنماذج.</p></div></article>
|
||||
<article className="benefit"><span className="benefit-num">02</span><div><h3>ملفات العمل</h3><p>نزّل القوالب وقوائم الفحص المرتبطة بكل دليل.</p></div></article>
|
||||
<article className="benefit"><span className="benefit-num">03</span><div><h3>نقطة بداية مخصصة</h3><p>إعداد العضوية ترتّب دليلك الأول ورسائلك بناءً على هدفك.</p></div></article>
|
||||
</div>
|
||||
<div className="upgrade-note">
|
||||
<strong>الحساب المجاني لا يختفي.</strong>
|
||||
<p>يمكنك قراءة الأدلة المجانية وحفظها، والترقية عندما تحتاج الأدلة الحصرية.</p>
|
||||
</div>
|
||||
<div className="actions" data-od-id="final-actions">
|
||||
{nextPath ? (
|
||||
<Link className="btn btn-primary" href={nextPath} data-od-id="continue-next-cta">تابع إلى وجهتك</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link className="btn btn-secondary" href="/articles?access=free" data-od-id="continue-free-cta">ابدأ بالأدلة المجانية</Link>
|
||||
<Link className="btn btn-primary" href="/subscription" data-od-id="view-membership-cta">شاهد العضوية والأسعار</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user