- launch.ts policy: LAUNCH_MODE=free_early_access default; effective access = published && (free || early); intended tier (access) never rewritten - Billing off server-side: /api/subscribe 503 honest copy; subscribe CTAs render disabled 'قريباً' from first paint; /subscription early-access panel + /api/notify (LaunchNotifications) with honest 503 when mail transport off - Signup trust: verification email at signup (Users afterChange), no confirm-password, 429 message fidelity, typo suggestion, live password hint - Agent pipeline: pickFields strips access/readingMinutes, sourceId dedup, secret-scan 422 (pattern names only), string-content rejected (was silently replaced), publish stays explicit opt-in - lexical.ts: root unwrap fix — lexicalToText/countLexicalWords returned 0 for real serialized trees (secret scan + readingMinutes were inert) - llms-full.txt: fetchIndexDocs(true) — bodies were never selected - Content honesty: contentType/authorName/sourceId fields + migration, readingMinutes computed at write (160 wpm) + backfill script, byline, 'حُدّث' freshness - SEO/AI-SEO: metadata rewrite (published-only, og:image 1200x630), Article + BreadcrumbList + FAQPage + Organization JSON-LD, isAccessibleForFree honest, table renderer, robots AI-crawler policy (10 named bots), llms.txt + llms-full.txt, /topics/* hubs + sitemap, noindex auth pages, www 301 - Ops: backup-db.sh (restore rehearsed on VPS), docs copies (launch readiness + marketing audit + owner-inputs for L-03), GLOSSARY.md entries Verified: tsc --noEmit 0, npm run build 0, local smoke (ghost VIP read, subscribe/notify/agent APIs, signup token, 429 on 6th, llms/topics/sitemap, og:image/JSON-LD)
61 lines
2.1 KiB
TypeScript
61 lines
2.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
/* Early-access notification signup (L-01). 200 → success copy;
|
|
503/4xx → the server's Arabic error verbatim; 5xx/network → generic fallback. */
|
|
export default function NotifyForm() {
|
|
const [email, setEmail] = useState('')
|
|
const [pending, setPending] = useState(false)
|
|
const [done, setDone] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
async function submit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
if (pending) return
|
|
setPending(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch('/api/notify', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email }),
|
|
})
|
|
const data = (await res.json().catch(() => null)) as { error?: string; alreadySubscribed?: boolean } | null
|
|
if (res.ok) {
|
|
setDone(data?.alreadySubscribed ? 'بريدك مسجَّل لدينا بالفعل — سنراسلك عند تفعيل العضوية.' : 'تم! سنراسلك عند تفعيل العضوية.')
|
|
} else {
|
|
setError(data?.error ?? 'تعذر الحفظ، حاول مرة أخرى.')
|
|
}
|
|
} catch {
|
|
setError('تعذر الحفظ، حاول مرة أخرى.')
|
|
} finally {
|
|
setPending(false)
|
|
}
|
|
}
|
|
|
|
if (done) {
|
|
return <p className="notify-done" role="status">{done}</p>
|
|
}
|
|
|
|
return (
|
|
<form className="notify-form" onSubmit={submit}>
|
|
<label className="sr-only" htmlFor="notify-email">البريد الإلكتروني</label>
|
|
<input
|
|
id="notify-email"
|
|
type="email"
|
|
name="email"
|
|
required
|
|
dir="ltr"
|
|
placeholder="you@example.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
<button className="btn btn-primary" type="submit" disabled={pending || email.trim() === ''}>
|
|
{pending ? 'جارٍ الحفظ…' : 'أعلمني عند تفعيل العضوية'}
|
|
</button>
|
|
{error && <p className="notify-error" role="alert">{error}</p>}
|
|
</form>
|
|
)
|
|
}
|