diff --git a/.gitflow b/.gitflow new file mode 100644 index 0000000..472221e --- /dev/null +++ b/.gitflow @@ -0,0 +1,9 @@ +[gitflow] + branch.master = main + branch.develop = develop + prefix.feature = feature/ + prefix.bugfix = bugfix/ + prefix.release = release/ + prefix.hotfix = hotfix/ + prefix.support = support/ + versiontag = v \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 407565e..e405d06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, develop] pull_request: jobs: diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..276f93c --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,20 @@ +# push-sec: block direct/forced pushes to protected branches (Git Flow). +# main + develop are protected: code reaches them only via PR merges +# (and main additionally from release/hotfix merges). Force pushes are +# blocked everywhere. Safe, single-owner escape hatch: ALLOW_PROTECTED_PUSH=1. +if [ "$ALLOW_PROTECTED_PUSH" = "1" ]; then + exit 0 +fi +protected="refs/heads/main refs/heads/develop" +while read -r local_ref local_oid remote_ref remote_oid; do + if [ "$local_ref" = "+${local_ref#+}" ]; then + echo "push-sec: force push blocked ($local_ref). Protected branches never allow force." >&2 + exit 1 + fi + case " $protected " in + *" $remote_ref "*) + echo "push-sec: direct push to $remote_ref blocked. Open a pull request; $remote_ref only receives PR/release merges." >&2 + exit 1 + ;; + esac +done \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index b67403c..e32fa9d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,12 +35,12 @@ day one. `recovery.ts` (12-word BIP39 secret + salted verifier), `transfer.ts`/`lan.ts` (QR/LAN), content loaders (`quran.ts`, `tafsir.ts`, `audio.ts`, `hadith-full.ts`), `privacy.ts` (sensitivity + network manifest), `strings.ts` (AR/EN - dictionary, ~1295 keys), `i18n.ts`. + dictionary, ~1307 keys), `i18n.ts`. - `app/components/` — React UI. `app/components/views/` route views, `app/components/library/` Quran/hadith, `transfer.tsx` (export infra), `profile-scope.tsx`, `login-gate.tsx`. - `app/` — routes: `/`(today), `/account`, `/calendar`, `/insights`, - `/review`, `/deen`, `/library`, `/recovery`. + `/deen`, `/library`, `/recovery`. - `e2e/` — Playwright (serial; `npm run test:e2e` pins `--workers=1`). - `public/data/` — generated bundles (only touched via pipeline). - `docs/`, `DOCUMENTATION.md`, `SECURITY.md`, `GOVERNANCE.md` — ground diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ef6db..a71e998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,33 @@ or architecture-level changes are listed. ## Unreleased +- **الحصاد folded into /deen**: the previous `/review` night-checklist page + now lives as a third "المراجعة / Review" tab inside the Deen journey + (`/deen`, tabs: Today · Library · Review); `/review` permanently + redirects (`308`→`/deen`) so old bookmarks, shared links, and PWA + notification deep-links keep working; nav, sitemap, SW `CORE` (bumped + to `wird-v7`), manifest shortcuts, notification defaults, metadata, + SEO/docs/audit counts, and all static + e2e suites updated. +- Branch security: GitHub-server protection ENABLED on `main` + `develop` + (`gh` API 2026-09-23): require PR + `CI / verify` status check (strict), + linear history, no force pushes, no deletions, no admins bypass — + direct pushes fail server-side (cryptographic signature check left off + until GPG/SSH signing is configured; `git commit -s` sign-off stays a + rule); local `.husky/pre-push` remains as the client-side wall; + `branch-guard` test keeps both documented. +- SEO content pass: homepage FAQ (5 honest long-tail Q&A) + matching + `FAQPage` JSON-LD, descriptive internal links to core routes, and new + regression guards (unique titles/descriptions per route, FAQ parity); + `docs/INDEXING.md` gained the post-index playbook (keyword/CTR/Vitals + owner steps). - Public-launch ops: `docs/INDEXING.md` records the verified live-index state (robots/sitemap/verification file/home metadata all `200`) and the Search Console checklist that actually puts Wird in Google; - `docs/BRANCHING.md` introduces GitHub Flow (protected `main`, - `feat/fix/docs/chore/hotfix` PR branches, signed commits). + **branch system live**: Git Flow with long-lived `main` + `develop` + (both on origin), `feature/bugfix/release/hotfix/support/*` short-lived + branches, `.gitflow` config, CI now also gates `develop`, + `docs/BRANCHING.md` documents it all (protection of both branches is an + owner action — `gh`/PAT not available from the dev env). - Polish + presence: README screenshots generated from the app (`docs/assets/shots/`, via `scripts/shot-readme.mjs`); raster app icons (180/192/512 PNGs derived 1:1 from `icon.svg` via diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 33c3810..b58aab1 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -100,8 +100,7 @@ husky pre-commit (lint-staged) + commit-msg hooks. | `/` | `app/page.tsx` | Today: hero, habits, rescue plan, goals, night review | | `/calendar` | `app/calendar/` | 30-day month grid, Friday plan | | `/insights` | `app/insights/` | Real stats, balance radar, coach brief, Hijri year | -| `/review` | `app/review/` | End-of-day checklist (tri-state) + score + mood | -| `/deen` | `app/deen/` | Deen journey: levels, quests, deeds, reflections, streaks (see §3) | +| `/deen` | `app/deen/` | Deen journey — tabs: Today · Library · Review (see §3) | | `/library` | `app/library/` | Tabs: Adhkar · Quran · Hadith · Paths · Dreams | | `/account` | `app/account/` | Profile, backup, transfer, reminders, times, theme | | `/recovery` | `app/recovery/` | Last-resort recovery: inspect, emergency export, restore, surgical reset | @@ -114,6 +113,10 @@ Shared chrome (sidebar, bottom nav, header, zikr dock, login gate) lives in (`WirdProvider` + `useWird()`). Views that need props live in `app/components/views/`. +> `/review` was folded into `/deen` (Review tab) and now permanently +> redirects — old bookmarks, shared links, and PWA notification deep-links +> keep working. + ## 3. Features by route **Today (`/`)** — hero ring (live %), day-mode pills, focus card, quick tools, @@ -126,10 +129,11 @@ reflection (persisted textarea), tiered return screen after ≥3 absent days adaptive companion card (one ranked guidance + verified verse/hadith, see §14), kids quests, qada / fasting / breaker cards, Ramadan banner in Ramadan. -**Review (`/review`)** — auto-built checklist from today's real data plus six -heart-check items; tri-state (done/partial/missed); mood + gratitude; weighted -score saved immutably per day (`wird-reviews-v1`, merged into history); -one-line night context for strong/low/return days (see §14). +**Review (Deen journey, `/deen` → Review tab)** — auto-built checklist from +today's real data plus six heart-check items; tri-state (done/partial/missed); +mood + gratitude; weighted score saved immutably per day (`wird-reviews-v1`, +merged into history); one-line night context for strong/low/return days +(see §14). Retains tab state in memory only, like the other deen tabs. **Insights (`/insights`)** — period pills (1–365d buckets), weekly bars, 4 metrics, coach brief (at-risk → neglect → pace → lift + praise), 6-axis diff --git a/MASTER_AUDIT.md b/MASTER_AUDIT.md index a60890d..6de3c29 100644 --- a/MASTER_AUDIT.md +++ b/MASTER_AUDIT.md @@ -30,8 +30,8 @@ the JSONs). This run's numbers: `AUDIT_REPORT.md`. Frozen snapshot: ## 5–7. Routes, journeys, navigation — PASS (+1 FIXED) -- Inventory: 10 routes (`/`, account, calendar, insights, library, - recovery, review, private-plans, terms, privacy); static check +- Inventory: 10 routes (`/`, account, calendar, insights, deen, library, + recovery, private-plans, terms, privacy); static check `app/lib/__tests__/audit.test.ts` asserts every `href="/…"` and `router.push("/…")` resolves, sitemap lists the public routes, and the SW `CORE` shell matches existing routes (a stale shell entry would diff --git a/README.md b/README.md index 472b556..25b56d0 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Node 22 (`.nvmrc`). CI (`.github/workflows/ci.yml`) runs typecheck → lint → - **اليوم**: صلوات وأوراد، خطة إنقاذ، جلسة قرآن بمؤقت، بطاقات الجمعة/الوتر، عادات مخصصة، تحديات، عهود، ركن الصغار، قضاء الفوائت، الصيام، كسر العادات، وضع رمضان التلقائي. - **المرشد التكيفي**: بطاقة واحدة ذكية (26 حالة: عودة، تعثر، زخم، إرهاق…) مع آية/حديث موثّقين من مجموعات محلية، ومسار توبة اختياري بعد الغياب الطويل — دون أحكام ودون ادعاء معرفة القلوب. -- **الحصاد** (`/review`): مراجعة ليلية tri-state مع درجة ومزاج وامتنان — تُحفظ كسجل يومي، مع سطر سياق ليلي. +- **الدين** (`/deen`): رحلة اليوم (أذكار ومهام وأعمال صالحة) + مكتبة وتعلم + مراجعة ليلية tri-state مع درجة ومزاج وامتنان — تُحفظ كسجل يومي، مع سطر سياق ليلي. - **التقدّم** (`/insights`): طبقات تحليلية محلية قابلة للتفسير (اتجاهات، عبادة-عبادة، قرآن وحفظ، أذكار، أهداف وتحديات وعهود، عودات واستمرارية، خريطة نشاط، مراجعة شهرية/سنوية، إنجازات) + مدى مخصص للمقارنة — لا بيانات مفتعلة أبدًا. - **المكتبة** (`/library`): قارئ قرآن كامل دون إنترنت (بحث، علامات، حفظ بتاريخ مراجعة)، مختارات الكتب التسعة + الأربعون النووية، مسارات علمية ٤ مستويات × ٨ علوم، لوحة الأحلام. - **العودة**: شاشة رجوع متدرجة حسب عمق الغياب (3/7/14/30/90 يومًا) + سلّم تنبيهات محلية + مواقيت يدوية بعدّادات حية + وضع المسجد. diff --git a/app/additions.css b/app/additions.css index 86c0e8a..8692703 100644 --- a/app/additions.css +++ b/app/additions.css @@ -2245,3 +2245,64 @@ video { animation: none; } } + +/* SEO FAQ + internal links (public landing block at page bottom). */ +.seo-faq { + margin-block: 40px 8px; + border-block-start: 1px solid var(--border, #d8dfd5); + padding-block-start: 16px; + max-inline-size: 720px; + margin-inline: auto; +} + +.seo-faq h2, +.seo-links h3 { + font-size: 17px; + margin: 0 0 10px; +} + +.seo-faq details { + border-block-end: 1px solid var(--border, #d8dfd5); +} + +.seo-faq summary { + cursor: pointer; + font-weight: 600; + color: var(--ink, #16211c); + padding-block: 10px; +} + +.seo-faq details[open] summary { + margin-block-end: 4px; +} + +.seo-faq details p { + margin: 0 0 12px; + color: var(--muted, #5c6b63); + font-size: 13px; + line-height: 1.7; +} + +.seo-links { + margin-block-start: 20px; +} + +.seo-links ul { + list-style: none; + margin: 0; + padding-inline-start: 0; + display: flex; + flex-wrap: wrap; + gap: 6px 18px; +} + +.seo-links a { + color: var(--link, #14725d); + text-underline-offset: 3px; +} + +.seo-links a:focus-visible { + outline: 2px solid var(--focus, #1c6b4f); + outline-offset: 2px; + border-radius: 4px; +} diff --git a/app/components/views/deen-page.tsx b/app/components/views/deen-page.tsx index 708d276..e7ce734 100644 --- a/app/components/views/deen-page.tsx +++ b/app/components/views/deen-page.tsx @@ -1,16 +1,18 @@ -// Deen page shell (STEP 10): Today/Library tabs over the two deen -// views. Tab selection is in-memory only (never in the URL), so no -// tracking state can leak into history, titles, or shared links. +// Deen page shell (STEP 10): Today/Library/Review tabs over the three deen +// views. Tab selection is in-memory only (never in the URL), so no tracking +// state can leak into history, titles, or shared links. Review is the folded +// /review (الحصاد) checklist, reachable now only through /deen. "use client"; import { useState } from "react"; import { useT } from "../../lib/i18n"; import { DeenTodayView } from "./deen-today"; import { DeenLibraryView } from "./deen-library"; +import { ReviewTab } from "./review-tab"; export function DeenPage() { const t = useT(); - const [tab, setTab] = useState<"today" | "library">("today"); + const [tab, setTab] = useState<"today" | "library" | "review">("today"); return ( <>
@@ -32,8 +34,23 @@ export function DeenPage() { > {t("dn.library")} +
- {tab === "today" ? : } + {tab === "today" ? ( + + ) : tab === "library" ? ( + + ) : ( + + )} ); } diff --git a/app/review/page.tsx b/app/components/views/review-tab.tsx similarity index 92% rename from app/review/page.tsx rename to app/components/views/review-tab.tsx index a837e73..f20b3fe 100644 --- a/app/review/page.tsx +++ b/app/components/views/review-tab.tsx @@ -1,14 +1,20 @@ -// Review page (/review): tri-state nightly checklist + score/mood/gratitude. -// Submits write BOTH the per-day review record (enveloped) and the history -// merge. Includes one honest night-context line for strong/low/return days. - +// Review tab (Deen journey, /deen): tri-state nightly checklist + score/mood/ +// gratitude. Submits write BOTH the per-day review record (enveloped) and the +// history merge. Includes one honest night-context line for strong/low/return +// days. Moved here from /review (now a permanent redirect to /deen). "use client"; import { useEffect, useMemo, useState } from "react"; -import { dayId, hijriParts, loadFromStorage, QURAN_GOAL_PAGES, saveToStorage } from "../lib/wird"; -import { assessUser, selectGuidance } from "../lib/companion"; -import { useT } from "../lib/i18n"; -import { useWird } from "../components/wird-store"; +import { + dayId, + hijriParts, + loadFromStorage, + QURAN_GOAL_PAGES, + saveToStorage, +} from "../../lib/wird"; +import { assessUser, selectGuidance } from "../../lib/companion"; +import { useT } from "../../lib/i18n"; +import { useWird } from "../wird-store"; type Status = "done" | "partial" | "missed"; type Mood = "good" | "ok" | "low" | null; @@ -47,7 +53,7 @@ function loadReviews(): Record { return loadFromStorage>("wird-reviews-v1", {}); } -export default function ReviewPage() { +export function ReviewTab() { const t = useT(); const { done, @@ -118,8 +124,8 @@ export default function ReviewPage() { const gradeTitle = loadedScore >= 85 ? t("rv.g85") : loadedScore >= 60 ? t("rv.g60") : t("rv.gLow"); - // Night context (2.39): one honest line for the states that matter at - // night — no card, no logging (Today owns the guidance log). + // Night context: one honest line for the states that matter at night — no + // card, no logging (Today owns the guidance log). const nightLine = useMemo(() => { try { const todayId = dayId(); diff --git a/app/deen/page.tsx b/app/deen/page.tsx index 51ccfcd..0cfa0b9 100644 --- a/app/deen/page.tsx +++ b/app/deen/page.tsx @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // Deen route (/deen): the STEP 10 journey space — today dashboard plus -// library/learning tabs (in-memory tab state, no URL params, static -// generic title). Server component; views own all client state. +// library/learning and nightly review tabs (in-memory tab state, no URL +// params, static generic title). Server component; views own all client +// state. Review was folded here from /review (permanent redirect). import type { Metadata } from "next"; import { DeenPage } from "../components/views/deen-page"; @@ -9,11 +10,11 @@ export const metadata: Metadata = { // Discreet by design: short generic title, one plain description of // the public feature — never user data, never scores or ranks. title: "دين", - description: "رحلة الدين: مستويات تتبع ومهام وأعمال صالحة وتأمل خاص.", + description: "رحلة الدين: متابعة اليوم، أذكار وتعلم، ومراجعة ليلية خاصة.", alternates: { canonical: "/deen" }, openGraph: { title: "دين", - description: "رحلة الدين: مستويات تتبع ومهام وأعمال صالحة وتأمل خاص.", + description: "رحلة الدين: متابعة اليوم، أذكار وتعلم، ومراجعة ليلية خاصة.", url: "/deen", images: [{ url: "/opengraph-image", width: 1200, height: 630, alt: "Wird — Daily Companion" }], }, diff --git a/app/lib/__tests__/audit.test.ts b/app/lib/__tests__/audit.test.ts index 128c7bf..4c81ab5 100644 --- a/app/lib/__tests__/audit.test.ts +++ b/app/lib/__tests__/audit.test.ts @@ -40,7 +40,6 @@ describe("route inventory (audit 4-5)", () => { "/insights", "/library", "/recovery", - "/review", "/private-plans", "/terms", "/privacy", @@ -66,7 +65,6 @@ describe("route inventory (audit 4-5)", () => { for (const r of [ "/calendar", "/insights", - "/review", "/deen", "/library", "/account", @@ -80,7 +78,6 @@ describe("route inventory (audit 4-5)", () => { "/", "/calendar", "/insights", - "/review", "/deen", "/library", "/account", diff --git a/app/lib/__tests__/branch-guard.test.ts b/app/lib/__tests__/branch-guard.test.ts new file mode 100644 index 0000000..3703936 --- /dev/null +++ b/app/lib/__tests__/branch-guard.test.ts @@ -0,0 +1,28 @@ +// Branch-security guard: the pre-push hook that blocks direct/forced +// pushes to protected branches must exist and stay wired, and the +// branch doc keeps the matching server-side settings. If this fails, +// secured branches silently regressed. +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const ROOT = join(__dirname, "..", "..", ".."); +const read = (p: string): string => readFileSync(join(ROOT, p), "utf8"); + +describe("branch security", () => { + it("pre-push hook exists and blocks protected branches + force pushes", () => { + const hook = read(".husky/pre-push"); + expect(hook, "blocks main").toContain("refs/heads/main"); + expect(hook, "blocks develop").toContain("refs/heads/develop"); + expect(hook, "blocks force push refs").toContain("+${local_ref#+}"); + expect(hook, "escape hatch documented").toContain("ALLOW_PROTECTED_PUSH"); + }); + it("branching doc keeps the server-side settings and the check name", () => { + const doc = read("docs/BRANCHING.md"); + expect(doc, "pr required").toMatch(/require a pull request/i); + expect(doc, "status check required").toMatch(/require status checks/i); + expect(doc, "ci check named").toContain("verify"); + expect(doc, "no force pushes").toMatch(/do not allow force pushes/i); + expect(doc, "no deletions").toMatch(/do not allow deletions/i); + }); +}); diff --git a/app/lib/__tests__/seo.test.ts b/app/lib/__tests__/seo.test.ts index c954403..94e53cd 100644 --- a/app/lib/__tests__/seo.test.ts +++ b/app/lib/__tests__/seo.test.ts @@ -40,16 +40,7 @@ describe("titles, descriptions, canonicals", () => { expect(seen.has(t), `duplicate title "${t}" in ${rel} (first: ${seen.get(t)})`).toBe(false); seen.set(t, rel); } - for (const r of [ - "calendar", - "insights", - "review", - "library", - "account", - "terms", - "privacy", - "deen", - ]) { + for (const r of ["calendar", "insights", "library", "account", "terms", "privacy", "deen"]) { expect( [...seen.values()].some((v) => v.startsWith(`${r}/`)), `route ${r} titled`, @@ -64,16 +55,7 @@ describe("titles, descriptions, canonicals", () => { return read(`app/${r}/page.tsx`); } }; - for (const r of [ - "calendar", - "insights", - "review", - "library", - "account", - "terms", - "privacy", - "deen", - ]) { + for (const r of ["calendar", "insights", "library", "account", "terms", "privacy", "deen"]) { expect(srcOf(r), `${r} description`).toMatch(/description:\s*"/); } // Recovery is fully discreet: no metadata export at all, so the @@ -99,6 +81,53 @@ describe("titles, descriptions, canonicals", () => { }); }); +describe("long-tail content, internal links, duplicate content", () => { + it("home carries a visible FAQ block with matching FAQPage JSON-LD", () => { + const src = read("app/page.tsx"); + expect(src, "visible faq section").toContain('className="seo-faq"'); + expect(src, "JsonLD FAQ type").toContain('"@type": "FAQPage"'); + expect(src, "JsonLD answer shape").toContain("acceptedAnswer"); + // Same strings render both the on-page
list and the schema, + // so the two can never drift apart. + expect( + src.match(/\["q\d", "a\d"\]/g) ?? [], + "faq pairs in visible block + schema", + ).toHaveLength(10); + expect(src).toContain("seo.faq.${q}"); + expect(src).toContain("seo.faq.${a}"); + const strings = read("app/lib/strings.ts"); + expect(strings.match(/seo\.faq\.q[1-5]/g) ?? [], "ar+en q keys").toHaveLength(10); + expect(strings.match(/seo\.faq\.a[1-5]/g) ?? [], "ar+en a keys").toHaveLength(10); + }); + it("home links descriptively to every core route", () => { + const src = read("app/page.tsx"); + for (const route of ["/library", "/deen", "/calendar", "/insights"]) { + expect(src, `internal link ${route}`).toContain(`Link href="${route}"`); + } + expect(src, "descriptive anchor text (nav.* labels)").toContain('t("nav.library")'); + expect(src).not.toContain('href="/library">click here'); + }); + it("listed routes never share a description (no cannibalized queries)", () => { + const srcOf = (r: string): string => { + try { + return read(`app/${r}/layout.tsx`); + } catch { + return read(`app/${r}/page.tsx`); + } + }; + const seen = new Map(); + for (const r of ["calendar", "insights", "library", "account", "terms", "privacy", "deen"]) { + const d = srcOf(r).match(/description:\s*"([^"]+)"/); + if (!d) continue; + expect( + seen.has(d[1] ?? ""), + `duplicate description on ${r} (first: ${seen.get(d[1] ?? "")})`, + ).toBe(false); + seen.set(d[1] ?? "", r); + } + }); +}); + describe("robots, sitemap, verification file", () => { it("robots allows crawling and references the sitemap", () => { const src = read("app/robots.ts"); diff --git a/app/lib/notify.ts b/app/lib/notify.ts index ffe15d4..282f69c 100644 --- a/app/lib/notify.ts +++ b/app/lib/notify.ts @@ -41,7 +41,7 @@ export async function ensurePermission(): Promise { } } -export function fireNotification(title: string, body: string, url = "/review"): void { +export function fireNotification(title: string, body: string, url = "/deen"): void { try { if (!canNotify() || Notification.permission !== "granted") return; if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { diff --git a/app/lib/strings.ts b/app/lib/strings.ts index 40b215d..9a6533f 100644 --- a/app/lib/strings.ts +++ b/app/lib/strings.ts @@ -8,7 +8,6 @@ const AR: Record = { "nav.today": "اليوم", "nav.calendar": "التقويم", "nav.insights": "التقدّم", - "nav.review": "الحصاد", "nav.library": "الأذكار", "nav.account": "حسابي", "nav.skip": "تخطَّ إلى المحتوى", @@ -160,6 +159,23 @@ const AR: Record = { "night.journalPh": "اكتب لنفسك كلمة طيبة…", "night.journalNote": "هذه المساحة لك وحدك، ولا تدخل في الإحصاءات.", "footer.verse": "﴿ وَاذْكُر رَّبَّكَ كَثِيرًا وَسَبِّحْ بِالْعَشِيِّ وَالْإِبْكَارِ ﴾", + "seo.faq.title": "أسئلة شائعة عن وِرد", + "seo.faq.q1": "هل يحتاج تطبيق وِرد إلى اتصال بالإنترنت؟", + "seo.faq.a1": + "لا، يعمل وِرد دون إنترنت؛ الصلوات والأذكار والقرآن والبيانات كلها على جهازك. الإنترنت مطلوب فقط عند طلب تفسير أو حديث أو صوتية غير محمّلة مسبقًا.", + "seo.faq.q2": "أين تُخزَّن بياناتي، وهل تُرفع إلى خادم؟", + "seo.faq.a2": + "تُحفظ بياناتك على جهازك فقط (تخزين محلي)، ولا تُرسل إلى أي خادم. لا حسابات إلزامية، ولا أدوات تتبّع، ولا تحليلات خارجية.", + "seo.faq.q3": "هل أستطيع متابعة الصلوات والأذكار والقرآن في تطبيق واحد؟", + "seo.faq.a3": + "نعم، يجمع وِرد الصلوات والأذكار والقرآن والأهداف والمراجعة اليومية في شاشة اليوم، مع إحصاءات تفصّلها سعيك الصحيح دون كشفها لأحد.", + "seo.faq.q4": "هل وِرد عربي ويدعم اتجاه RTL؟", + "seo.faq.a4": + "نعم، وِرد عربي أولًا بواجهة كاملة الاتجاه من اليمين لليسار، مع إمكانية التبديل إلى الإنجليزية في أي وقت.", + "seo.faq.q5": "هل يعمل وِرد كتطبيق مستقل حتى دون إنترنت؟", + "seo.faq.a5": + "نعم، يمكنك تثبيته كتطبيق (PWA) من المتصفح؛ يعمل دون إنترنت ويحفظ بياناتك بين الجلسات دون حساب.", + "seo.links.title": "استكشف التطبيق أكثر", "minplan.title": "خطة الحد الأدنى مفعّلة", "minplan.sub": "الصلوات، ذكر قصير، آية واحدة، استغفار ١٠ مرات، والوتر. هذا يكفي لليوم.", "minplan.cancel": "إلغاء", @@ -1124,6 +1140,7 @@ const AR: Record = { "dn.ach.disciplined": "المنضبط", "dn.ach.disciplinedD": "حافظت على قاعدة شخصية 7 أيام.", "dn.library": "المكتبة والتعلم", + "dn.review": "المراجعة", "dn.mustahabb": "النمو بالمستحبات", "dn.mustahabbSub": "ممارسات مستحبة مجمعة للنمو. كل عنصر يوضح إن كان موثقا أو هدفا شخصيا — والأحكام تختلف بين المذاهب.", @@ -1323,7 +1340,6 @@ const EN: Record = { "nav.today": "Today", "nav.calendar": "Calendar", "nav.insights": "Progress", - "nav.review": "Review", "nav.library": "Adhkar", "nav.account": "Account", "nav.skip": "Skip to content", @@ -1475,6 +1491,23 @@ const EN: Record = { "night.journalPh": "Write yourself something kind…", "night.journalNote": "Yours alone — excluded from stats.", "footer.verse": "﴿ وَاذْكُر رَّبَّكَ كَثِيرًا وَسَبِّحْ بِالْعَشِيِّ وَالْإِبْكَارِ ﴾", + "seo.faq.title": "Frequently asked questions about Wird", + "seo.faq.q1": "Does Wird need an internet connection?", + "seo.faq.a1": + "No - Wird works offline; prayers, adhkar, Quran and your data all live on your device. Internet is only needed when you request a tafsir, hadith, or audio not bundled in advance.", + "seo.faq.q2": "Where is my data stored, and is it uploaded to a server?", + "seo.faq.a2": + "Your data is stored on your device only (local storage) and is never sent to a server. No mandatory accounts, no tracking, no third-party analytics.", + "seo.faq.q3": "Can I track prayers, adhkar and Quran in one app?", + "seo.faq.a3": + "Yes - Wird brings prayers, adhkar, Quran, goals and the daily review into one Today screen, with statistics that stay private to you.", + "seo.faq.q4": "Is Wird in Arabic and does it support RTL?", + "seo.faq.a4": + "Yes - Wird is Arabic-first with a full right-to-left interface, and you can switch to English at any time.", + "seo.faq.q5": "Does Wird work as a standalone app even offline?", + "seo.faq.a5": + "Yes - you can install it as a PWA from the browser; it works offline and keeps your data between sessions without an account.", + "seo.links.title": "Explore the app further", "minplan.title": "Minimum plan active", "minplan.sub": "Prayers, short dhikr, one verse, 10 istighfar, and Witr. Enough for today.", "minplan.cancel": "Cancel", @@ -2462,6 +2495,7 @@ const EN: Record = { "dn.ach.disciplined": "Disciplined", "dn.ach.disciplinedD": "Kept a personal discipline rule for 7 days.", "dn.library": "Library and learning", + "dn.review": "Review", "dn.mustahabb": "Mustahabb growth", "dn.mustahabbSub": "Recommended practices grouped for growth. Each item shows whether it is sourced or a personal goal - rulings vary by school.", diff --git a/app/lib/wird.ts b/app/lib/wird.ts index 316e755..218b57c 100644 --- a/app/lib/wird.ts +++ b/app/lib/wird.ts @@ -213,7 +213,6 @@ export const NAV_ITEMS: [string, string, string][] = [ ["today", "⌂", "اليوم"], ["calendar", "▦", "التقويم"], ["insights", "↗", "التقدّم"], - ["review", "☾", "الحصاد"], ["deen", "✦", "الدين"], ["library", "◈", "الأذكار"], ["account", "◌", "حسابي"], @@ -223,7 +222,6 @@ export const NAV_HREFS: Record = { today: "/", calendar: "/calendar", insights: "/insights", - review: "/review", deen: "/deen", library: "/library", account: "/account", diff --git a/app/page.tsx b/app/page.tsx index 30286da..9e4c9b3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -12,6 +12,7 @@ import { fastLabel, PRAYER_ID } from "./lib/daymode"; import { EditModal } from "./components/edit-modal"; import { NowView } from "./components/views/now"; import { PrayerArc } from "./components/prayer-arc"; +import Link from "next/link"; // Inline vault unlock (locked reflections): password + button only, generic // failure message. Session-scoped — reload locks again by design. @@ -429,7 +430,7 @@ export default function TodayPage() { const handleGuideAction = (kind: string) => { try { if (kind === "rescue") scrollToId("rescue-plan"); - else if (kind === "review") router.push("/review"); + else if (kind === "review") router.push("/deen"); else if (kind === "core") { setMinimumPlan(true); scrollToId("rescue-plan"); @@ -1794,6 +1795,65 @@ export default function TodayPage() { onDelete={deleteEditing} /> )} +
+

{t("seo.faq.title")}

+ {( + [ + ["q1", "a1"], + ["q2", "a2"], + ["q3", "a3"], + ["q4", "a4"], + ["q5", "a5"], + ] as Array<[string, string]> + ).map(([q, a]) => ( +
+ {t(`seo.faq.${q}`)} +

{t(`seo.faq.${a}`)}

+
+ ))} + +