Audit fixes: VIP self-grant, draft leak, onboarding flag, hero personalization, gated downloads
Deploy GitHub Pages / deploy (push) Canceled after 0s

This commit is contained in:
AHMED
2026-10-09 16:20:55 +03:00
parent c34611dc36
commit 5ee706ebd6
150 changed files with 24054 additions and 0 deletions
@@ -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>
</>
)
}