# thinkera.academy

**On a server:** Apache in front of `127.0.0.1:8890`, with `SITE_URL=https://thinkera.academy` — see [`deploy/`](../deploy/README.md).

Arabic-first (RTL) course catalogue and marketing site for Thinkera Academy, with an English mirror.
**Version 2 (18 September 2026): a dynamic site.** Next.js 16 + Payload CMS 3.90 + PostgreSQL 17, in Docker.
Every word, course, price and image is edited in the CMS, and the change shows on the site immediately.
The static version (plain HTML generated by `build.py`) is kept whole in `_archive/2026-09-18-static-site/`.

| Address | What it is |
|---|---|
| http://localhost:8890 | the site in Arabic (`/courses`, `/courses/<slug>`, `/pricing`, `/about`) |
| http://localhost:8890/en | the same site in English (`/en/courses`, …) |
| http://localhost:8890/admin | the CMS (sign in with `ADMIN_EMAIL` / `ADMIN_PASSWORD` from `.env`) |
| http://localhost:8890/brand/index.html | the identity guide (static, unchanged) |

```bash
cd "D:/OneDrive - DolfTech/Thinkera/thinkera.academy"
docker compose up -d --build        # build and start the site and its database
docker compose logs -f web          # watch the site
docker compose down                 # stop (content stays in the pgdata and media volumes)
```

Nothing is installed on the machine: `node_modules` exists only inside the Docker images, never in OneDrive.

## Editing the content (CMS)

Sign in at `/admin`. The CMS speaks Arabic or English (each editor chooses it under their account). Every text
field has an Arabic and an English value; switch the language from the language menu at the top of the edit screen.
Arabic is the default: an English field left empty shows the Arabic text.

| In the CMS | Holds | Shows on |
|---|---|---|
| **Courses** | the five courses: title, tagline, facts, what's included, outcomes, units, status, price, preview image, free-lesson video link, link to the course on the platform | the course cards, `/courses/<slug>`, the footer, the form's course list |
| **Home page** | hero, figures, "beyond the book", layers, the three steps, the Egyptian scene, units, team, FAQ | `/` |
| **Courses page** | its heading, and every label of the course page | `/courses` and each course page |
| **Pricing page** | plans, the interest form's labels, role options, thank-you and error messages | `/pricing` |
| **About page** | lead, principles, sections (the team cards come from the home page) | `/about` |
| **Site settings** | name, contact, platform address, menu, button labels, status labels, currency, footer | every page |
| **Interest requests** | every «سجّل اهتمامك» form sent, with a follow-up status (new, contacted, enrolled, closed) | CMS only |
| **Images** | uploaded images; each needs an alternative text in both languages | wherever they are chosen |

**Enrolment goes to the platform.** A course with *Course address on the learning platform* set (tab "Price and
enrolment") shows «اشترك في الدورة» and sends the student to that page, where they sign up and pay with InstaPay.
Now set for the English-school baccalaureate course: `http://localhost:8881/enrol/index.php?id=2`. A course
without it shows «سجّل اهتمامك» and opens the interest form, preselected on that course. The header shows
«دخول المنصة» when *Learning platform address* is set in Site settings.

**Interest form.** Saved to *Interest requests* after checks on the server: a name, a valid email, a plausible
mobile number if one is given, length limits, and a hidden field that silently drops bots. Nobody outside the CMS
can read the requests (`/api/leads` answers 403 without a signed-in editor).

## Addresses

- Arabic at the root, English under `/en`; `/ar/…` redirects to `/…` so each page has one address.
- The old static addresses still work: `/course/bacc-en.html` → `/courses/bacc-en`, `/en/pricing.html` →
  `/en/pricing`, and so on (permanent redirects in `next.config.ts`).
- `/sitemap.xml` lists every page in both languages (a new course appears there by itself); `/robots.txt`
  keeps crawlers out of `/admin` and `/api`.
- An address that does not exist shows a bilingual not-found page inside the site frame.

## How it is built

```
src/
  payload.config.ts        CMS: languages (ar default, en), collections, globals, PostgreSQL, first-run seed
  collections/             Courses, Leads (interest requests), Media, Users (editors)
  globals/index.ts         Home, Catalogue (courses page), Pricing, About, Settings
  fields.ts                builders for bilingual fields
  app/(frontend)/[locale]/ the pages; each reads the CMS on every request
  app/(payload)/           the CMS screens and API (generated by Payload; do not edit)
  components/              Shell (header and footer), Course (card, status badge, unit list), admin/Brand
  proxy.ts                 serves Arabic at / and English at /en
  seed/index.ts            loads seed/content/*.json and seed/images once, into an empty database
  migrations/              the database schema, one file per change
public/brand, public/assets  the identity files, tokens.css and site.css, served as they are
```

**Contact.** The footer and the pricing page show the same block: the WhatsApp number (with its icon, opening a
chat with a first line already typed), the support address and the channel. All of it is CMS content —
Settings → التواصل: the number with its country code, the first line of the chat, and the sentence above them.

**Google Analytics.** `GANALYTICSCODE` in `.env` (the measurement id) puts the gtag snippet in every page's
head; empty means no tag at all. The pages render per request, so a change needs `docker compose up -d web`,
not a rebuild.

**The Arabic is one voice** (20 September 2026): friendly Egyptian, the way a student speaks, with the marketing
kept precise — no formal-Arabic sentences mixed in. Numbers, English terms and the book's own wording stay as they
are. The content before that pass is in `scratchpad`/the CMS history; the founder's biography is the one place
that stays in formal Arabic.

**The stylesheet is mobile first** (rewritten 20 September 2026; the earlier desktop-first copy is in
`_archive/2026-09-20/`). The base rules are the phone layout — one column, a menu that opens as a panel, full-width
buttons — and four `min-width` queries add to it: 481px (a large phone), 681px (a small tablet: two-column grids,
the header's own row), 961px (the hero's art, the course page's side column, three-column grids) and 1025px (the
menu becomes a row of links). Most students read on a phone, so that is the layout that needs no overrides.
Nothing interactive is under 44px, no text on a phone is under 14px, form fields are 16px (below that iOS zooms the
page on focus), and no page scrolls sideways at 320px.

A course can carry a list of **free lessons** (الدروس المجانية in the CMS: a title, a YouTube address and an
image per lesson). They appear on the course page as «شوف الدروس المجانية», each opening in its own place. The
images the four Unit 1 lessons use are the ones the YouTube kit produces, uploaded by
`src/scripts/2026-09-20-free-lessons.ts` (run it with the media volume mounted — see the file's header).

A course's «رابط فيديو الدرس المجاني» (YouTube or Vimeo) turns the preview image into a player: the poster is a
plain link until it is tapped, and only then is the player loaded — on a phone that saves about a megabyte the
visitor never asked for. The embed address comes from `embedUrl()` in `src/lib/site.ts`, the same rule the learning
platform uses.

The pages use the same markup and classes as the static site, so `brand/tokens.css` and `assets/css/site.css`
style them unchanged. A text comparison of ten pages against the static version found only the intended
differences (the «دخول المنصة» link and the price note on the course card).

**First run.** When the database is empty, the site creates the administrator from `.env` and loads the site
copy and the five courses from `seed/` (the JSON the static site was built from). From then on the CMS is the
only source: editing `seed/content/*.json` changes nothing.

### Changing the data model

A new field or collection needs a database migration. The `tools` container has the Payload CLI:

```bash
docker compose --profile tools run --rm tools npx payload generate:types        # after any schema change
docker compose --profile tools run --rm tools npx payload migrate:create <name> # writes src/migrations/…
docker compose --profile tools run --rm tools npx payload generate:importmap    # after adding admin components
docker compose up -d --build web                                                 # applies the migration on start
```

Done once already: `cta_subscribe` added the «اشترك في الدورة» label.

## Settings (`.env`)

`POSTGRES_*` (the database, created with these), `PAYLOAD_SECRET` (signs editor sessions), `SITE_URL` (the public
address, used for the sitemap and link previews), `ADMIN_EMAIL` / `ADMIN_PASSWORD` (the first editor, created
once; change the password after signing in). `.env.example` has the same keys without values.
`certs/extra-ca.pem` is this PC's antivirus root certificate, so the image build can reach npm; on a server,
leave the file empty.

## Content update, 19 September 2026 (Unit 1 free, Egyptian hero, parents' questions)

Applied with `src/scripts/2026-09-19-unit1-free.ts` through Payload's local API (no sign-in), in both languages:
the offer is the whole of Unit 1 («ابدأ الوحدة الأولى مجاناً» on every button; the first pricing plan is «الوحدة
الأولى»); the hero reads «هتفهم المنهج بالمصري، درس درس. والوحدة الأولى كلها مجاناً.»; the three steps are
«اتفرّج على الشرح · راجع النصايح · حِلّ أسئلة كتير»; the FAQ opens with «هل الوحدة الأولى مجانية فعلاً؟» and
«لماذا تطلبون رقم وليّ الأمر؟», and the payment answer now says InstaPay on the platform. The home page also carries
the FAQ as FAQPage structured data for search engines. The content before the change is in
`src/scripts/backup-2026-09-19-original/`. The platform matches: every Unit 1 activity is free there too
(`platform/moodle/plugins/local/thinkera/cli/set_unit_free.php --unit=1`).

**Price, the same day:** the full course is final at **299 EGP** for the school year (the owner's decision; the
platform charges the same through InstaPay). `src/scripts/2026-09-19-price-299.ts` removed the «وحدة واحدة» plan (the
platform sells the full course only, and the plan was showing the full-course price), put the amount in the pricing
lead and in the «كيف أدفع؟» answer, renamed «الخطط المقترحة» to «الخطط», and stopped the interest form promising to
"send the price". Content before the change: `src/scripts/backup-2026-09-19-price.json`.

**Running a Payload script in the tools container:** always add `-e NODE_ENV=production`
(`docker compose --profile tools run --rm -e NODE_ENV=production tools npx tsx src/scripts/<file>.ts`). Without it
Payload runs in dev mode, writes a `dev` row (batch -1) in `payload_migrations`, and the site then stops at start-up
waiting for an answer to "you've run Payload in dev mode". If that happens:
`docker compose exec db psql -U thinkera -d thinkera_site -c "DELETE FROM payload_migrations WHERE name='dev' AND batch=-1;"`
then `docker compose restart web`.

## Before this site goes public

1. **HTTPS and the real addresses.** Put it behind HTTPS, set `SITE_URL=https://thinkera.academy`, and in the CMS
   change *Learning platform address* and each course's platform link from `localhost:8881` to the platform's
   public address.
2. **Email.** No email service is connected, so editors are not notified of new interest requests and "forgot
   password" emails only reach the server log. Add an SMTP adapter (`@payloadcms/email-nodemailer`).
3. **Backups.** The `pgdata` volume (or `docker compose exec db pg_dump -U thinkera thinkera_site`) and the
   `media` volume.
4. **The free lesson must play.** Every «ابدأ الوحدة الأولى مجاناً» button lands on the preview of
   `/courses/bacc-en#preview`, and its *Free lesson video address* is empty. Set it in the CMS (Courses →
   bacc-en → Preview) once the public cut of lesson 1-1 exists; the poster then becomes a link.
5. **Practice by unit or whole syllabus.** Step 3 promises a quiz for «درس أو وحدة أو المنهج كله». The platform
   grades per lesson; the unit and whole-syllabus quiz is not built yet. Build it, or cut the phrase in the CMS.
6. **The 384 depth items.** `HighSchool-EG/00-Analysis/06-Depth-Gap-Inventory.md` still totals 369 while its
   tables hold 384 rows; fix the total so the published number has one source.
7. **«تقنية» or «تكنولوجيا» المعلومات.** The site writes «تقنية المعلومات»; the lesson narration says
   «تكنولوجيا المعلومات». One choice for the site, the channel and the platform.
8. **Test entries.** Two interest requests named «طالب تجريبي» and "Test Student" (emails `test.lead@example.com`,
   `test.lead.en@example.com`) came from testing the form; mark them *closed* or remove them in the CMS.

## Still open from before

- **Contact details:** only `hello@thinkera.academy`; phone and WhatsApp are empty (Site settings).
- **The Arabic form of the name:** «أكاديمية Thinkera» (Latin wordmark + Arabic descriptor), a naming decision.
- **Course facts for the three new courses** (children, advanced, AI in daily life) are assumptions: audience age,
  level, and what each will include.

## Identity

- `public/brand/tokens.css`: semantic tokens (light and dark). Components reference roles such as
  `--tk-text-brand`, never a raw ramp step, so a re-brand or dark mode stays cheap.
- `public/brand/thinkera-logo.svg` (colour) · `-white.svg` (dark surfaces) · `-mono.svg` · `thinkera-mark.svg` ·
  `favicon.svg`. The CMS uses the logo on its sign-in screen and the mark in its side bar.
- Royal blue `#113972` with the wordmark **think** royal + **era** azure. Light surfaces lead with royal, dark
  surfaces with azure-300 `#8FB3E6` (5.27:1 on royal, 8.42:1 on navy).
- No mention of Dolf anywhere: Thinkera is not a Dolf project and has no partnership with it.
- The claim limit holds: the site says «يهيّئك لما بعد الامتحان», never «جاهز لسوق العمل» or «يضمن».

## Checks run on this build (18 September 2026)

- 25 addresses: every page 200 in both languages, old `.html` addresses and `/ar/…` redirect (308), unknown
  addresses 404, `/admin`, `/sitemap.xml`, `/robots.txt` and the static identity guide 200.
- 10 pages × 5 widths (320, 390, 768, 1024, 1320 px): no horizontal scroll, one `h1` per page, every image has an
  `alt`, no broken image. Below 1024 px «دخول المنصة» moves into the menu.
- `lang`/`dir` correct (`ar`/`rtl` at the root, `en`/`ltr` under `/en`).
- Interest form: a valid request is saved with its course and language; a bad email returns the error message; the
  bot trap drops silently; the thank-you message shows.
- Access: `/api/leads` and `/api/users` answer 403 to visitors; courses and page content are public (read only).
- CMS round trip: an English title changed through the CMS showed on `/en/courses/kids` at once and not on the
  Arabic page; then restored.
- Migrations: both applied automatically when the container started.
