Free early-access launch: LAUNCH_MODE policy, CRO, pipeline hardening, AI-SEO, ops
- 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)
This commit is contained in:
@@ -1,12 +1,52 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/* Resend button + status — real /api/auth/resend-verification endpoint.
|
||||
Without SMTP configured the API answers 503 with a clear message. */
|
||||
export default function VerifyActions({ verified }: { verified: boolean }) {
|
||||
Without SMTP configured the API answers 503 with a clear message.
|
||||
«البريد غير صحيح؟» — collapsed change-email affordance: PATCH /api/users/:id
|
||||
(self-update allowed in Users.ts access) then resend; 400 → duplicate-email
|
||||
copy. Hidden without a session (no id → nothing to patch). */
|
||||
export default function VerifyActions({ verified, email, userId }: { verified: boolean; email: string | null; userId: number | null }) {
|
||||
const [status, setStatus] = useState('')
|
||||
const [newEmail, setNewEmail] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (email) {
|
||||
setNewEmail(email)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const saved = sessionStorage.getItem('anwarpi:signup-email')
|
||||
if (saved) setNewEmail(saved)
|
||||
} catch { /* private mode */ }
|
||||
}, [email])
|
||||
|
||||
async function changeEmail() {
|
||||
if (busy || !userId) return
|
||||
setBusy(true)
|
||||
setStatus('جارٍ التحديث…')
|
||||
try {
|
||||
const patchRes = await fetch(`/api/users/${userId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: newEmail.trim() }),
|
||||
})
|
||||
if (!patchRes.ok) {
|
||||
setStatus(patchRes.status === 400 ? 'هذا البريد مسجَّل لدينا.' : 'تعذّر تحديث البريد، حاول مرة أخرى.')
|
||||
return
|
||||
}
|
||||
const resendRes = await fetch('/api/auth/resend-verification', { method: 'POST' })
|
||||
const data = await resendRes.json().catch(() => null) as { error?: string; already?: boolean } | null
|
||||
setStatus(resendRes.ok || data?.already ? 'أرسلنا رسالة تأكيد جديدة إلى بريدك المحدَّث.' : data?.error || 'تم تحديث البريد لكن تعذّر الإرسال الآن.')
|
||||
} catch {
|
||||
setStatus('تعذّر الاتصال، حاول مرة أخرى.')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -19,7 +59,7 @@ export default function VerifyActions({ verified }: { verified: boolean }) {
|
||||
button.disabled = true
|
||||
setStatus('جارٍ الإرسال…')
|
||||
const res = await fetch('/api/auth/resend-verification', { method: 'POST' })
|
||||
const data = await res.json().catch(() => null)
|
||||
const data = await res.json().catch(() => null) as { error?: string; already?: boolean } | null
|
||||
if (res.ok || data?.already) {
|
||||
setStatus('أرسلنا الرسالة من جديد — افحص صندوق الوارد.')
|
||||
} else {
|
||||
@@ -33,6 +73,17 @@ export default function VerifyActions({ verified }: { verified: boolean }) {
|
||||
)}
|
||||
</div>
|
||||
<p className="status" id="resendStatus" role="status" aria-live="polite">{status}</p>
|
||||
{userId && !verified && (
|
||||
<details className="change-email" data-od-id="change-email">
|
||||
<summary>البريد غير صحيح؟</summary>
|
||||
<div className="change-email-row">
|
||||
<input className="input" type="email" inputMode="email" dir="ltr" value={newEmail} onChange={e => setNewEmail(e.target.value)} aria-label="البريد الإلكتروني الصحيح" />
|
||||
<button className="btn btn-secondary" type="button" onClick={changeEmail} disabled={busy || newEmail.trim() === ''}>
|
||||
{busy ? 'جارٍ التحديث…' : 'حدّث البريد وأعد الإرسال'}
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user