diff --git a/README.md b/README.md index ddd2c29..fd4ff5b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CRM Template for WhatsApp +# wacrm — CRM Template for WhatsApp > Self-hostable CRM template for WhatsApp® — shared inbox, contacts, > sales pipelines, broadcasts, and no-code automations. Fork it, brand @@ -10,6 +10,11 @@ [![Supabase](https://img.shields.io/badge/Supabase-Postgres%20%2B%20Auth-3ecf8e?logo=supabase)](https://supabase.com) [![Stars](https://img.shields.io/github/stars/ArnasDon/wacrm?style=social)](https://github.com/ArnasDon/wacrm/stargazers) +The marketing site and self-host docs live in a separate repo: +[ArnasDon/wacrm-site](https://github.com/ArnasDon/wacrm-site) +([wacrm.tech](https://wacrm.tech)). This repo is the product — +clone or fork it to run your own CRM. + ## What you get out of the box - **Shared inbox** on the official WhatsApp Business API — multiple @@ -56,27 +61,24 @@ cp .env.local.example .env.local # fill in Supabase + Meta creds npm run dev ``` -Open . - -Full setup — Supabase migrations, WhatsApp Business API config, and -production deploy — is in [`docs/`](./docs/README.md). +Open . You'll be redirected to `/login` (or +`/dashboard` if already signed in). ## Documentation -**Setup** -- [Getting started](./docs/getting-started.md) -- [Supabase setup](./docs/supabase-setup.md) -- [WhatsApp setup](./docs/whatsapp-setup.md) -- [Environment variables](./docs/environment-variables.md) +Full self-host documentation — Supabase migrations, WhatsApp Business +API config, and production deploy — lives at +**[wacrm.tech/docs](https://wacrm.tech/docs)** +(source: [ArnasDon/wacrm-site](https://github.com/ArnasDon/wacrm-site)). -**Deploy** -- [Deploy on Hostinger](./docs/deployment-hostinger.md) -- [Automations cron](./docs/automations-and-cron.md) - -**Reference** -- [Architecture](./docs/architecture.md) — stack, folder layout, - request lifecycle -- [Troubleshooting](./docs/troubleshooting.md) +Key pages: +- [Getting started](https://wacrm.tech/docs/getting-started) +- [Supabase setup](https://wacrm.tech/docs/supabase-setup) +- [WhatsApp setup](https://wacrm.tech/docs/whatsapp-setup) +- [Environment variables](https://wacrm.tech/docs/environment-variables) +- [Deploy on Hostinger](https://wacrm.tech/docs/deployment-hostinger) +- [Architecture](https://wacrm.tech/docs/architecture) +- [Troubleshooting](https://wacrm.tech/docs/troubleshooting) ## Stack diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 7703ea9..0000000 --- a/docs/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Documentation - -A self-hostable CRM template for WhatsApp® built on Next.js and Supabase. -These docs walk you from a freshly forked repo to a production deploy. - -## Reading order - -**Setup** - -1. **[Getting started](./getting-started.md)** — fork the repo, install, run - locally. -2. **[Supabase setup](./supabase-setup.md)** — create the database, run the - migrations, grab the keys. -3. **[WhatsApp setup](./whatsapp-setup.md)** — create a Meta app, connect a - phone number, wire the webhook. -4. **[Environment variables](./environment-variables.md)** — the full reference. - -**Deploy** - -5. **[Deploy on Hostinger](./deployment-hostinger.md)** — Managed Node.js - Hosting walkthrough, the recommended way to ship the template. -6. **[Automations cron](./automations-and-cron.md)** — schedule the pending - executions drain so waits and delays fire. - -**Reference** - -7. **[Architecture](./architecture.md)** — stack, folder layout, request - lifecycle, where to change things. -8. **[Troubleshooting](./troubleshooting.md)** — the usual suspects. - -## What you will need before starting - -- A GitHub account (to fork the repo). -- A [Supabase](https://supabase.com) project on any paid or free tier. -- A [Meta for Developers](https://developers.facebook.com) account with a - WhatsApp Business app. -- A WhatsApp phone number that is **not** already tied to the regular - WhatsApp or WhatsApp Business mobile apps. -- A [Hostinger Managed Node.js Hosting](https://www.hostinger.com/web-apps-hosting) - plan (or any Node host that runs long-running Node.js 20+ processes). - -## Stack at a glance - -- **Frontend / API** — Next.js 16 (App Router), React 19, Tailwind v4. -- **Database + auth + storage** — Supabase (Postgres + RLS). -- **WhatsApp transport** — Meta Cloud API (official WhatsApp Business API). -- **Encryption** — AES-256-GCM for per-user WhatsApp access + verify - tokens at rest. Legacy CBC-encrypted rows self-heal on next use. -- **Scheduler** — cron (or any pinger) that hits `GET /api/automations/cron`. - -## Getting help - -If something is unclear or broken, open an issue against the source repo: -. diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 215b8fe..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,155 +0,0 @@ -# Architecture - -One-page tour of the codebase — so you know where to look when you -want to change something in your fork. - -## Stack - -| Layer | Tool | Why | -|---|---|---| -| Rendering | **Next.js 16** (App Router) | Server components for data-fetch pages; client components where interactivity's needed. React 19. | -| UI | **Tailwind v4** + **shadcn/base-ui** primitives | Tailwind's zero-runtime styling, shadcn patterns for composable, dark-theme-first components. | -| Data + Auth | **Supabase** | Postgres with Row-Level Security, built-in email/password auth, Storage for avatars, Realtime for the inbox. | -| WhatsApp® | **Meta Cloud API** | Official Business API. No third-party gateway. | -| Encryption | `node:crypto` AES-256-GCM | Per-user WhatsApp access + verify tokens at rest. | -| Scheduler | External HTTP pinger | Hits `GET /api/automations/cron` to drain Wait-step executions. | - -No ORM, no GraphQL layer, no dedicated backend. The Next server-side -routes read and write Supabase directly via `@supabase/ssr`. - -## Folder layout - -``` -wacrm/ -├─ src/ -│ ├─ app/ Next.js App Router routes -│ │ ├─ (auth)/ login, signup, forgot-password -│ │ ├─ (dashboard)/ authenticated UI -│ │ │ ├─ dashboard/ home / metrics / activity feed -│ │ │ ├─ inbox/ shared inbox -│ │ │ ├─ contacts/ contacts + tags -│ │ │ ├─ pipelines/ Kanban deals -│ │ │ ├─ broadcasts/ campaign list + builder -│ │ │ ├─ automations/ flow builder + logs -│ │ │ └─ settings/ Profile / WhatsApp / Templates / Tags -│ │ ├─ docs/ this documentation, rendered -│ │ ├─ api/ JSON endpoints (server-only) -│ │ │ ├─ whatsapp/webhook/ inbound from Meta -│ │ │ ├─ whatsapp/send/ outbound message -│ │ │ ├─ whatsapp/broadcast/ bulk dispatch -│ │ │ ├─ whatsapp/config/ settings read/write -│ │ │ ├─ whatsapp/templates/ template sync -│ │ │ ├─ whatsapp/media/[id]/ media relay -│ │ │ └─ automations/ engine + cron + CRUD -│ │ ├─ page.tsx marketing landing -│ │ ├─ layout.tsx root layout, metadata, icon -│ │ ├─ opengraph-image.tsx dynamic OG image -│ │ └─ icon.tsx dynamic favicon -│ ├─ components/ -│ │ ├─ landing/ marketing page sections -│ │ ├─ dashboard/ dashboard widgets -│ │ ├─ inbox/ conversation list, thread, composer -│ │ ├─ contacts/, pipelines/, broadcasts/, automations/, settings/ -│ │ ├─ docs/ docs viewer shell -│ │ ├─ seo/ JSON-LD helpers -│ │ ├─ layout/ header, sidebar, shell -│ │ └─ ui/ shadcn/base-ui primitives -│ ├─ hooks/ use-auth, use-realtime, etc. -│ ├─ lib/ -│ │ ├─ supabase/ client, server, middleware factories -│ │ ├─ whatsapp/ Meta API client, encryption, phone utils -│ │ ├─ automations/ engine, steps, validation, templates -│ │ ├─ dashboard/ queries + date utils -│ │ ├─ docs/ markdown loader for /docs/ -│ │ ├─ seo/ site config, FAQ data, structured data -│ │ └─ rate-limit.ts per-user token bucket -│ ├─ types/ shared TypeScript types -│ └─ middleware.ts session-refresh middleware -├─ supabase/ -│ └─ migrations/ idempotent SQL files, run in order -├─ docs/ you are here -├─ public/ static assets -└─ next.config.ts Cache-Control + security headers -``` - -## Request lifecycle: inbound WhatsApp message - -``` - Meta Cloud API ──POST──▶ /api/whatsapp/webhook - │ - ├─ verifyMetaWebhookSignature (HMAC-SHA256) - │ └─ rejects 401 if META_APP_SECRET unset or wrong - │ - ├─ supabaseAdmin (service-role, lazy init) - │ - ├─ find contact by phone (create if new) - ├─ find or create conversation - ├─ insert message row - │ - ├─ fire runAutomationsForTrigger(...) - │ └─ matches automations on new_message / keyword - │ - └─ 200 OK (Meta retries otherwise) - -Realtime fan-out: - messages INSERT ──▶ supabase/realtime ──▶ inbox page subscription - └─ appends to thread without refetch -``` - -## Request lifecycle: outbound message from the inbox - -``` - Composer ──fetch──▶ /api/whatsapp/send - │ - ├─ createClient() — user-scoped Supabase client - ├─ auth.getUser() — 401 if unauthed - ├─ checkRateLimit('send:', 60/min) - │ - ├─ select whatsapp_config row (RLS-scoped) - ├─ decrypt(access_token) - │ └─ self-heal: upgrade CBC → GCM (fire-and-forget) - │ - ├─ sendTextMessage / sendTemplateMessage (Meta API) - ├─ retry with phone-number variants on "not allowed" - │ - ├─ insert message row with status = 'sent' - └─ 200 + wamid - -UI: - Optimistic temp row shown immediately. - Real row arrives via Supabase realtime → replaces temp. - If 429 / 5xx: temp row flips to status = 'failed'. -``` - -## Security primitives - -- **Row-Level Security** on every table. Users see only their own - rows via `auth.uid() = user_id`. Service-role bypasses RLS; only - server-only modules use it. -- **Encryption** (`src/lib/whatsapp/encryption.ts`) — AES-256-GCM for - WhatsApp tokens at rest. Backward-compatible decrypt for legacy - CBC rows; self-heals on first use. -- **Webhook signatures** (`src/lib/whatsapp/webhook-signature.ts`) — - HMAC-SHA256 with `META_APP_SECRET`. Fail-closed if the env var - isn't set. -- **Cron secret** — `GET /api/automations/cron` requires - `x-cron-secret: `. Constant-ish string - comparison. -- **Rate limiting** (`src/lib/rate-limit.ts`) — per-user fixed window, - process-local `Map`. Swap for Redis/Upstash at horizontal scale. -- **HTTP headers** (`next.config.ts`) — HSTS, nosniff, - X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy, and a - report-only CSP. - -## Where to change things - -| Want to change… | Start here | -|---|---| -| Marketing copy / landing page | `src/app/page.tsx` + `src/components/landing/*` | -| Dashboard metrics | `src/lib/dashboard/queries.ts` + `src/components/dashboard/*` | -| Inbox behaviour | `src/app/(dashboard)/inbox/page.tsx` + `src/components/inbox/*` | -| Automation triggers / actions | `src/lib/automations/engine.ts`, `steps-tree.ts`, `meta-send.ts` | -| Add a DB column | new migration in `supabase/migrations/NNN_*.sql`, then update the matching `src/types/*.ts` | -| Change auth provider | `src/lib/supabase/{client,server}.ts` + `src/hooks/use-auth.tsx` + `middleware.ts` | -| Add a new API route | `src/app/api//route.ts`, pattern after existing routes | -| Tweak rate limits | `RATE_LIMITS` in `src/lib/rate-limit.ts` | diff --git a/docs/automations-and-cron.md b/docs/automations-and-cron.md deleted file mode 100644 index 90268b4..0000000 --- a/docs/automations-and-cron.md +++ /dev/null @@ -1,77 +0,0 @@ -# Automations cron - -The **Automations** module lets you build flows that react to WhatsApp® -events (new message, new contact, keyword match, schedule, etc.). Most -steps run inline — the exception is the **Wait** step, which parks the -execution in `automation_pending_executions` until its due time. - -A cron job has to drain that table. If you skip this, Wait steps never -resume and any flow that uses them stalls. - -## The endpoint - -``` -GET /api/automations/cron -Header: x-cron-secret: -``` - -- Returns `{ "processed": }` with how many rows were claimed. -- Returns `503` if the env var is not set. -- Returns `401` if the header is missing or wrong. - -The route claims up to 50 pending rows per call via a two-step -UPDATE-by-id so overlapping calls don't double-process. - -## 1. Generate the secret - -Any long random string works. One option: - -```bash -openssl rand -hex 32 -``` - -Copy the result into `AUTOMATION_CRON_SECRET` in your deployment env. - -## 2. Schedule the pinger - -### Option A — hPanel cron (Hostinger Managed Node.js) - -hPanel → **Advanced → Cron Jobs** → add a new job set to every minute -(`* * * * *`). Command: - -``` -curl -s -H "x-cron-secret: " https://crm.example.com/api/automations/cron > /dev/null -``` - -Paste the literal secret — hPanel cron jobs don't inherit the Node app's -environment variables. - -### Option B — external pinger - -Any uptime monitor that supports custom headers works: - -- [UptimeRobot](https://uptimerobot.com) — "Keyword Monitoring" with - custom HTTP headers. -- [Better Stack](https://betterstack.com). -- GitHub Actions with a `schedule:` trigger. - -Hit the URL once per minute. Anything slower just means Wait-step -resolution is that much more delayed. - -## 3. Verify - -```bash -curl -s -H "x-cron-secret: " https://crm.example.com/api/automations/cron -# -> {"processed":0} -``` - -Then create a test automation with a short Wait step (e.g., 30 seconds) -and trigger it. Watch the automation's **Logs** tab — the execution -should resume and mark as completed within one cron interval of the wait -expiring. - -## 4. What if you don't use Wait? - -Skip this page. Trigger → inline steps → done flows do not need a cron — -they finish synchronously inside the webhook handler or from the UI. The -cron only matters for flows that park execution. diff --git a/docs/deployment-hostinger.md b/docs/deployment-hostinger.md deleted file mode 100644 index adf84a4..0000000 --- a/docs/deployment-hostinger.md +++ /dev/null @@ -1,156 +0,0 @@ -# Deploy on Hostinger - -Hostinger's **[Managed Node.js Hosting](https://www.hostinger.com/web-apps-hosting)** -is the recommended way to run this template in production: Hostinger patches the -OS and the Node runtime, handles SSL, and gives you a Git-based deploy -flow from hPanel. You deploy from your fork without ever opening an SSH -session if you don't want to. - -This walkthrough assumes you have already completed -[getting-started](./getting-started.md), [supabase-setup](./supabase-setup.md), -and [whatsapp-setup](./whatsapp-setup.md) — i.e., your fork builds locally -and you have your Supabase + Meta credentials ready. - -## 1. Buy a Managed Node.js plan - -1. Sign up at and choose a - Node.js plan that gives you enough memory for `npm run build` (2 GB+ - recommended). -2. During the onboarding wizard, pick the **Node.js** stack and the region - closest to your users. -3. Finish setup until you land in **hPanel**. - -## 2. Create the app in hPanel - -In hPanel, open the hosting plan and go to **Websites → Create / Manage** -and pick the **Node.js** application option, then: - -- **Application name** — `wacrm`. -- **Application root** — accept the default (e.g., `domains/yourdomain/public_html`). -- **Application URL** — the domain or subdomain you want to use (e.g., - `crm.example.com`). You can attach a domain now or later under - **Domains**. -- **Node.js version** — 20 or newer. -- **Application startup file / command** — Next.js uses `npm start` once - it is built, so set: - - Start command: `npm start` - - Alternatively: `node node_modules/next/dist/bin/next start -p $PORT` - -Save the app. hPanel provisions a container and shows you the app's -control page. - -## 3. Connect your GitHub fork - -In hPanel → **Git**: - -1. **Create repository** → paste your fork's HTTPS URL - (e.g., `https://github.com//wacrm.git`). -2. Pick the branch you want to deploy (usually `main`). -3. Set the deploy path to the **Application root** from step 2. - -Alternative: if you prefer ZIP uploads, use **File Manager** instead — -upload the repo contents into the application root. Git-based deploy is -simpler because redeploying is one click. - -## 4. Install dependencies and build - -hPanel's Node.js app page exposes a **Run NPM install** button and a -terminal. Either works: - -- **Button flow**: - 1. Click **Run NPM install**. Wait for it to finish. - 2. Click **Run NPM Build** (or execute `npm run build` from the app - terminal). -- **Terminal flow**: - ```bash - npm ci - npm run build - ``` - -> Next.js expects `NEXT_PUBLIC_*` variables to be present **at build -> time**, so set env vars (step 5) **before** running the build. - -## 5. Configure environment variables - -In hPanel → **Node.js app → Environment variables**, add every value from -[environment-variables.md](./environment-variables.md): - -- `NEXT_PUBLIC_SUPABASE_URL` -- `NEXT_PUBLIC_SUPABASE_ANON_KEY` -- `SUPABASE_SERVICE_ROLE_KEY` -- `ENCRYPTION_KEY` -- `META_APP_SECRET` -- `NEXT_PUBLIC_SITE_URL` — set to `https://` (with the - scheme, without a trailing slash). -- `AUTOMATION_CRON_SECRET` — if you plan to use Wait steps - ([automations-and-cron.md](./automations-and-cron.md)). - -Save, then **re-run the build** so the new `NEXT_PUBLIC_*` values get -baked into the client bundle. - -## 6. Start the app - -From the app page, click **Restart application** (or run the equivalent -from the terminal). hPanel boots `npm start` behind its own reverse proxy -on the domain you configured. Hit the URL in a browser — you should land -on the marketing page. - -SSL is provisioned automatically. If you used a subdomain, Hostinger's -AutoSSL usually takes a minute or two; until then the site may serve the -plain-HTTP version. - -## 7. Update the Meta webhook - -Back in **Meta for Developers → WhatsApp® → Configuration**, change the -callback URL to `https:///api/whatsapp/webhook` and re-verify. - -## 8. Schedule the automations cron - -If you use the Wait step in any automation, schedule the cron drain. - -- **Inside hPanel** — open **Advanced → Cron Jobs** and add: - ``` - * * * * * curl -s -H "x-cron-secret: " https:///api/automations/cron > /dev/null - ``` - Paste the literal secret here (cron jobs in hPanel don't read app env - vars) or store it in a file the cron reads. -- **Outside** — any uptime monitor (UptimeRobot, Better Stack, GitHub - Actions) can hit the URL once a minute. See - [automations-and-cron.md](./automations-and-cron.md) for detail. - -## 9. Deploying updates - -Two options: - -- **Click-deploy**: push to your fork, then hit **Pull** in hPanel → Git - and **Restart application**. -- **Terminal**: - ```bash - cd - git pull - npm ci - npm run build - # then restart from the Node.js app page - ``` - -If the database schema changed, apply any new SQL files from -`supabase/migrations/` in the Supabase SQL editor first — migrations are -idempotent. - -## When to reach for a VPS instead - -Managed Node.js covers most deploys of this template. Consider -[Hostinger VPS](https://www.hostinger.com/vps-hosting) if you need: - -- Long-running background workers beyond what the single Next.js process - gives you. -- System-level cron behaviour you can't replicate from hPanel. -- Custom binaries (e.g., ffmpeg) for media transforms. - -Otherwise, Managed Node.js is the fast path. - -## Where to go next - -- [Automations cron →](./automations-and-cron.md) — must-do if you use - the Wait step in any automation. -- [Troubleshooting →](./troubleshooting.md) — common deploy issues. diff --git a/docs/environment-variables.md b/docs/environment-variables.md deleted file mode 100644 index 19b5ab5..0000000 --- a/docs/environment-variables.md +++ /dev/null @@ -1,63 +0,0 @@ -# Environment variables - -All runtime configuration lives in `.env.local` during development and in -your host's environment settings in production. `.env.local.example` is a -minimal template; the table below is the full reference. - -## Required - -| Variable | Where to find it | Notes | -| --------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------- | -| `NEXT_PUBLIC_SUPABASE_URL` | Supabase → Project Settings → API → **Project URL** | Public. Shipped to the browser. | -| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase → Project Settings → API → **anon / public** key | Public. Relies on RLS for safety. | -| `SUPABASE_SERVICE_ROLE_KEY` | Supabase → Project Settings → API → **service_role** key | **Secret.** Bypasses RLS. Used by webhook + admin routes only. | -| `ENCRYPTION_KEY` | Generate: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` | 64 hex chars (32 bytes, AES-256-CBC). **Rotating breaks existing tokens.** | -| `META_APP_SECRET` | Meta → App Settings → Basic → **App Secret** | Verifies the `X-Hub-Signature-256` HMAC on every inbound webhook. **Without it the webhook rejects every request** — a public deploy cannot receive messages until this is set. | - -## Recommended - -| Variable | Purpose | -| ---------------------- | ---------------------------------------------------------------------------------------- | -| `NEXT_PUBLIC_SITE_URL` | Canonical public URL (e.g., `https://crm.example.com`). Used for absolute URLs, sitemap, OG images. | - -## Optional - -| Variable | Purpose | -| --------------------------- | -------------------------------------------------------------------------------------------------------- | -| `AUTOMATION_CRON_SECRET` | Shared secret that protects `GET /api/automations/cron`. Required if you schedule the automations drain. See [automations-and-cron.md](./automations-and-cron.md). | - -## Sample `.env.local` - -```bash -# Supabase -NEXT_PUBLIC_SUPABASE_URL=https://abcd1234.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi... -SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi... - -# Meta App Secret — required for webhook signature verification -META_APP_SECRET=abcdef0123456789... - -# Encryption — DO NOT change after first deploy -ENCRYPTION_KEY=3f9c0a7e4d8b2f1a6c5e8d4b9f0a2c6e8d4b9f0a2c6e8d4b9f0a2c6e8d4b9f0a - -# Public URL -NEXT_PUBLIC_SITE_URL=https://crm.example.com - -# Automation cron -AUTOMATION_CRON_SECRET=generate-a-long-random-string -``` - -## Security checklist - -- Never commit `.env.local`. The repo already ignores it. -- On Hostinger Managed Node.js (and any other host), set env vars via the - platform's **Environment variables** panel rather than writing them - into a tracked file on disk. -- Rotate `SUPABASE_SERVICE_ROLE_KEY` if it leaks — Supabase lets you - regenerate it under Project Settings → API. -- Treat `ENCRYPTION_KEY` like a database master key. Losing it means - connected WhatsApp® accounts must reconnect; rotating it means the same. - -## Next step - -[Deploy on Hostinger →](./deployment-hostinger.md) diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index c3ebb9e..0000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,79 +0,0 @@ -# Getting started - -Run the template locally in about 15 minutes. This guide assumes Node.js 20+ and -npm are already installed. - -## 1. Fork and clone - -1. Go to and click **Fork** (top right). -2. Clone your fork: - - ```bash - git clone https://github.com//wacrm.git - cd wacrm - ``` - -## 2. Install dependencies - -```bash -npm install -``` - -## 3. Create the environment file - -Copy the template and fill in the values (full reference in -[environment-variables.md](./environment-variables.md)): - -```bash -cp .env.local.example .env.local -``` - -You cannot run `npm run dev` until **at least** `NEXT_PUBLIC_SUPABASE_URL` -and `NEXT_PUBLIC_SUPABASE_ANON_KEY` are set. You need the full Supabase -setup before WhatsApp® features will work, so do that next. - -## 4. Set up Supabase - -Follow [supabase-setup.md](./supabase-setup.md) to create the project and -run the migrations. Come back here when the URL, anon key, and service-role -key are populated in `.env.local`. - -## 5. Generate the encryption key - -Access tokens for WhatsApp are encrypted at rest with AES-256-CBC. Generate -a 64-character hex key: - -```bash -node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -``` - -Paste the output into `ENCRYPTION_KEY` in `.env.local`. **Do not change -this value later** — existing tokens are encrypted with whatever value was -set at the time, and rotating it will make them unreadable. - -## 6. Run the dev server - -```bash -npm run dev -``` - -Open . You should see the marketing landing page. - -- Create an account at `/signup`. -- After sign-in you land on `/dashboard`. -- The inbox, contacts, pipelines, automations, and broadcasts modules are - all empty until you connect a WhatsApp number - ([whatsapp-setup.md](./whatsapp-setup.md)). - -## 7. Commands cheat sheet - -| Command | Purpose | -| ---------------- | ---------------------------------------------- | -| `npm run dev` | Dev server with Turbopack HMR on `:3000`. | -| `npm run build` | Production build. | -| `npm start` | Run the production build. | -| `npm run lint` | ESLint across the project. | - -## Next step - -[Supabase setup →](./supabase-setup.md) diff --git a/docs/supabase-setup.md b/docs/supabase-setup.md deleted file mode 100644 index 59866b7..0000000 --- a/docs/supabase-setup.md +++ /dev/null @@ -1,83 +0,0 @@ -# Supabase setup - -This template uses Supabase for Postgres, authentication, row-level security (RLS), -and (optionally) storage. You need one Supabase project per deployment. - -## 1. Create the project - -1. Sign in at and create a **new project**. -2. Pick the region closest to your users. Save the database password shown - at creation time — you will not see it again. -3. Wait for the project to provision (about a minute). - -## 2. Grab your keys - -Open **Project Settings → API** in the Supabase dashboard. Copy: - -- **Project URL** → `NEXT_PUBLIC_SUPABASE_URL` -- **anon / public** key → `NEXT_PUBLIC_SUPABASE_ANON_KEY` -- **service_role** key → `SUPABASE_SERVICE_ROLE_KEY` - -Paste them into `.env.local`. - -> **Do not commit the service-role key.** It bypasses RLS entirely. The -> repo's `.gitignore` already excludes `.env.local` — keep it that way. - -## 3. Run the migrations - -All schema changes live in `supabase/migrations/` as plain SQL and are -idempotent (safe to re-run). The simplest way to apply them: - -### Option A — SQL Editor (quickest) - -1. Open **SQL Editor** in the Supabase dashboard. -2. For each file in `supabase/migrations/`, in numeric order: - - `001_initial_schema.sql` - - `002_pipelines_enhancements.sql` - - `003_broadcast_recipient_wamid.sql` - - `004_contact_delete_set_null.sql` - - `005_broadcast_counts_incremental.sql` - - `006_automations.sql` - - `007_automations_increment_counter.sql` -3. Paste into the editor and run. Each file prints its own status. - -### Option B — Supabase CLI - -```bash -npm install --global supabase -supabase login -supabase link --project-ref -supabase db push -``` - -The CLI applies every file in `supabase/migrations/` on top of the linked -project. - -## 4. Verify - -Open **Table Editor** in the Supabase dashboard. You should see tables -including `profiles`, `contacts`, `conversations`, `messages`, `pipelines`, -`broadcasts`, `automations`, and `whatsapp_config`. If any are missing, a -migration failed — re-run it and check the SQL output. - -## 5. Auth settings - -Under **Authentication → Providers**, confirm: - -- **Email** is enabled (default). -- **Confirm email** is on for production, off for local if you want - frictionless testing. - -Under **Authentication → URL Configuration**, add your production URL -(e.g., `https://crm.example.com`) to the allow-list so password-reset -emails link back correctly. - -## 6. (Optional) Storage - -The app downloads WhatsApp® media through Meta's `/download` endpoint and -currently relays it on demand rather than caching it in Supabase Storage. -No bucket setup is required for a default install. - -## Next step - -[WhatsApp setup →](./whatsapp-setup.md) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index 42ddef1..0000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,129 +0,0 @@ -# Troubleshooting - -The greatest hits. If your problem is not here, open an issue at -. - -## Build-time - -### `Error: ENCRYPTION_KEY must be 64 hex chars` - -`ENCRYPTION_KEY` is missing or not the right length. Generate one: - -```bash -node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -``` - -Put the output into `.env.local` / your production env. - -### `Error: supabaseUrl is required` - -`NEXT_PUBLIC_SUPABASE_URL` or `NEXT_PUBLIC_SUPABASE_ANON_KEY` is not set -at build time. On Hostinger / any Node host, make sure the env vars are -exported **before** `npm run build` runs. - -### Next.js complains about missing framework docs - -The repo's `AGENTS.md` pins contributors to the docs bundled in -`node_modules/next/dist/docs/` — that notice is for humans editing the -code, it does not affect your build. - -## Auth - -### Sign-up succeeds but the confirmation email never arrives - -In Supabase → **Authentication → Providers → Email**, either: - -- Configure a custom SMTP provider (recommended for production), or -- Temporarily turn off **Confirm email** while testing. - -### Password-reset links point at `localhost:3000` in production - -Set `NEXT_PUBLIC_SITE_URL` **and** add the same origin to Supabase → -**Authentication → URL Configuration → Redirect URLs**. - -## WhatsApp® - -### Webhook verification fails in Meta - -- The **verify token** in Meta must match exactly what you saved in - **Settings → WhatsApp** inside the app. -- The callback URL must be reachable without auth. Test it yourself: - `curl 'https://crm.example.com/api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=&hub.challenge=test'` - should return `test`. -- `META_APP_SECRET` must be set on the deployment (it's required — - without it, every inbound POST gets a 401). If it's set but wrong, - you'll also see 401s for every POST while the initial subscribe - handshake (which doesn't use it) works. Double-check the value - matches Meta → App Settings → Basic → App Secret exactly. - -### Messages go out but nothing comes in - -1. Confirm the webhook is **Subscribed** to `messages` under Meta → - WhatsApp → Configuration. -2. Confirm the phone number is listed under **Recipients** during testing - (test-number apps only deliver to whitelisted testers). -3. Check server logs — inbound webhooks hit - `POST /api/whatsapp/webhook`. A 200 with no message row means the - payload parsed successfully but didn't match any known contact's - config — double-check `phone_number_id`. - -### `Token decryption failed` - -`ENCRYPTION_KEY` changed since the token was saved. Reconnect the -WhatsApp account from **Settings → WhatsApp** to re-encrypt with the -current key. - -## Automations - -### Wait steps never resume - -The cron drain is not running. See -[automations-and-cron.md](./automations-and-cron.md). - -Quick sanity check from your machine: - -```bash -curl -s -H "x-cron-secret: " https://crm.example.com/api/automations/cron -``` - -If that returns `{"processed":N}` with N growing when a wait is due, the -endpoint is fine and the problem is just the schedule. - -### An automation fires twice for the same message - -Most likely two app instances share one Supabase project and both -receive the same webhook. The webhook handler deduplicates by Meta's -`wamid`, but only within a single deploy — split-brain setups can double -up. Either consolidate to one deploy or route the webhook to exactly one. - -## Deploy - -### `502 Bad Gateway` or "Application not running" from Hostinger - -- Open the Node.js app page in hPanel and confirm the status is **Started**. -- Check the app log viewer for a crash on boot — almost always a missing - env var. Re-save your env vars, re-run the build, and restart. -- Confirm the **Application startup command** is `npm start` (or - equivalent) and that a build has actually run (`.next/` exists in the - application root). - -### Env var changes don't take effect in the browser - -`NEXT_PUBLIC_*` values are baked into the client bundle at build time. -After changing any of them, re-run `npm run build` (hPanel's **Run NPM -Build** button works) and restart the app. - -### Meta webhook suddenly stops after a domain change - -Hostinger provisions a new SSL certificate when you attach a new domain -or subdomain. Until it is issued, the webhook URL serves plain HTTP and -Meta rejects it. Wait for AutoSSL to complete (minutes), then re-verify -the webhook in Meta. - -## Still stuck? - -Open an issue with: - -- Environment (local / Hostinger Managed Node.js / other). -- The exact error from the app log viewer in hPanel or Next's dev output. -- What you were trying to do when it happened. diff --git a/docs/whatsapp-setup.md b/docs/whatsapp-setup.md deleted file mode 100644 index 3774efb..0000000 --- a/docs/whatsapp-setup.md +++ /dev/null @@ -1,106 +0,0 @@ -# WhatsApp setup - -This template talks to the official **WhatsApp® Business Cloud API** (Meta). Each -account stores its own `phone_number_id`, `waba_id`, and encrypted -`access_token` — there is no shared app-level token. This page walks -through getting those values and wiring the webhook. - -## 1. Prerequisites - -- A [Meta for Developers](https://developers.facebook.com) account. -- A Meta **Business Manager** account. -- A phone number that is **not** already in use on the regular WhatsApp or - WhatsApp Business mobile apps. Landlines work; the number must be able - to receive an SMS or voice call during verification. - -## 2. Create the Meta app - -1. In Meta for Developers, go to **My Apps → Create App**. -2. Pick the **Business** app type. -3. On the dashboard, under **Add products**, click **Set up** on - **WhatsApp**. -4. Connect your Business Manager and accept the terms. - -Meta gives you a **test number** for free. You can use it for local -testing, but for production you need to add your own number under -**WhatsApp → API Setup → From**. - -## 3. Collect the IDs and the token - -On **WhatsApp → API Setup** you will see: - -| Value | Goes into | -| --------------------- | ------------------------------------------------ | -| Phone number ID | `phone_number_id` (entered in Settings → Connect) | -| WhatsApp Business ID | `waba_id` (optional but recommended) | -| Temporary access token | `access_token` (short-lived — see next step) | - -The temporary token expires after 24 hours. For production you must -generate a **System User** access token: - -1. **Business Settings → Users → System users → Add**. -2. Name it `wacrm-system-user`, role **Admin**. -3. Click **Generate new token**, pick the app, and grant: - - `whatsapp_business_management` - - `whatsapp_business_messaging` -4. Copy the token. **This is shown only once.** - -## 4. Connect it inside the app - -1. Run the app locally (or open your deployed instance). -2. Sign in, then go to **Settings → WhatsApp**. -3. Fill in: - - **Phone number ID** — from step 3. - - **WhatsApp Business ID** — optional. - - **Access token** — the System User token from step 3. - - **Verify token** — any random string you make up; you will paste the - same value into Meta in the next step. -4. Save. The app encrypts the access token and verify token with your - `ENCRYPTION_KEY` before writing them to Supabase. - -## 5. Configure the webhook in Meta - -Under **WhatsApp → Configuration → Webhook**: - -- **Callback URL**: - - Local dev: run `npx ngrok http 3000` (or similar) and paste - `https://.ngrok.app/api/whatsapp/webhook`. - - Production: `https:///api/whatsapp/webhook`. -- **Verify token**: the same string you entered in step 4. -- Click **Verify and save**. Meta sends a GET with - `hub.challenge` — the app's webhook route decrypts stored verify tokens - and echoes the challenge back. If verification fails, double-check the - verify token matches and the callback URL is publicly reachable. - -### Subscribe to events - -Under the webhook config, **Subscribe to** at minimum: - -- `messages` -- `message_template_status_update` - -`messages` delivers inbound messages and delivery / read statuses; -`message_template_status_update` keeps Meta-approved template statuses in -sync on the broadcast UI. - -## 6. Signature verification (required) - -Set `META_APP_SECRET` to your app's **App Secret** (Meta for Developers → -App Settings → Basic). The app validates the `X-Hub-Signature-256` HMAC -on every inbound webhook and **rejects every request if the env var is -not set** — anyone who knew the webhook URL could otherwise spoof -messages and status updates. Configure it before pointing the webhook -at a deployed instance. - -## 7. Send a test message - -1. From Meta's **API Setup** page, send a test message to a number that is - a registered tester. -2. Watch the Inbox — the message should appear within a second or two. -3. Reply from the app; the recipient gets a real WhatsApp message. - -If nothing shows up, see [troubleshooting.md](./troubleshooting.md). - -## Next step - -[Environment variables →](./environment-variables.md) diff --git a/package-lock.json b/package-lock.json index def0f67..246a0fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "lucide-react": "^1.8.0", - "marked": "^18.0.2", "next": "16.2.4", "react": "19.2.4", "react-dom": "19.2.4", @@ -7078,18 +7077,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/marked": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.2.tgz", - "integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 35b2c6b..2141e2e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Self-hostable CRM template for WhatsApp built on Next.js and Supabase — shared inbox, contacts, sales pipelines, broadcasts, and no-code automations.", "license": "MIT", "author": "Arnas Donauskas", - "homepage": "https://wacrm.tech", + "homepage": "https://github.com/ArnasDon/wacrm", "repository": { "type": "git", "url": "git+https://github.com/ArnasDon/wacrm.git" @@ -47,7 +47,6 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "lucide-react": "^1.8.0", - "marked": "^18.0.2", "next": "16.2.4", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/src/app/docs/[slug]/page.tsx b/src/app/docs/[slug]/page.tsx deleted file mode 100644 index e5c4635..0000000 --- a/src/app/docs/[slug]/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { Metadata } from 'next' -import { notFound } from 'next/navigation' -import Link from 'next/link' -import { ArrowLeft, ArrowRight } from 'lucide-react' -import { listDocs, loadDoc } from '@/lib/docs/content' - -interface DocPageProps { - params: Promise<{ slug: string }> -} - -export async function generateStaticParams() { - const pages = await listDocs() - return pages.map((p) => ({ slug: p.slug })) -} - -export async function generateMetadata({ - params, -}: DocPageProps): Promise { - const { slug } = await params - const doc = await loadDoc(slug) - if (!doc) return {} - return { - title: doc.title, - description: doc.description, - } -} - -export default async function DocPage({ params }: DocPageProps) { - const { slug } = await params - const doc = await loadDoc(slug) - if (!doc) notFound() - - const pages = await listDocs() - const idx = pages.findIndex((p) => p.slug === slug) - const prev = idx > 0 ? pages[idx - 1] : null - const next = idx >= 0 && idx < pages.length - 1 ? pages[idx + 1] : null - - return ( -
-

- {doc.section} -

-
- - -
- ) -} diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx deleted file mode 100644 index 0b29c5a..0000000 --- a/src/app/docs/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from 'next' -import { DocsShell } from '@/components/docs/shell' -import { listDocs } from '@/lib/docs/content' - -export const metadata: Metadata = { - title: { - template: '%s — CRM Template for WhatsApp Docs', - default: 'CRM Template for WhatsApp Docs', - }, - description: - 'Setup, configuration, and deployment docs for the CRM Template for WhatsApp.', -} - -export default async function DocsLayout({ - children, -}: { - children: React.ReactNode -}) { - const pages = await listDocs() - return {children} -} diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx deleted file mode 100644 index 92ecec7..0000000 --- a/src/app/docs/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Link from 'next/link' -import { ArrowRight } from 'lucide-react' -import { listDocs } from '@/lib/docs/content' - -export default async function DocsIndexPage() { - const pages = await listDocs() - const sections: Record = {} - for (const p of pages) { - sections[p.section] ??= [] - sections[p.section].push(p) - } - - return ( -
-

- Documentation -

-

- Self-host the CRM template for WhatsApp end to end -

-

- Everything you need to take the template from a fresh fork to a - production deploy. Work through the pages in order, or jump to the - one you need. -

- -
- {Object.entries(sections).map(([section, items]) => ( -
-

- {section} -

-
- {items.map((p) => ( - -

- {p.title} -

- {p.description && ( -

- {p.description} -

- )} - - Read - - - - ))} -
-
- ))} -
-
- ) -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3e81250..9e3207d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,71 +2,21 @@ import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import { Toaster } from "sonner"; import "./globals.css"; -import { - OG_IMAGE_ALT, - SITE_DESCRIPTION, - SITE_KEYWORDS, - SITE_NAME, - SITE_TAGLINE, - SITE_URL, -} from "@/lib/seo/site-config"; const inter = Inter({ variable: "--font-sans", subsets: ["latin"], }); -// Consolidated site-wide metadata. Page-specific metadata (landing, -// auth pages, etc.) override the narrower fields (title, description, -// robots); defaults defined here are inherited everywhere else so we -// never ship a page without OpenGraph / Twitter coverage. export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), title: { - default: `${SITE_NAME} — ${SITE_TAGLINE}`, - template: `%s — ${SITE_NAME}`, - }, - description: SITE_DESCRIPTION, - keywords: SITE_KEYWORDS, - applicationName: SITE_NAME, - authors: [{ name: SITE_NAME, url: SITE_URL }], - creator: SITE_NAME, - publisher: SITE_NAME, - category: "Business Software", - alternates: { - canonical: "/", - }, - openGraph: { - type: "website", - siteName: SITE_NAME, - title: `${SITE_NAME} — ${SITE_TAGLINE}`, - description: SITE_DESCRIPTION, - url: SITE_URL, - locale: "en_US", - images: [ - { - url: "/opengraph-image", - width: 1200, - height: 630, - alt: OG_IMAGE_ALT, - }, - ], - }, - twitter: { - card: "summary_large_image", - title: `${SITE_NAME} — ${SITE_TAGLINE}`, - description: SITE_DESCRIPTION, - images: ["/opengraph-image"], + default: "wacrm", + template: "%s — wacrm", }, + description: "Self-hostable CRM template for WhatsApp.", robots: { - index: true, - follow: true, - googleBot: { - index: true, - follow: true, - "max-image-preview": "large", - "max-snippet": -1, - }, + index: false, + follow: false, }, icons: { icon: [{ url: "/icon" }], @@ -78,7 +28,6 @@ export const metadata: Metadata = { }, }; -// Dark theme color for the mobile browser chrome. export const viewport: Viewport = { themeColor: "#020617", colorScheme: "dark", diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx deleted file mode 100644 index f95a227..0000000 --- a/src/app/opengraph-image.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { ImageResponse } from 'next/og' -import { SITE_NAME, SITE_TAGLINE } from '@/lib/seo/site-config' - -/** - * Auto-generated OG image served at /opengraph-image. - * Next renders this JSX to a PNG at build time, caches it, and wires - * the URL into the default `` for every - * page that inherits the root metadata. - * - * Facebook / LinkedIn / Slack / Twitter all fetch this when the link - * is shared, so keep it readable at thumbnail sizes (no tiny text, - * high contrast). - */ - -export const alt = `${SITE_NAME} — ${SITE_TAGLINE}` -export const size = { width: 1200, height: 630 } -export const contentType = 'image/png' - -export default async function Image() { - // Satori (the renderer behind next/og) is strict about two things - // that the first version of this file violated at BUILD time — - // which meant `next build` failed on /opengraph-image during - // static generation and the whole production build blew up. - // - // 1. Every
with more than one child must explicitly declare - // display: flex / contents / none. - // 2. Any Unicode glyph outside the default font's coverage fails - // with "Failed to download dynamic font". ✓ was the worst - // offender — replaced below with plain text dot separators. - return new ImageResponse( - ( -
-
-
- W -
-
- {SITE_NAME} -
-
- -
- Run your WhatsApp® business from  - one inbox. -
- -
- Shared inbox, sales pipelines, broadcasts, and no-code automations — built on the official WhatsApp Business API. -
- -
- Shared inbox - · - Automations - · - Pipelines - · - Analytics -
-
- ), - size, - ) -} diff --git a/src/app/page.tsx b/src/app/page.tsx index 1317f02..7073c07 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,118 +1,5 @@ -import type { Metadata } from 'next' +import { redirect } from 'next/navigation' -import { LandingNav } from '@/components/landing/nav' -import { Hero } from '@/components/landing/hero' -import { FeaturesGrid } from '@/components/landing/features-grid' -import { HowItWorks } from '@/components/landing/how-it-works' -import { FeatureSpotlight } from '@/components/landing/feature-spotlight' -import { FAQ } from '@/components/landing/faq' -import { OpenSource } from '@/components/landing/open-source' -import { CtaBanner } from '@/components/landing/cta-banner' -import { Footer } from '@/components/landing/footer' -import { InboxMock } from '@/components/landing/mock/inbox-mock' -import { PipelineMock } from '@/components/landing/mock/pipeline-mock' -import { AutomationMock } from '@/components/landing/mock/automation-mock' -import { AnalyticsMock } from '@/components/landing/mock/analytics-mock' -import { JsonLd } from '@/components/seo/json-ld' -import { landingPageLd } from '@/lib/seo/structured-data' -import { SITE_DESCRIPTION, SITE_NAME, SITE_TAGLINE } from '@/lib/seo/site-config' - -// Landing-specific metadata. Most fields are inherited from the root -// layout's metadata — we override title (so the hero copy shows in -// SERPs / tab titles) and set an explicit canonical for "/" to avoid -// trailing-slash duplicate-content signals. -export const metadata: Metadata = { - title: { - absolute: `${SITE_NAME} — ${SITE_TAGLINE}`, - }, - description: SITE_DESCRIPTION, - alternates: { - canonical: '/', - }, -} - -// Marketing landing. Visible at / for everyone (auth redirect that -// used to send anonymous visitors to /login was removed). Authed users -// still see this page; the nav swaps its CTA to "Go to Dashboard". -export default function LandingPage() { - return ( -
- {/* JSON-LD — WebSite, Organization, SoftwareApplication, FAQPage. - Emitted before any visible content so crawlers hit it first. */} - - -
- - - - - } - /> - - - - } - /> - - } - /> - - } - /> - - - - - - -
-
-
- ) +export default function RootPage() { + redirect('/dashboard') } diff --git a/src/app/robots.ts b/src/app/robots.ts deleted file mode 100644 index 73543ab..0000000 --- a/src/app/robots.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { MetadataRoute } from 'next' -import { SITE_URL } from '@/lib/seo/site-config' - -/** - * robots.txt served at /robots.txt. We explicitly disallow every - * gated area so crawlers don't spend their budget on pages they'll - * bounce off. Auth pages are closed off too — no SEO value, and - * we don't want a "Sign in" page competing with the landing page - * in SERPs. - */ -export default function robots(): MetadataRoute.Robots { - return { - rules: [ - { - userAgent: '*', - allow: '/', - disallow: [ - '/api/', - '/dashboard', - '/inbox', - '/contacts', - '/pipelines', - '/broadcasts', - '/automations', - '/settings', - '/login', - '/signup', - '/forgot-password', - ], - }, - ], - sitemap: `${SITE_URL}/sitemap.xml`, - host: SITE_URL, - } -} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts deleted file mode 100644 index b387557..0000000 --- a/src/app/sitemap.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { MetadataRoute } from 'next' -import { SITE_URL } from '@/lib/seo/site-config' - -/** - * Sitemap served at /sitemap.xml. Only lists public, indexable pages - * — auth pages are explicitly noindex, and every route under /app is - * gated. The landing anchors (#features, #how-it-works, #faq) don't - * belong here; fragments aren't a sitemap concept. - */ -export default function sitemap(): MetadataRoute.Sitemap { - const now = new Date() - return [ - { - url: `${SITE_URL}/`, - lastModified: now, - changeFrequency: 'weekly', - priority: 1, - }, - ] -} diff --git a/src/components/docs/shell.tsx b/src/components/docs/shell.tsx deleted file mode 100644 index 79a8a99..0000000 --- a/src/components/docs/shell.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client" - -import { useState, type ReactNode } from 'react' -import Link from 'next/link' -import { Menu, X } from 'lucide-react' -import { DocsSidebar } from './sidebar' -import { GithubIcon } from '@/components/landing/github-icon' -import { MessageSquare } from 'lucide-react' -import type { DocPage } from '@/lib/docs/content' - -const REPO_URL = 'https://github.com/ArnasDon/wacrm' - -interface DocsShellProps { - pages: DocPage[] - children: ReactNode -} - -export function DocsShell({ pages, children }: DocsShellProps) { - const [mobileOpen, setMobileOpen] = useState(false) - const closeMobile = () => setMobileOpen(false) - - return ( -
-
-
-
- - - - - - - CRM Template for WhatsApp - - - Docs - - -
- -
- - Home - - - - - - Get started - -
-
-
- -
- - - {mobileOpen && ( -
-
-
- -
-
- )} - -
{children}
-
- -
-
- WhatsApp® is a registered trademark of Meta Platforms, Inc. - Hostinger is not affiliated with, endorsed by, or sponsored by - Meta Platforms, Inc. -
-
-
- ) -} diff --git a/src/components/docs/sidebar.tsx b/src/components/docs/sidebar.tsx deleted file mode 100644 index ba4e0fe..0000000 --- a/src/components/docs/sidebar.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client" - -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { cn } from '@/lib/utils' -import type { DocPage } from '@/lib/docs/content' - -interface DocsSidebarProps { - pages: DocPage[] - onNavigate?: () => void -} - -export function DocsSidebar({ pages, onNavigate }: DocsSidebarProps) { - const pathname = usePathname() - - const sections: Record = {} - for (const p of pages) { - sections[p.section] ??= [] - sections[p.section].push(p) - } - - return ( - - ) -} diff --git a/src/components/landing/cta-banner.tsx b/src/components/landing/cta-banner.tsx deleted file mode 100644 index d82092c..0000000 --- a/src/components/landing/cta-banner.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import Link from 'next/link' -import { ArrowRight } from 'lucide-react' -import { Section } from './section' - -/** - * Last push before the footer. Visually distinct (border, gradient - * background) so it doesn't read as "just another section". - */ -export function CtaBanner() { - return ( -
-
-

- Ready to stop switching between tools? -

-

- Bring your WhatsApp conversations, contacts, and deals into one place. -

-
- - Get started free - - - - Sign in - -
-
-
- ) -} diff --git a/src/components/landing/faq.tsx b/src/components/landing/faq.tsx deleted file mode 100644 index 1436b77..0000000 --- a/src/components/landing/faq.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client" - -import { useState } from 'react' -import { ChevronDown } from 'lucide-react' -import { Section, SectionHeader } from './section' -import { FAQ_ITEMS } from '@/lib/seo/faq-data' -import { cn } from '@/lib/utils' - -export function FAQ() { - const [openIdx, setOpenIdx] = useState(0) - return ( -
- - -
- {FAQ_ITEMS.map((item, i) => { - const isOpen = openIdx === i - return ( -
- - {isOpen && ( -
- {item.a} -
- )} -
- ) - })} -
-
- ) -} diff --git a/src/components/landing/feature-spotlight.tsx b/src/components/landing/feature-spotlight.tsx deleted file mode 100644 index 4663643..0000000 --- a/src/components/landing/feature-spotlight.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { ReactNode } from 'react' -import { Check } from 'lucide-react' -import { Section } from './section' -import { cn } from '@/lib/utils' - -interface FeatureSpotlightProps { - eyebrow: string - title: string - body: string - bullets?: string[] - /** Right-side by default; flip to put the visual on the left. */ - reverse?: boolean - visual: ReactNode - anchorId?: string -} - -/** - * Reusable alternating feature section (copy on one side, product - * miniature on the other). Three instances on the page so we use the - * same component to keep spacing + typography consistent. - */ -export function FeatureSpotlight({ - eyebrow, - title, - body, - bullets, - reverse, - visual, - anchorId, -}: FeatureSpotlightProps) { - return ( -
-
*:first-child]:order-2', - )} - > -
-

- {eyebrow} -

-

- {title} -

-

- {body} -

- {bullets && bullets.length > 0 && ( -
    - {bullets.map((b) => ( -
  • - - {b} -
  • - ))} -
- )} -
-
{visual}
-
-
- ) -} diff --git a/src/components/landing/features-grid.tsx b/src/components/landing/features-grid.tsx deleted file mode 100644 index 41a5f57..0000000 --- a/src/components/landing/features-grid.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - MessageSquare, - Users, - GitBranch, - Radio, - Zap, - LineChart, -} from 'lucide-react' -import type { ComponentType } from 'react' -import { Section, SectionHeader } from './section' - -interface Feature { - title: string - description: string - icon: ComponentType<{ className?: string }> - tint: string -} - -// Intentionally six — more than that becomes a feature list, less -// feels thin. Copy is benefit-first ("never miss a lead") rather -// than feature-first ("notifications"). -const FEATURES: Feature[] = [ - { - title: 'Shared inbox', - description: - 'Every WhatsApp conversation in one place. Assign threads, reply as a team, never drop a lead.', - icon: MessageSquare, - tint: 'text-blue-400 bg-blue-500/10', - }, - { - title: 'Contact hub', - description: - 'Tags, custom fields, notes, and automatic deduplication. Import existing contacts from CSV.', - icon: Users, - tint: 'text-violet-400 bg-violet-500/10', - }, - { - title: 'Sales pipelines', - description: - 'Drag deals through stages. See what is won, what is slipping, and where revenue is stuck.', - icon: GitBranch, - tint: 'text-violet-400 bg-violet-500/10', - }, - { - title: 'Broadcast campaigns', - description: - 'Send Meta-approved templates to segmented lists. Track delivery, reads, and replies in real time.', - icon: Radio, - tint: 'text-amber-400 bg-amber-500/10', - }, - { - title: 'No-code automations', - description: - 'Welcome new contacts, chase unanswered replies, route leads by keyword. Visual flow builder.', - icon: Zap, - tint: 'text-rose-400 bg-rose-500/10', - }, - { - title: 'Real-time analytics', - description: - 'Response times, daily volume, pipeline value. See what is working without building dashboards.', - icon: LineChart, - tint: 'text-cyan-400 bg-cyan-500/10', - }, -] - -export function FeaturesGrid() { - return ( -
- - -
- {FEATURES.map((f) => { - const Icon = f.icon - return ( -
-
- -
-

- {f.title} -

-

- {f.description} -

-
- ) - })} -
-
- ) -} diff --git a/src/components/landing/footer.tsx b/src/components/landing/footer.tsx deleted file mode 100644 index 91a7469..0000000 --- a/src/components/landing/footer.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import Link from 'next/link' -import { MessageSquare } from 'lucide-react' - -export function Footer() { - const year = new Date().getFullYear() - return ( -
-
-
- - - - - - CRM Template for WhatsApp - - -

- One toolkit for your WhatsApp business — shared inbox, pipelines, - broadcasts, and automations. -

-
- - - - - - -
- -
-
- © {year} CRM Template for WhatsApp. All rights reserved. - Built on the official WhatsApp Business API. -
-
- WhatsApp® is a registered trademark of Meta Platforms, Inc. - Hostinger is not affiliated with, endorsed by, or sponsored by - Meta Platforms, Inc. -
-
-
- ) -} - -function FooterColumn({ - title, - links, -}: { - title: string - links: { href: string; label: string; external?: boolean }[] -}) { - return ( -
-

- {title} -

-
    - {links.map((l) => - l.external ? ( -
  • - - {l.label} - -
  • - ) : ( -
  • - - {l.label} - -
  • - ), - )} -
-
- ) -} diff --git a/src/components/landing/github-icon.tsx b/src/components/landing/github-icon.tsx deleted file mode 100644 index cec9fcc..0000000 --- a/src/components/landing/github-icon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { SVGProps } from 'react' - -export function GithubIcon(props: SVGProps) { - return ( - - ) -} diff --git a/src/components/landing/hero.tsx b/src/components/landing/hero.tsx deleted file mode 100644 index 13106b1..0000000 --- a/src/components/landing/hero.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import Link from 'next/link' -import { ShieldCheck, ArrowRight } from 'lucide-react' -import { InboxMock } from './mock/inbox-mock' - -/** - * Above-the-fold hero. Two-column on desktop (copy + product visual), - * stacks on mobile. The product miniature doubles as proof of what - * the app actually looks like — better than a generic illustration. - */ -export function Hero() { - return ( -
- {/* Soft radial glow behind the hero. Pure CSS so it doesn't - count against any image budget. */} -
- -
-
-
- - Built on the official WhatsApp® Business API -
- -

- Run your WhatsApp business from{' '} - one inbox. -

- -

- Shared inbox, contact hub, sales pipelines, broadcasts, and - no-code automations — in one place. Get your team on the same - page without switching tools. -

- -
- - Get started - - - - Sign in - -
- -
- - - No credit card required - - - - Live in 30 minutes - - - - Your data, your Supabase - -
-
- -
- -
-
-
- ) -} diff --git a/src/components/landing/how-it-works.tsx b/src/components/landing/how-it-works.tsx deleted file mode 100644 index 30d4e76..0000000 --- a/src/components/landing/how-it-works.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Plug, Users, Zap } from 'lucide-react' -import { Section, SectionHeader } from './section' - -const STEPS = [ - { - num: '01', - icon: Plug, - title: 'Connect your WhatsApp number', - body: - 'Paste your phone number ID and access token from Meta. Works with any Meta-approved WhatsApp Business API provider.', - }, - { - num: '02', - icon: Users, - title: 'Bring in your contacts', - body: - 'Import a CSV, or let incoming messages build your contact list automatically. Tags and custom fields are ready from day one.', - }, - { - num: '03', - icon: Zap, - title: 'Reply, automate, measure', - body: - 'Use the shared inbox with your team, set up flows for repeat work, and track what is actually moving the needle in your analytics.', - }, -] - -export function HowItWorks() { - return ( -
- - -
- {STEPS.map((s) => { - const Icon = s.icon - return ( -
-
-
- -
- - {s.num} - -
-

- {s.title} -

-

- {s.body} -

-
- ) - })} -
-
- ) -} diff --git a/src/components/landing/mock/analytics-mock.tsx b/src/components/landing/mock/analytics-mock.tsx deleted file mode 100644 index adcc769..0000000 --- a/src/components/landing/mock/analytics-mock.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Miniature analytics panel with a line chart + 3 stat tiles. - * Shown alongside the dashboard feature spotlight. SVG rendered - * inline so there's no image to load. - */ -export function AnalyticsMock() { - // Pre-sampled line data so the curve looks "realistic" without - // being random (we want the same pleasing shape on every render). - const incoming = [4, 6, 3, 7, 9, 6, 11, 14, 10, 13, 16, 18, 15, 22] - const outgoing = [3, 5, 2, 6, 7, 5, 9, 12, 9, 11, 14, 15, 13, 19] - - const VB_W = 320 - const VB_H = 120 - const PAD = { top: 8, right: 10, bottom: 10, left: 24 } - const chartW = VB_W - PAD.left - PAD.right - const chartH = VB_H - PAD.top - PAD.bottom - const maxY = Math.max(...incoming, ...outgoing) - const stepX = chartW / (incoming.length - 1) - const y = (v: number) => PAD.top + chartH - (v / maxY) * chartH - const x = (i: number) => PAD.left + i * stepX - const path = (arr: number[]) => - arr.map((v, i) => `${i === 0 ? 'M' : 'L'}${x(i)},${y(v)}`).join(' ') - - return ( -
-
- - - - - Dashboard — CRM Template for WhatsApp - -
- -
-
- {[ - { label: 'Open convos', value: '42', delta: '+5', positive: true }, - { label: 'New today', value: '18', delta: '+3', positive: true }, - { label: 'Avg reply', value: '3.2m', delta: '-0.4m', positive: true }, - ].map((s) => ( -
-
{s.label}
-
- {s.value} -
-
- {s.delta} -
-
- ))} -
- -
-
- - Conversations over time - -
- - - Incoming - - - - Outgoing - -
-
- - {[0, 0.5, 1].map((t) => ( - - ))} - - - -
-
-
- ) -} diff --git a/src/components/landing/mock/automation-mock.tsx b/src/components/landing/mock/automation-mock.tsx deleted file mode 100644 index 1412556..0000000 --- a/src/components/landing/mock/automation-mock.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { MessageSquare, Zap, GitBranch, Hourglass } from 'lucide-react' - -/** - * Miniature flow-builder for the Automations feature spotlight. - * Mirrors the real builder's look (trigger + stepped cards + arrows) - * but is pure presentation — no interactivity. - */ -export function AutomationMock() { - const steps = [ - { - kind: 'trigger', - label: 'First Message from Contact', - icon: Zap, - accent: 'border-l-blue-500', - badgeClass: 'bg-blue-500/10 text-blue-400', - kindLabel: 'TRIGGER', - }, - { - kind: 'action', - label: 'Send "Hi! 👋 Thanks for reaching out."', - icon: MessageSquare, - accent: 'border-l-violet-500', - badgeClass: 'bg-violet-500/10 text-violet-400', - kindLabel: 'ACTION', - }, - { - kind: 'wait', - label: 'Wait 10 minutes', - icon: Hourglass, - accent: 'border-l-slate-500', - badgeClass: 'bg-slate-500/10 text-slate-400', - kindLabel: 'WAIT', - }, - { - kind: 'condition', - label: 'If tag = "lead" → assign to sales', - icon: GitBranch, - accent: 'border-l-amber-500', - badgeClass: 'bg-amber-500/10 text-amber-400', - kindLabel: 'CONDITION', - }, - ] - - return ( -
-
- - - - - Automations — CRM Template for WhatsApp - -
- - {/* Dotted canvas — matches the real flow builder's background. */} -
- {steps.map((s, i) => { - const Icon = s.icon - return ( -
- {i > 0 &&
} -
-
- - - -
-
- {s.kindLabel} -
-
- {s.label} -
-
-
-
-
- ) - })} -
-
- ) -} diff --git a/src/components/landing/mock/inbox-mock.tsx b/src/components/landing/mock/inbox-mock.tsx deleted file mode 100644 index efb71c2..0000000 --- a/src/components/landing/mock/inbox-mock.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { MessageSquare, Search } from 'lucide-react' - -/** - * Miniature of the app's shared inbox. Pure CSS — no real data, no - * supabase calls. Used in the hero and the inbox feature spotlight. - * Kept stateless and presentational so the page renders fast. - */ -export function InboxMock() { - return ( -
- {/* Fake window chrome — makes the mock read as "a product", not - an abstract component. */} -
- - - - - Inbox — CRM Template for WhatsApp - -
- -
- {/* Conversation list */} -
-
- - Search -
- {[ - { name: 'Aisha', preview: 'Thanks! Received it.', unread: false, active: true }, - { name: 'Diego', preview: 'Do you ship to Brazil?', unread: true, active: false }, - { name: 'Yuki', preview: 'Price sheet attached.', unread: false, active: false }, - { name: 'Luca', preview: 'Got it, will test.', unread: false, active: false }, - ].map((c, i) => ( -
- - {c.name[0]} - -
-
- {c.name} - {c.unread && } -
-

{c.preview}

-
-
- ))} -
- - {/* Thread */} -
-
- - A - -
-
Aisha
-
+44 7700 900123
-
-
- Open -
-
- -
-
- Hi! Is the kit available in large? -
-
- Yes — shipping today 📦 -
-
- Thanks! Received it. -
-
- -
-
- Type a message… -
- -
-
-
-
- ) -} diff --git a/src/components/landing/mock/pipeline-mock.tsx b/src/components/landing/mock/pipeline-mock.tsx deleted file mode 100644 index 3ef8b97..0000000 --- a/src/components/landing/mock/pipeline-mock.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Miniature kanban pipeline with three stages and a handful of fake - * deal cards. Feature-spotlight for the Pipelines section. - */ -export function PipelineMock() { - const stages = [ - { - name: 'Lead', - color: 'bg-blue-500', - deals: [ - { title: 'Acme Co', value: '$1,200' }, - { title: 'Bakery on 3rd', value: '$450' }, - ], - }, - { - name: 'Proposal', - color: 'bg-amber-500', - deals: [ - { title: 'Nova Studios', value: '$4,800' }, - { title: 'Riveria Hotel', value: '$2,100' }, - { title: 'Pine & Co', value: '$960' }, - ], - }, - { - name: 'Won', - color: 'bg-violet-500', - deals: [{ title: 'Lagoon Spa', value: '$3,200' }], - }, - ] - - return ( -
-
- - - - - Pipelines — CRM Template for WhatsApp - -
- -
- {stages.map((s) => ( -
-
- - - {s.name} - - - {s.deals.length} - -
- {s.deals.map((d, i) => ( -
-
{d.title}
-
- {d.value} -
-
- ))} -
- ))} -
-
- ) -} diff --git a/src/components/landing/nav.tsx b/src/components/landing/nav.tsx deleted file mode 100644 index c9617fc..0000000 --- a/src/components/landing/nav.tsx +++ /dev/null @@ -1,191 +0,0 @@ -"use client" - -import Link from 'next/link' -import { useEffect, useState } from 'react' -import { MessageSquare, Menu, X } from 'lucide-react' -import { createClient } from '@/lib/supabase/client' -import { cn } from '@/lib/utils' -import { GithubIcon } from './github-icon' - -/** - * Landing-page top nav. Client component because we need to flip the - * primary CTA depending on whether the visitor is already signed in. - * "Auth pending" renders a placeholder (a pair of muted pill shapes) - * so the CTA doesn't pop in jarringly after hydration. - */ -type AuthState = 'pending' | 'signed-in' | 'signed-out' - -const LINKS = [ - { href: '#features', label: 'Features' }, - { href: '#how-it-works', label: 'How it works' }, - { href: '#self-host', label: 'Self-host' }, - { href: '/docs', label: 'Docs' }, - { href: '#faq', label: 'FAQ' }, -] - -const REPO_URL = 'https://github.com/ArnasDon/wacrm' - -export function LandingNav() { - const [auth, setAuth] = useState('pending') - const [scrolled, setScrolled] = useState(false) - const [mobileOpen, setMobileOpen] = useState(false) - - useEffect(() => { - // Quick auth check — no realtime needed, just the initial state. - const supabase = createClient() - let cancelled = false - supabase.auth.getSession().then(({ data }) => { - if (cancelled) return - setAuth(data.session?.user ? 'signed-in' : 'signed-out') - }) - return () => { - cancelled = true - } - }, []) - - useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 8) - onScroll() - window.addEventListener('scroll', onScroll, { passive: true }) - return () => window.removeEventListener('scroll', onScroll) - }, []) - - return ( -
-
- - - - - - CRM Template for WhatsApp - - - - - -
- - - - -
- - -
- - {mobileOpen && ( -
-
- {LINKS.map((l) => ( - setMobileOpen(false)} - className="rounded-md px-3 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white" - > - {l.label} - - ))} - setMobileOpen(false)} - className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white" - > - - GitHub - -
- -
-
-
- )} -
- ) -} - -function NavCtas({ auth, mobile = false }: { auth: AuthState; mobile?: boolean }) { - const btnBase = - 'inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors' - const secondary = cn( - btnBase, - 'text-slate-300 hover:bg-slate-800 hover:text-white', - mobile && 'justify-center', - ) - const primary = cn( - btnBase, - 'bg-violet-500 text-white hover:bg-violet-400', - mobile && 'justify-center', - ) - - if (auth === 'pending') { - // Neutral placeholder that matches the eventual button sizes so - // nothing shifts once auth resolves. - return ( - <> - · - - · - - - ) - } - - if (auth === 'signed-in') { - return ( - - Go to Dashboard - - ) - } - - return ( - <> - - Sign in - - - Get started - - - ) -} diff --git a/src/components/landing/open-source.tsx b/src/components/landing/open-source.tsx deleted file mode 100644 index 5a3a817..0000000 --- a/src/components/landing/open-source.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import Link from 'next/link' -import { ArrowRight, BookOpen, Server } from 'lucide-react' -import { Section, SectionHeader } from './section' -import { GithubIcon } from './github-icon' - -const REPO_URL = 'https://github.com/ArnasDon/wacrm' -const HOSTINGER_URL = 'https://www.hostinger.com/web-apps-hosting' - -export function OpenSource() { - return ( -
- - - - -
- - - Read the full documentation - - -
-
- ) -} diff --git a/src/components/landing/section.tsx b/src/components/landing/section.tsx deleted file mode 100644 index e5fe2e8..0000000 --- a/src/components/landing/section.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { cn } from '@/lib/utils' -import type { ReactNode } from 'react' - -/** - * Shared section wrapper — consistent horizontal padding, max-width, - * and vertical rhythm across every landing section. Keeping it in one - * place means tweaking spacing once vs. in every section component. - */ -export function Section({ - id, - className, - children, - tight, -}: { - id?: string - className?: string - children: ReactNode - /** Compact vertical padding — useful for nav strips / dense sections. */ - tight?: boolean -}) { - return ( -
- {children} -
- ) -} - -/** - * Pair of small label + big headline used atop every section. The - * eyebrow establishes context in 1-2 words before the user commits - * to reading the headline. - */ -export function SectionHeader({ - eyebrow, - title, - description, - align = 'center', -}: { - eyebrow?: string - title: string - description?: string - align?: 'center' | 'left' -}) { - const base = - align === 'center' ? 'text-center' : 'text-left' - const max = align === 'center' ? 'max-w-2xl mx-auto' : 'max-w-2xl' - return ( -
- {eyebrow && ( -

- {eyebrow} -

- )} -

- {title} -

- {description && ( -

{description}

- )} -
- ) -} diff --git a/src/components/seo/json-ld.tsx b/src/components/seo/json-ld.tsx deleted file mode 100644 index a544b0f..0000000 --- a/src/components/seo/json-ld.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Emits one