Merge remote-tracking branch 'origin/main' into chore/whatsapp-trademark-symbol

# Conflicts:
#	README.md
#	docs/README.md
#	docs/automations-and-cron.md
#	docs/supabase-setup.md
#	docs/whatsapp-setup.md
#	src/app/opengraph-image.tsx
#	src/components/landing/hero.tsx
#	src/lib/seo/site-config.ts
This commit is contained in:
Arnas Donauskas 2026-05-06 13:50:03 +03:00
commit 1f2849df75
No known key found for this signature in database
90 changed files with 370 additions and 342 deletions

View File

@ -11,8 +11,8 @@ that standard.
If you witness or experience behaviour that violates the Code, please If you witness or experience behaviour that violates the Code, please
report it privately to the project maintainer: report it privately to the project maintainer:
- Email: **a.donauskas@hostinger.com** with `[WaCRM conduct]` in the - Email: **a.donauskas@hostinger.com** with `[CRM template conduct]` in
subject. the subject.
Reports are handled confidentially. Expect an acknowledgement within Reports are handled confidentially. Expect an acknowledgement within
72 hours and a decision on next steps within a week. 72 hours and a decision on next steps within a week.

View File

@ -1,5 +1,5 @@
name: Bug report name: Bug report
description: Something in WaCRM isn't working the way the docs say it should. description: Something in the template isn't working the way the docs say it should.
title: "[bug] " title: "[bug] "
labels: ["bug", "triage"] labels: ["bug", "triage"]
body: body:

View File

@ -6,6 +6,6 @@ contact_links:
- name: Setup / "how do I..." questions - name: Setup / "how do I..." questions
url: https://github.com/ArnasDon/wacrm/blob/main/docs/README.md url: https://github.com/ArnasDon/wacrm/blob/main/docs/README.md
about: Check the docs first — setup, deploy, troubleshooting are all covered. about: Check the docs first — setup, deploy, troubleshooting are all covered.
- name: Using WaCRM as a template (forking) - name: Using this as a template (forking)
url: https://github.com/ArnasDon/wacrm/blob/main/CONTRIBUTING.md url: https://github.com/ArnasDon/wacrm/blob/main/CONTRIBUTING.md
about: WaCRM is a template. Most changes belong in your fork, not an upstream issue — here's how that works. about: This is a template. Most changes belong in your fork, not an upstream issue — here's how that works.

View File

@ -8,7 +8,7 @@ body:
value: | value: |
Thanks for the idea — but read this first. Thanks for the idea — but read this first.
WaCRM is a **template**, not a collaborative product. The This is a **template**, not a collaborative product. The
upstream scope is intentionally narrow, so most feature upstream scope is intentionally narrow, so most feature
requests end up as *"build this in your fork"* rather than requests end up as *"build this in your fork"* rather than
landing here. That's the point of a template. landing here. That's the point of a template.
@ -29,7 +29,7 @@ body:
id: problem id: problem
attributes: attributes:
label: What's the problem? label: What's the problem?
description: What are you trying to do today that WaCRM makes harder than it should? description: What are you trying to do today that the template makes harder than it should?
placeholder: | placeholder: |
When a broadcast ends I have no way to see which recipients When a broadcast ends I have no way to see which recipients
didn't reply so I can follow up manually. didn't reply so I can follow up manually.

6
.github/SECURITY.md vendored
View File

@ -1,6 +1,6 @@
# Security Policy # Security Policy
Thanks for taking the time to look into WaCRM's security. Thanks for taking the time to look into the security of this template.
## Reporting a vulnerability ## Reporting a vulnerability
@ -12,7 +12,7 @@ Instead, please report privately via one of:
- [GitHub Security Advisories](https://github.com/ArnasDon/wacrm/security/advisories/new) - [GitHub Security Advisories](https://github.com/ArnasDon/wacrm/security/advisories/new)
(preferred — keeps the disclosure, fix, and CVE all in one place). (preferred — keeps the disclosure, fix, and CVE all in one place).
- Email: `a.donauskas@hostinger.com` with `[WaCRM security]` in the subject. - Email: `a.donauskas@hostinger.com` with `[CRM template security]` in the subject.
Include, if you can: Include, if you can:
@ -59,4 +59,4 @@ action against anyone who:
- Gives us reasonable time to respond before any public disclosure. - Gives us reasonable time to respond before any public disclosure.
- Doesn't exploit the issue beyond what's necessary to demonstrate it. - Doesn't exploit the issue beyond what's necessary to demonstrate it.
Thanks for helping keep WaCRM (and its forks) safe. Thanks for helping keep this template (and its forks) safe.

View File

@ -1,5 +1,5 @@
<!-- <!--
Heads up: WaCRM is a template, not a collaborative product. Most Heads up: this is a template, not a collaborative product. Most
changes belong in your fork. See CONTRIBUTING.md for which kinds of changes belong in your fork. See CONTRIBUTING.md for which kinds of
upstream PRs tend to land (security, correctness, docs) vs. which upstream PRs tend to land (security, correctness, docs) vs. which
belong in a fork (new features, stack swaps, opinionated refactors). belong in a fork (new features, stack swaps, opinionated refactors).

View File

@ -1,6 +1,6 @@
# Using WaCRM # Using this template
WaCRM is a **template repository**, not a collaborative product. The This is a **template repository**, not a collaborative product. The
expected flow is: expected flow is:
1. **Fork** it to your own GitHub account or organisation. 1. **Fork** it to your own GitHub account or organisation.
@ -95,9 +95,9 @@ closed — open the issue first to align.
## If you maintain a public fork ## If you maintain a public fork
- Rebrand. The WaCRM name, favicon, and `wacrm.tech` URL belong to the - Rebrand. The "CRM Template for WhatsApp" name, favicon, and
upstream project; please swap them for your own before putting your `wacrm.tech` URL belong to the upstream project; please swap them
deployment in front of users. for your own before putting your deployment in front of users.
- Keep the MIT [`LICENSE`](./LICENSE) file — that's how the template's - Keep the MIT [`LICENSE`](./LICENSE) file — that's how the template's
permissions travel with the code. Attribution in a `README` section permissions travel with the code. Attribution in a `README` section
is appreciated but not required. is appreciated but not required.

View File

@ -1,10 +1,10 @@
# WaCRM # CRM Template for WhatsApp
> Self-hostable WhatsApp® CRM template — shared inbox, contacts, sales > Self-hostable CRM template for WhatsApp® — shared inbox, contacts,
> pipelines, broadcasts, and no-code automations. Fork it, brand it, > sales pipelines, broadcasts, and no-code automations. Fork it, brand
> host it. > it, host it.
[![License: MIT](https://img.shields.io/badge/License-MIT-emerald.svg)](./LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-violet.svg)](./LICENSE)
[![CI](https://github.com/ArnasDon/wacrm/actions/workflows/ci.yml/badge.svg)](https://github.com/ArnasDon/wacrm/actions/workflows/ci.yml) [![CI](https://github.com/ArnasDon/wacrm/actions/workflows/ci.yml/badge.svg)](https://github.com/ArnasDon/wacrm/actions/workflows/ci.yml)
[![Next.js 16](https://img.shields.io/badge/Next.js-16-black?logo=nextdotjs)](https://nextjs.org) [![Next.js 16](https://img.shields.io/badge/Next.js-16-black?logo=nextdotjs)](https://nextjs.org)
[![Supabase](https://img.shields.io/badge/Supabase-Postgres%20%2B%20Auth-3ecf8e?logo=supabase)](https://supabase.com) [![Supabase](https://img.shields.io/badge/Supabase-Postgres%20%2B%20Auth-3ecf8e?logo=supabase)](https://supabase.com)
@ -28,7 +28,7 @@
## Why fork this? ## Why fork this?
WaCRM is a **template**, not a product. Forking means you get: This is a **template**, not a product. Forking means you get:
- **Full ownership** — your code, your Supabase project, your domain, - **Full ownership** — your code, your Supabase project, your domain,
your data. No SaaS lock-in, no seat pricing, no trust dance. your data. No SaaS lock-in, no seat pricing, no trust dance.

View File

@ -1,7 +1,7 @@
# WaCRM documentation # Documentation
WaCRM is a self-hostable WhatsApp® CRM built on Next.js and Supabase. These A self-hostable CRM template for WhatsApp® built on Next.js and Supabase.
docs walk you from a freshly forked repo to a production deploy. These docs walk you from a freshly forked repo to a production deploy.
## Reading order ## Reading order

View File

@ -1,6 +1,6 @@
# Automations cron # Automations cron
WaCRM's **Automations** module lets you build flows that react to WhatsApp® The **Automations** module lets you build flows that react to WhatsApp®
events (new message, new contact, keyword match, schedule, etc.). Most events (new message, new contact, keyword match, schedule, etc.). Most
steps run inline — the exception is the **Wait** step, which parks the steps run inline — the exception is the **Wait** step, which parks the
execution in `automation_pending_executions` until its due time. execution in `automation_pending_executions` until its due time.

View File

@ -1,7 +1,7 @@
# Deploy on Hostinger # Deploy on Hostinger
Hostinger's **[Managed Node.js Hosting](https://www.hostinger.com/web-apps-hosting)** Hostinger's **[Managed Node.js Hosting](https://www.hostinger.com/web-apps-hosting)**
is the recommended way to run WaCRM in production: Hostinger patches the 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 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 flow from hPanel. You deploy from your fork without ever opening an SSH
session if you don't want to. session if you don't want to.
@ -93,7 +93,7 @@ baked into the client bundle.
From the app page, click **Restart application** (or run the equivalent From the app page, click **Restart application** (or run the equivalent
from the terminal). hPanel boots `npm start` behind its own reverse proxy 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 domain you configured. Hit the URL in a browser — you should land
on the WaCRM marketing page. on the marketing page.
SSL is provisioned automatically. If you used a subdomain, Hostinger's 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 AutoSSL usually takes a minute or two; until then the site may serve the
@ -139,7 +139,7 @@ idempotent.
## When to reach for a VPS instead ## When to reach for a VPS instead
Managed Node.js covers most WaCRM deploys. Consider Managed Node.js covers most deploys of this template. Consider
[Hostinger VPS](https://www.hostinger.com/vps-hosting) if you need: [Hostinger VPS](https://www.hostinger.com/vps-hosting) if you need:
- Long-running background workers beyond what the single Next.js process - Long-running background workers beyond what the single Next.js process

View File

@ -1,6 +1,6 @@
# Getting started # Getting started
Run WaCRM locally in about 15 minutes. This guide assumes Node.js 20+ and Run the template locally in about 15 minutes. This guide assumes Node.js 20+ and
npm are already installed. npm are already installed.
## 1. Fork and clone ## 1. Fork and clone

View File

@ -1,8 +1,7 @@
# Supabase setup # Supabase setup
WaCRM uses Supabase for Postgres, authentication, row-level security (RLS), This template uses Supabase for Postgres, authentication, row-level security (RLS),
and (optionally) storage. You need one Supabase project per WaCRM and (optionally) storage. You need one Supabase project per deployment.
deployment.
## 1. Create the project ## 1. Create the project
@ -75,7 +74,7 @@ emails link back correctly.
## 6. (Optional) Storage ## 6. (Optional) Storage
WaCRM downloads WhatsApp® media through Meta's `/download` endpoint and The app downloads WhatsApp® media through Meta's `/download` endpoint and
currently relays it on demand rather than caching it in Supabase Storage. currently relays it on demand rather than caching it in Supabase Storage.
No bucket setup is required for a default install. No bucket setup is required for a default install.

View File

@ -46,7 +46,7 @@ Set `NEXT_PUBLIC_SITE_URL` **and** add the same origin to Supabase →
### Webhook verification fails in Meta ### Webhook verification fails in Meta
- The **verify token** in Meta must match exactly what you saved in - The **verify token** in Meta must match exactly what you saved in
**Settings → WhatsApp** inside WaCRM. **Settings → WhatsApp** inside the app.
- The callback URL must be reachable without auth. Test it yourself: - 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=<token>&hub.challenge=test'` `curl 'https://crm.example.com/api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=<token>&hub.challenge=test'`
should return `test`. should return `test`.
@ -91,7 +91,7 @@ endpoint is fine and the problem is just the schedule.
### An automation fires twice for the same message ### An automation fires twice for the same message
Most likely two WaCRM instances share one Supabase project and both Most likely two app instances share one Supabase project and both
receive the same webhook. The webhook handler deduplicates by Meta's receive the same webhook. The webhook handler deduplicates by Meta's
`wamid`, but only within a single deploy — split-brain setups can double `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. up. Either consolidate to one deploy or route the webhook to exactly one.

View File

@ -1,6 +1,6 @@
# WhatsApp setup # WhatsApp setup
WaCRM talks to the official **WhatsApp® Business Cloud API** (Meta). Each This template talks to the official **WhatsApp® Business Cloud API** (Meta). Each
account stores its own `phone_number_id`, `waba_id`, and encrypted account stores its own `phone_number_id`, `waba_id`, and encrypted
`access_token` — there is no shared app-level token. This page walks `access_token` — there is no shared app-level token. This page walks
through getting those values and wiring the webhook. through getting those values and wiring the webhook.
@ -45,9 +45,9 @@ generate a **System User** access token:
- `whatsapp_business_messaging` - `whatsapp_business_messaging`
4. Copy the token. **This is shown only once.** 4. Copy the token. **This is shown only once.**
## 4. Connect it inside WaCRM ## 4. Connect it inside the app
1. Run WaCRM locally (or open your deployed instance). 1. Run the app locally (or open your deployed instance).
2. Sign in, then go to **Settings → WhatsApp**. 2. Sign in, then go to **Settings → WhatsApp**.
3. Fill in: 3. Fill in:
- **Phone number ID** — from step 3. - **Phone number ID** — from step 3.
@ -55,7 +55,7 @@ generate a **System User** access token:
- **Access token** — the System User token from step 3. - **Access token** — the System User token from step 3.
- **Verify token** — any random string you make up; you will paste the - **Verify token** — any random string you make up; you will paste the
same value into Meta in the next step. same value into Meta in the next step.
4. Save. WaCRM encrypts the access token and verify token with your 4. Save. The app encrypts the access token and verify token with your
`ENCRYPTION_KEY` before writing them to Supabase. `ENCRYPTION_KEY` before writing them to Supabase.
## 5. Configure the webhook in Meta ## 5. Configure the webhook in Meta
@ -68,7 +68,7 @@ Under **WhatsApp → Configuration → Webhook**:
- Production: `https://<your-domain>/api/whatsapp/webhook`. - Production: `https://<your-domain>/api/whatsapp/webhook`.
- **Verify token**: the same string you entered in step 4. - **Verify token**: the same string you entered in step 4.
- Click **Verify and save**. Meta sends a GET with - Click **Verify and save**. Meta sends a GET with
`hub.challenge` — WaCRM's webhook route decrypts stored verify tokens `hub.challenge` — the app's webhook route decrypts stored verify tokens
and echoes the challenge back. If verification fails, double-check the and echoes the challenge back. If verification fails, double-check the
verify token matches and the callback URL is publicly reachable. verify token matches and the callback URL is publicly reachable.
@ -86,7 +86,7 @@ sync on the broadcast UI.
## 6. Signature verification (required) ## 6. Signature verification (required)
Set `META_APP_SECRET` to your app's **App Secret** (Meta for Developers → Set `META_APP_SECRET` to your app's **App Secret** (Meta for Developers →
App Settings → Basic). WaCRM validates the `X-Hub-Signature-256` HMAC 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 on every inbound webhook and **rejects every request if the env var is
not set** — anyone who knew the webhook URL could otherwise spoof not set** — anyone who knew the webhook URL could otherwise spoof
messages and status updates. Configure it before pointing the webhook messages and status updates. Configure it before pointing the webhook
@ -96,9 +96,8 @@ at a deployed instance.
1. From Meta's **API Setup** page, send a test message to a number that is 1. From Meta's **API Setup** page, send a test message to a number that is
a registered tester. a registered tester.
2. Watch the WaCRM Inbox — the message should appear within a second or 2. Watch the Inbox — the message should appear within a second or two.
two. 3. Reply from the app; the recipient gets a real WhatsApp message.
3. Reply from WaCRM; the recipient gets a real WhatsApp message.
If nothing shows up, see [troubleshooting.md](./troubleshooting.md). If nothing shows up, see [troubleshooting.md](./troubleshooting.md).

View File

@ -2,7 +2,7 @@
"name": "wacrm", "name": "wacrm",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"description": "Self-hostable WhatsApp CRM built on Next.js and Supabase — shared inbox, contacts, sales pipelines, broadcasts, and no-code automations.", "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", "license": "MIT",
"author": "Arnas Donauskas", "author": "Arnas Donauskas",
"homepage": "https://wacrm.tech", "homepage": "https://wacrm.tech",

View File

@ -46,8 +46,8 @@ export default function ForgotPasswordPage() {
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-slate-800 bg-slate-900">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-violet-500/10">
<CheckCircle className="h-6 w-6 text-emerald-500" /> <CheckCircle className="h-6 w-6 text-violet-500" />
</div> </div>
<CardTitle className="text-xl text-white"> <CardTitle className="text-xl text-white">
Check your email Check your email
@ -77,8 +77,8 @@ export default function ForgotPasswordPage() {
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-slate-800 bg-slate-900">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-violet-500/10">
<MessageSquare className="h-6 w-6 text-emerald-500" /> <MessageSquare className="h-6 w-6 text-violet-500" />
</div> </div>
<CardTitle className="text-xl text-white">Reset password</CardTitle> <CardTitle className="text-xl text-white">Reset password</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
@ -104,14 +104,14 @@ export default function ForgotPasswordPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-emerald-500 focus-visible:ring-emerald-500/20" className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-violet-500 focus-visible:ring-violet-500/20"
/> />
</div> </div>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="mt-2 h-10 w-full bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50" className="mt-2 h-10 w-full bg-violet-600 text-white hover:bg-violet-500 disabled:opacity-50"
> >
{loading ? "Sending..." : "Send reset link"} {loading ? "Sending..." : "Send reset link"}
</Button> </Button>

View File

@ -47,12 +47,12 @@ export default function LoginPage() {
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-slate-800 bg-slate-900">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-violet-500/10">
<MessageSquare className="h-6 w-6 text-emerald-500" /> <MessageSquare className="h-6 w-6 text-violet-500" />
</div> </div>
<CardTitle className="text-xl text-white">Welcome back</CardTitle> <CardTitle className="text-xl text-white">Welcome back</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
Sign in to your WaCRM account Sign in to your account
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -74,7 +74,7 @@ export default function LoginPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-emerald-500 focus-visible:ring-emerald-500/20" className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-violet-500 focus-visible:ring-violet-500/20"
/> />
</div> </div>
@ -85,7 +85,7 @@ export default function LoginPage() {
</Label> </Label>
<Link <Link
href="/forgot-password" href="/forgot-password"
className="text-sm text-emerald-500 hover:text-emerald-400" className="text-sm text-violet-500 hover:text-violet-400"
> >
Forgot password? Forgot password?
</Link> </Link>
@ -97,14 +97,14 @@ export default function LoginPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-emerald-500 focus-visible:ring-emerald-500/20" className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-violet-500 focus-visible:ring-violet-500/20"
/> />
</div> </div>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="mt-2 h-10 w-full bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50" className="mt-2 h-10 w-full bg-violet-600 text-white hover:bg-violet-500 disabled:opacity-50"
> >
{loading ? "Signing in..." : "Sign in"} {loading ? "Signing in..." : "Sign in"}
</Button> </Button>
@ -114,7 +114,7 @@ export default function LoginPage() {
Don&apos;t have an account?{" "} Don&apos;t have an account?{" "}
<Link <Link
href="/signup" href="/signup"
className="text-emerald-500 hover:text-emerald-400" className="text-violet-500 hover:text-violet-400"
> >
Create account Create account
</Link> </Link>

View File

@ -66,8 +66,8 @@ export default function SignupPage() {
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-slate-800 bg-slate-900">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-violet-500/10">
<CheckCircle className="h-6 w-6 text-emerald-500" /> <CheckCircle className="h-6 w-6 text-violet-500" />
</div> </div>
<CardTitle className="text-xl text-white"> <CardTitle className="text-xl text-white">
Check your email Check your email
@ -97,12 +97,12 @@ export default function SignupPage() {
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-slate-800 bg-slate-900">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-violet-500/10">
<MessageSquare className="h-6 w-6 text-emerald-500" /> <MessageSquare className="h-6 w-6 text-violet-500" />
</div> </div>
<CardTitle className="text-xl text-white">Create account</CardTitle> <CardTitle className="text-xl text-white">Create account</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
Get started with WaCRM Get started with CRM Template for WhatsApp
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -124,7 +124,7 @@ export default function SignupPage() {
value={fullName} value={fullName}
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setFullName(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-emerald-500 focus-visible:ring-emerald-500/20" className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-violet-500 focus-visible:ring-violet-500/20"
/> />
</div> </div>
@ -139,7 +139,7 @@ export default function SignupPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-emerald-500 focus-visible:ring-emerald-500/20" className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-violet-500 focus-visible:ring-violet-500/20"
/> />
</div> </div>
@ -154,7 +154,7 @@ export default function SignupPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-emerald-500 focus-visible:ring-emerald-500/20" className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-violet-500 focus-visible:ring-violet-500/20"
/> />
</div> </div>
@ -169,14 +169,14 @@ export default function SignupPage() {
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-emerald-500 focus-visible:ring-emerald-500/20" className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-violet-500 focus-visible:ring-violet-500/20"
/> />
</div> </div>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="mt-2 h-10 w-full bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50" className="mt-2 h-10 w-full bg-violet-600 text-white hover:bg-violet-500 disabled:opacity-50"
> >
{loading ? "Creating account..." : "Create account"} {loading ? "Creating account..." : "Create account"}
</Button> </Button>
@ -186,7 +186,7 @@ export default function SignupPage() {
Already have an account?{" "} Already have an account?{" "}
<Link <Link
href="/login" href="/login"
className="text-emerald-500 hover:text-emerald-400" className="text-violet-500 hover:text-violet-400"
> >
Sign in Sign in
</Link> </Link>

View File

@ -54,7 +54,7 @@ export default function EditAutomationPage({
<p className="text-sm text-red-400">{error}</p> <p className="text-sm text-red-400">{error}</p>
<button <button
onClick={() => router.push("/automations")} onClick={() => router.push("/automations")}
className="text-sm text-emerald-400 hover:text-emerald-300" className="text-sm text-violet-400 hover:text-violet-300"
> >
Back to Automations Back to Automations
</button> </button>
@ -65,7 +65,7 @@ export default function EditAutomationPage({
if (!initial) { if (!initial) {
return ( return (
<div className="flex h-screen items-center justify-center"> <div className="flex h-screen items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-emerald-500" /> <Loader2 className="h-6 w-6 animate-spin text-violet-500" />
</div> </div>
) )
} }

View File

@ -76,7 +76,7 @@ export default function AutomationLogsPage({
if (!automation || logs === null) { if (!automation || logs === null) {
return ( return (
<div className="flex h-64 items-center justify-center"> <div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-emerald-500" /> <Loader2 className="h-6 w-6 animate-spin text-violet-500" />
</div> </div>
) )
} }
@ -167,7 +167,7 @@ export default function AutomationLogsPage({
function StatusBadge({ status }: { status: AutomationLog["status"] }) { function StatusBadge({ status }: { status: AutomationLog["status"] }) {
const classes = const classes =
status === "success" status === "success"
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-300" ? "border-violet-500/30 bg-violet-500/10 text-violet-300"
: status === "partial" : status === "partial"
? "border-amber-500/30 bg-amber-500/10 text-amber-300" ? "border-amber-500/30 bg-amber-500/10 text-amber-300"
: "border-red-500/30 bg-red-500/10 text-red-300" : "border-red-500/30 bg-red-500/10 text-red-300"
@ -190,7 +190,7 @@ function StepRow({ result }: { result: AutomationLogStepResult }) {
<span <span
className={cn( className={cn(
"mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full", "mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full",
ok ? "bg-emerald-500/20 text-emerald-400" : "bg-red-500/20 text-red-400", ok ? "bg-violet-500/20 text-violet-400" : "bg-red-500/20 text-red-400",
)} )}
aria-hidden aria-hidden
> >

View File

@ -146,7 +146,7 @@ export default function AutomationsPage() {
if (automations === null) { if (automations === null) {
return ( return (
<div className="flex h-64 items-center justify-center"> <div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-emerald-500" /> <Loader2 className="h-6 w-6 animate-spin text-violet-500" />
</div> </div>
) )
} }
@ -164,7 +164,7 @@ export default function AutomationsPage() {
</div> </div>
<Button <Button
onClick={() => router.push("/automations/new")} onClick={() => router.push("/automations/new")}
className="bg-emerald-600 text-white hover:bg-emerald-700" className="bg-violet-600 text-white hover:bg-violet-700"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Create Automation Create Automation
@ -182,9 +182,9 @@ export default function AutomationsPage() {
<button <button
key={slug} key={slug}
onClick={() => startFromTemplate(slug)} onClick={() => startFromTemplate(slug)}
className="group flex flex-col items-start rounded-xl border border-slate-800 bg-slate-900 p-4 text-left transition-colors hover:border-emerald-500/50 hover:bg-slate-900/80" className="group flex flex-col items-start rounded-xl border border-slate-800 bg-slate-900 p-4 text-left transition-colors hover:border-violet-500/50 hover:bg-slate-900/80"
> >
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400 group-hover:bg-emerald-500/15"> <div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400 group-hover:bg-violet-500/15">
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
</div> </div>
<div className="text-sm font-semibold text-white">{t.name}</div> <div className="text-sm font-semibold text-white">{t.name}</div>
@ -198,8 +198,8 @@ export default function AutomationsPage() {
{automations.length === 0 ? ( {automations.length === 0 ? (
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-slate-800 bg-slate-900/40"> <div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-slate-800 bg-slate-900/40">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10"> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-500/10">
<Zap className="h-6 w-6 text-emerald-500" /> <Zap className="h-6 w-6 text-violet-500" />
</div> </div>
<p className="mt-3 text-sm font-medium text-white">No automations yet</p> <p className="mt-3 text-sm font-medium text-white">No automations yet</p>
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-slate-400">
@ -275,10 +275,10 @@ function AutomationCard({
<li className="rounded-xl border border-slate-800 bg-slate-900 transition-colors hover:border-slate-700"> <li className="rounded-xl border border-slate-800 bg-slate-900 transition-colors hover:border-slate-700">
<div className="flex items-center gap-4 p-4"> <div className="flex items-center gap-4 p-4">
<div <div
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-emerald-500/10" className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-violet-500/10"
aria-hidden aria-hidden
> >
<Zap className="h-5 w-5 text-emerald-400" /> <Zap className="h-5 w-5 text-violet-400" />
</div> </div>
<button <button
@ -292,8 +292,8 @@ function AutomationCard({
</span> </span>
{automation.is_active && ( {automation.is_active && (
<span className="relative flex h-2 w-2" aria-label="active"> <span className="relative flex h-2 w-2" aria-label="active">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" /> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-violet-500" />
</span> </span>
)} )}
</div> </div>

View File

@ -246,7 +246,7 @@ export default function BroadcastDetailPage() {
if (loading) { if (loading) {
return ( return (
<div className="flex h-64 items-center justify-center"> <div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-emerald-500" /> <Loader2 className="h-6 w-6 animate-spin text-violet-500" />
</div> </div>
); );
} }
@ -265,7 +265,7 @@ export default function BroadcastDetailPage() {
const status = getBroadcastStatus(broadcast.status); const status = getBroadcastStatus(broadcast.status);
const funnelSteps: FunnelStep[] = [ const funnelSteps: FunnelStep[] = [
{ label: 'Sent', value: broadcast.sent_count, color: 'bg-emerald-500' }, { label: 'Sent', value: broadcast.sent_count, color: 'bg-violet-500' },
{ label: 'Delivered', value: broadcast.delivered_count, color: 'bg-teal-500' }, { label: 'Delivered', value: broadcast.delivered_count, color: 'bg-teal-500' },
{ label: 'Read', value: broadcast.read_count, color: 'bg-blue-500' }, { label: 'Read', value: broadcast.read_count, color: 'bg-blue-500' },
{ label: 'Replied', value: broadcast.replied_count, color: 'bg-indigo-500' }, { label: 'Replied', value: broadcast.replied_count, color: 'bg-indigo-500' },
@ -361,7 +361,7 @@ export default function BroadcastDetailPage() {
value={broadcast.sent_count} value={broadcast.sent_count}
total={broadcast.total_recipients} total={broadcast.total_recipients}
icon={<Send className="h-4 w-4" />} icon={<Send className="h-4 w-4" />}
color="bg-emerald-500/10 text-emerald-400" color="bg-violet-500/10 text-violet-400"
/> />
<StatCard <StatCard
label="Delivered" label="Delivered"
@ -423,7 +423,7 @@ export default function BroadcastDetailPage() {
<DropdownMenuItem <DropdownMenuItem
onClick={() => setStatusFilter('all')} onClick={() => setStatusFilter('all')}
className={ className={
statusFilter === 'all' ? 'text-emerald-400' : 'text-slate-300' statusFilter === 'all' ? 'text-violet-400' : 'text-slate-300'
} }
> >
All statuses All statuses
@ -434,7 +434,7 @@ export default function BroadcastDetailPage() {
onClick={() => setStatusFilter(s)} onClick={() => setStatusFilter(s)}
className={ className={
statusFilter === s statusFilter === s
? 'text-emerald-400' ? 'text-violet-400'
: 'text-slate-300' : 'text-slate-300'
} }
> >

View File

@ -140,9 +140,9 @@ export default function NewBroadcastPage() {
<div <div
className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium transition-all ${ className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium transition-all ${
isCompleted isCompleted
? 'bg-emerald-500 text-white' ? 'bg-violet-500 text-white'
: isActive : isActive
? 'border-2 border-emerald-500 bg-emerald-500/10 text-emerald-400' ? 'border-2 border-violet-500 bg-violet-500/10 text-violet-400'
: 'border border-slate-700 bg-slate-800 text-slate-500' : 'border border-slate-700 bg-slate-800 text-slate-500'
}`} }`}
> >
@ -150,7 +150,7 @@ export default function NewBroadcastPage() {
</div> </div>
<span <span
className={`hidden text-sm font-medium sm:block ${ className={`hidden text-sm font-medium sm:block ${
isActive ? 'text-white' : isCompleted ? 'text-emerald-400' : 'text-slate-500' isActive ? 'text-white' : isCompleted ? 'text-violet-400' : 'text-slate-500'
}`} }`}
> >
{step.label} {step.label}
@ -159,7 +159,7 @@ export default function NewBroadcastPage() {
{index < steps.length - 1 && ( {index < steps.length - 1 && (
<div <div
className={`mx-3 h-px flex-1 ${ className={`mx-3 h-px flex-1 ${
index < currentStep ? 'bg-emerald-500' : 'bg-slate-800' index < currentStep ? 'bg-violet-500' : 'bg-slate-800'
}`} }`}
/> />
)} )}

View File

@ -35,7 +35,7 @@ function RateCell({
}: { }: {
value: number; value: number;
total: number; total: number;
/** Tailwind bg class for the fill, e.g. "bg-emerald-500" */ /** Tailwind bg class for the fill, e.g. "bg-violet-500" */
color: string; color: string;
}) { }) {
const pct = percent(value, total); const pct = percent(value, total);
@ -128,7 +128,7 @@ export default function BroadcastsPage() {
if (loading) { if (loading) {
return ( return (
<div className="flex h-64 items-center justify-center"> <div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-emerald-500" /> <Loader2 className="h-6 w-6 animate-spin text-violet-500" />
</div> </div>
); );
} }
@ -154,7 +154,7 @@ export default function BroadcastsPage() {
aria-label="Broadcast in progress" aria-label="Broadcast in progress"
className="broadcast-indeterminate fixed inset-x-0 top-0 z-40 h-0.5 overflow-hidden bg-slate-800" className="broadcast-indeterminate fixed inset-x-0 top-0 z-40 h-0.5 overflow-hidden bg-slate-800"
> >
<div className="broadcast-indeterminate-bar h-0.5 bg-emerald-500" /> <div className="broadcast-indeterminate-bar h-0.5 bg-violet-500" />
<style jsx>{` <style jsx>{`
.broadcast-indeterminate-bar { .broadcast-indeterminate-bar {
width: 33%; width: 33%;
@ -183,7 +183,7 @@ export default function BroadcastsPage() {
</div> </div>
<Button <Button
onClick={() => router.push('/broadcasts/new')} onClick={() => router.push('/broadcasts/new')}
className="bg-emerald-600 text-white hover:bg-emerald-700" className="bg-violet-600 text-white hover:bg-violet-700"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
New Broadcast New Broadcast
@ -199,7 +199,7 @@ export default function BroadcastsPage() {
</p> </p>
<Button <Button
onClick={() => router.push('/broadcasts/new')} onClick={() => router.push('/broadcasts/new')}
className="mt-4 bg-emerald-600 text-white hover:bg-emerald-700" className="mt-4 bg-violet-600 text-white hover:bg-violet-700"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
New Broadcast New Broadcast
@ -243,7 +243,7 @@ export default function BroadcastsPage() {
<RateCell <RateCell
value={broadcast.delivered_count} value={broadcast.delivered_count}
total={broadcast.total_recipients} total={broadcast.total_recipients}
color="bg-emerald-500" color="bg-violet-500"
/> />
</TableCell> </TableCell>
<TableCell className="hidden lg:table-cell"> <TableCell className="hidden lg:table-cell">

View File

@ -226,7 +226,7 @@ export default function ContactsPage() {
</Button> </Button>
<Button <Button
onClick={openAddForm} onClick={openAddForm}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
<Plus className="size-4" /> <Plus className="size-4" />
Add Contact Add Contact
@ -269,7 +269,7 @@ export default function ContactsPage() {
<TableRow className="border-slate-800"> <TableRow className="border-slate-800">
<TableCell colSpan={7} className="text-center py-12"> <TableCell colSpan={7} className="text-center py-12">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<Loader2 className="size-6 animate-spin text-emerald-500" /> <Loader2 className="size-6 animate-spin text-violet-500" />
<p className="text-sm text-slate-500">Loading contacts...</p> <p className="text-sm text-slate-500">Loading contacts...</p>
</div> </div>
</TableCell> </TableCell>

View File

@ -29,7 +29,7 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
return ( return (
<div className="flex h-screen items-center justify-center bg-slate-950"> <div className="flex h-screen items-center justify-center bg-slate-950">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-emerald-500 border-t-transparent" /> <div className="h-8 w-8 animate-spin rounded-full border-2 border-violet-500 border-t-transparent" />
<p className="text-sm text-slate-400">Loading...</p> <p className="text-sm text-slate-400">Loading...</p>
</div> </div>
</div> </div>

View File

@ -302,7 +302,7 @@ export default function PipelinesPage() {
<DropdownMenuTrigger <DropdownMenuTrigger
className="inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white hover:bg-slate-800 transition-colors data-[popup-open]:bg-slate-800" className="inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white hover:bg-slate-800 transition-colors data-[popup-open]:bg-slate-800"
> >
<GitBranch className="h-4 w-4 text-emerald-500" /> <GitBranch className="h-4 w-4 text-violet-500" />
<span className="font-semibold"> <span className="font-semibold">
{selectedPipeline?.name ?? "Select Pipeline"} {selectedPipeline?.name ?? "Select Pipeline"}
</span> </span>
@ -323,7 +323,7 @@ export default function PipelinesPage() {
onClick={() => setSelectedPipelineId(p.id)} onClick={() => setSelectedPipelineId(p.id)}
className={ className={
p.id === selectedPipelineId p.id === selectedPipelineId
? "text-emerald-400" ? "text-violet-400"
: "text-slate-300" : "text-slate-300"
} }
> >
@ -357,7 +357,7 @@ export default function PipelinesPage() {
<Button <Button
onClick={() => handleAddDeal()} onClick={() => handleAddDeal()}
disabled={!selectedPipelineId || stages.length === 0} disabled={!selectedPipelineId || stages.length === 0}
className="bg-emerald-600 text-white hover:bg-emerald-700" className="bg-violet-600 text-white hover:bg-violet-700"
> >
<Plus className="mr-1 h-4 w-4" /> <Plus className="mr-1 h-4 w-4" />
Add Deal Add Deal
@ -377,7 +377,7 @@ export default function PipelinesPage() {
</p> </p>
<Button <Button
onClick={() => setNewPipelineOpen(true)} onClick={() => setNewPipelineOpen(true)}
className="mt-4 bg-emerald-600 text-white hover:bg-emerald-700" className="mt-4 bg-violet-600 text-white hover:bg-violet-700"
> >
<Plus className="mr-1 h-4 w-4" /> <Plus className="mr-1 h-4 w-4" />
Create Pipeline Create Pipeline
@ -428,7 +428,7 @@ export default function PipelinesPage() {
<Button <Button
onClick={handleCreatePipeline} onClick={handleCreatePipeline}
disabled={creating || !newPipelineName.trim()} disabled={creating || !newPipelineName.trim()}
className="bg-emerald-600 text-white hover:bg-emerald-700" className="bg-violet-600 text-white hover:bg-violet-700"
> >
{creating ? "Creating..." : "Create Pipeline"} {creating ? "Creating..." : "Create Pipeline"}
</Button> </Button>

View File

@ -48,28 +48,28 @@ export default function SettingsPage() {
<TabsList className="bg-slate-900 border border-slate-700"> <TabsList className="bg-slate-900 border border-slate-700">
<TabsTrigger <TabsTrigger
value="profile" value="profile"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
<User className="size-4" /> <User className="size-4" />
Profile Profile
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="whatsapp" value="whatsapp"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
<Settings className="size-4" /> <Settings className="size-4" />
WhatsApp Config WhatsApp Config
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="templates" value="templates"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
<MessageSquare className="size-4" /> <MessageSquare className="size-4" />
Templates Templates
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="tags" value="tags"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
<Tag className="size-4" /> <Tag className="size-4" />
Tags Tags

View File

@ -11,11 +11,11 @@ import { decrypt } from '@/lib/whatsapp/encryption'
* template locally, try to broadcast with it, and hit Meta's error * template locally, try to broadcast with it, and hit Meta's error
* #132001 "Template name does not exist in the translation" — because * #132001 "Template name does not exist in the translation" — because
* Meta had never seen the template, or had it approved under a * Meta had never seen the template, or had it approved under a
* different language code than what WaCRM stored. * different language code than what we stored locally.
* *
* This route pulls the source of truth (Meta's approved templates) * This route pulls the source of truth (Meta's approved templates)
* and upserts them into the local catalog by (user_id, name, language). * and upserts them into the local catalog by (user_id, name, language).
* After a sync, every template row in WaCRM is guaranteed to match * After a sync, every local template row is guaranteed to match
* something Meta will actually accept on send. * something Meta will actually accept on send.
* *
* Scope: * Scope:

View File

@ -37,7 +37,7 @@ export default async function DocPage({ params }: DocPageProps) {
return ( return (
<article className="mx-auto w-full max-w-3xl"> <article className="mx-auto w-full max-w-3xl">
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-400"> <p className="text-xs font-semibold uppercase tracking-wider text-violet-400">
{doc.section} {doc.section}
</p> </p>
<div <div

View File

@ -3,9 +3,12 @@ import { DocsShell } from '@/components/docs/shell'
import { listDocs } from '@/lib/docs/content' import { listDocs } from '@/lib/docs/content'
export const metadata: Metadata = { export const metadata: Metadata = {
title: { template: '%s — WaCRM Docs', default: 'WaCRM Docs' }, title: {
template: '%s — CRM Template for WhatsApp Docs',
default: 'CRM Template for WhatsApp Docs',
},
description: description:
'Setup, configuration, and deployment docs for the WaCRM template.', 'Setup, configuration, and deployment docs for the CRM Template for WhatsApp.',
} }
export default async function DocsLayout({ export default async function DocsLayout({

View File

@ -12,11 +12,11 @@ export default async function DocsIndexPage() {
return ( return (
<article className="mx-auto w-full max-w-3xl"> <article className="mx-auto w-full max-w-3xl">
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-400"> <p className="text-xs font-semibold uppercase tracking-wider text-violet-400">
Documentation Documentation
</p> </p>
<h1 className="mt-2 text-3xl font-bold tracking-tight text-white sm:text-4xl"> <h1 className="mt-2 text-3xl font-bold tracking-tight text-white sm:text-4xl">
Self-host WaCRM end to end Self-host the CRM template for WhatsApp end to end
</h1> </h1>
<p className="mt-4 text-base leading-relaxed text-slate-400"> <p className="mt-4 text-base leading-relaxed text-slate-400">
Everything you need to take the template from a fresh fork to a Everything you need to take the template from a fresh fork to a
@ -45,7 +45,7 @@ export default async function DocsIndexPage() {
{p.description} {p.description}
</p> </p>
)} )}
<span className="mt-4 inline-flex items-center gap-1.5 text-xs font-medium text-emerald-400 transition-colors group-hover:text-emerald-300"> <span className="mt-4 inline-flex items-center gap-1.5 text-xs font-medium text-violet-400 transition-colors group-hover:text-violet-300">
Read Read
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" /> <ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
</span> </span>

View File

@ -55,7 +55,7 @@
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.18 0.01 260); --popover: oklch(0.18 0.01 260);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.696 0.17 162); --primary: oklch(0.526 0.247 293);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.22 0.01 260); --secondary: oklch(0.22 0.01 260);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
@ -66,8 +66,8 @@
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.28 0.01 260); --border: oklch(0.28 0.01 260);
--input: oklch(0.28 0.01 260); --input: oklch(0.28 0.01 260);
--ring: oklch(0.696 0.17 162); --ring: oklch(0.526 0.247 293);
--chart-1: oklch(0.696 0.17 162); --chart-1: oklch(0.526 0.247 293);
--chart-2: oklch(0.556 0 0); --chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0); --chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0); --chart-4: oklch(0.371 0 0);
@ -75,12 +75,12 @@
--radius: 0.625rem; --radius: 0.625rem;
--sidebar: oklch(0.16 0.01 260); --sidebar: oklch(0.16 0.01 260);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.696 0.17 162); --sidebar-primary: oklch(0.526 0.247 293);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.22 0.01 260); --sidebar-accent: oklch(0.22 0.01 260);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.28 0.01 260); --sidebar-border: oklch(0.28 0.01 260);
--sidebar-ring: oklch(0.696 0.17 162); --sidebar-ring: oklch(0.526 0.247 293);
} }
@layer base { @layer base {

View File

@ -1,9 +1,9 @@
import { ImageResponse } from "next/og"; import { ImageResponse } from "next/og";
// Replaces the default Next.js favicon with the WaCRM mark — emerald // Replaces the default Next.js favicon with the brand mark — Hostinger
// rounded square + white chat-bubble glyph — matching the sidebar // violet rounded square + white chat-square glyph — matching the
// logo in `src/components/layout/sidebar.tsx`. Next.js renders this // sidebar logo in `src/components/layout/sidebar.tsx`. Next.js renders
// at build time and auto-injects <link rel="icon"> into <head>. // this at build time and auto-injects <link rel="icon"> into <head>.
// //
// This route takes precedence over src/app/favicon.ico, which is the // This route takes precedence over src/app/favicon.ico, which is the
// Next.js default and can stay on disk harmlessly (or be removed). // Next.js default and can stay on disk harmlessly (or be removed).
@ -22,7 +22,7 @@ export default function Icon() {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
background: "#10b981", // emerald-500 background: "#7c3aed", // violet-600 (Hostinger-aligned purple)
borderRadius: 6, borderRadius: 6,
}} }}
> >

View File

@ -38,7 +38,7 @@ export default async function Image() {
justifyContent: 'center', justifyContent: 'center',
padding: 80, padding: 80,
background: background:
'linear-gradient(135deg, #020617 0%, #0f172a 55%, #064e3b 100%)', 'linear-gradient(135deg, #020617 0%, #0f172a 55%, #2e1065 100%)',
color: 'white', color: 'white',
fontFamily: 'sans-serif', fontFamily: 'sans-serif',
}} }}
@ -49,7 +49,7 @@ export default async function Image() {
width: 64, width: 64,
height: 64, height: 64,
borderRadius: 14, borderRadius: 14,
background: '#10b981', background: '#7c3aed',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -78,7 +78,7 @@ export default async function Image() {
}} }}
> >
<span>Run your WhatsApp® business from&nbsp;</span> <span>Run your WhatsApp® business from&nbsp;</span>
<span style={{ color: '#34d399' }}>one inbox.</span> <span style={{ color: '#a78bfa' }}>one inbox.</span>
</div> </div>
<div <div

View File

@ -5,7 +5,7 @@ import { SITE_URL } from '@/lib/seo/site-config'
* robots.txt served at /robots.txt. We explicitly disallow every * robots.txt served at /robots.txt. We explicitly disallow every
* gated area so crawlers don't spend their budget on pages they'll * 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 * bounce off. Auth pages are closed off too — no SEO value, and
* we don't want "Sign in to WaCRM" competing with the landing page * we don't want a "Sign in" page competing with the landing page
* in SERPs. * in SERPs.
*/ */
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {

View File

@ -77,17 +77,17 @@ interface StepMeta {
} }
const STEP_META: Record<AutomationStepType, StepMeta> = { const STEP_META: Record<AutomationStepType, StepMeta> = {
send_message: { label: "Send Message", icon: MessageSquare, border: "border-l-emerald-500" }, send_message: { label: "Send Message", icon: MessageSquare, border: "border-l-violet-500" },
send_template: { label: "Send Template", icon: FileText, border: "border-l-emerald-500" }, send_template: { label: "Send Template", icon: FileText, border: "border-l-violet-500" },
add_tag: { label: "Add Tag", icon: Tag, border: "border-l-emerald-500" }, add_tag: { label: "Add Tag", icon: Tag, border: "border-l-violet-500" },
remove_tag: { label: "Remove Tag", icon: TagIcon, border: "border-l-emerald-500" }, remove_tag: { label: "Remove Tag", icon: TagIcon, border: "border-l-violet-500" },
assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-emerald-500" }, assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-violet-500" },
update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-emerald-500" }, update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-violet-500" },
create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-emerald-500" }, create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-violet-500" },
wait: { label: "Wait", icon: Hourglass, border: "border-l-slate-500" }, wait: { label: "Wait", icon: Hourglass, border: "border-l-slate-500" },
condition: { label: "Condition (If/Else)", icon: GitBranch, border: "border-l-amber-500" }, condition: { label: "Condition (If/Else)", icon: GitBranch, border: "border-l-amber-500" },
send_webhook: { label: "Send Webhook", icon: Webhook, border: "border-l-emerald-500" }, send_webhook: { label: "Send Webhook", icon: Webhook, border: "border-l-violet-500" },
close_conversation: { label: "Close Conversation", icon: CircleSlash, border: "border-l-emerald-500" }, close_conversation: { label: "Close Conversation", icon: CircleSlash, border: "border-l-violet-500" },
} }
const ADDABLE_STEPS: AutomationStepType[] = [ const ADDABLE_STEPS: AutomationStepType[] = [
@ -275,7 +275,7 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
<Button <Button
onClick={save} onClick={save}
disabled={saving} disabled={saving}
className="bg-emerald-600 text-white hover:bg-emerald-700" className="bg-violet-600 text-white hover:bg-violet-700"
> >
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : null} {saving ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
{isEditing ? "Save" : "Save Draft"} {isEditing ? "Save" : "Save Draft"}
@ -356,7 +356,7 @@ function TriggerCard({
<select <select
value={type} value={type}
onChange={(e) => onTypeChange(e.target.value as AutomationTriggerType)} onChange={(e) => onTypeChange(e.target.value as AutomationTriggerType)}
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:border-emerald-500 focus:outline-none" className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:border-violet-500 focus:outline-none"
> >
{TRIGGER_OPTIONS.map((o) => ( {TRIGGER_OPTIONS.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
@ -637,7 +637,7 @@ function ConditionBranches({
// cram each branch to ~170px which is too narrow for the nested // cram each branch to ~170px which is too narrow for the nested
// cards. Two-column grid returns on sm+. // cards. Two-column grid returns on sm+.
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
<BranchColumn label="Yes" color="text-emerald-400"> <BranchColumn label="Yes" color="text-violet-400">
<StepList {...props} steps={yes} parentPath={yesPath} /> <StepList {...props} steps={yes} parentPath={yesPath} />
</BranchColumn> </BranchColumn>
<BranchColumn label="No" color="text-rose-400"> <BranchColumn label="No" color="text-rose-400">
@ -670,7 +670,7 @@ function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
<div className="h-4 w-[2px] bg-slate-700" aria-hidden /> <div className="h-4 w-[2px] bg-slate-700" aria-hidden />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-dashed border-slate-700 bg-slate-950 text-slate-400 transition-colors hover:border-emerald-500 hover:bg-emerald-500/10 hover:text-emerald-400 data-[popup-open]:border-emerald-500 data-[popup-open]:bg-emerald-500/20 data-[popup-open]:text-emerald-400" className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-dashed border-slate-700 bg-slate-950 text-slate-400 transition-colors hover:border-violet-500 hover:bg-violet-500/10 hover:text-violet-400 data-[popup-open]:border-violet-500 data-[popup-open]:bg-violet-500/20 data-[popup-open]:text-violet-400"
aria-label="Add step" aria-label="Add step"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />

View File

@ -48,7 +48,7 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
if (loading) { if (loading) {
return ( return (
<div className="flex h-64 items-center justify-center"> <div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-emerald-500" /> <Loader2 className="h-6 w-6 animate-spin text-violet-500" />
</div> </div>
); );
} }
@ -88,7 +88,7 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
onClick={() => onSelect(template)} onClick={() => onSelect(template)}
className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${ className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${
isSelected isSelected
? 'border-emerald-500 bg-emerald-500/5 ring-1 ring-emerald-500/30' ? 'border-violet-500 bg-violet-500/5 ring-1 ring-violet-500/30'
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700 hover:bg-slate-900' : 'border-slate-800 bg-slate-900/50 hover:border-slate-700 hover:bg-slate-900'
}`} }`}
> >
@ -123,7 +123,7 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
<Button <Button
onClick={onNext} onClick={onNext}
disabled={!selectedTemplate} disabled={!selectedTemplate}
className="bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50" className="bg-violet-600 text-white hover:bg-violet-700 disabled:opacity-50"
> >
Next Next
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />

View File

@ -279,14 +279,14 @@ export function Step2SelectAudience({
} }
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${ className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
isSelected isSelected
? 'border-emerald-500 bg-emerald-500/5 ring-1 ring-emerald-500/30' ? 'border-violet-500 bg-violet-500/5 ring-1 ring-violet-500/30'
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700' : 'border-slate-800 bg-slate-900/50 hover:border-slate-700'
}`} }`}
> >
<div <div
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${ className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
isSelected isSelected
? 'bg-emerald-500/10 text-emerald-400' ? 'bg-violet-500/10 text-violet-400'
: 'bg-slate-800 text-slate-400' : 'bg-slate-800 text-slate-400'
}`} }`}
> >
@ -307,7 +307,7 @@ export function Step2SelectAudience({
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
<p className="mb-3 text-sm font-medium text-white">Select Tags</p> <p className="mb-3 text-sm font-medium text-white">Select Tags</p>
{loadingTags ? ( {loadingTags ? (
<Loader2 className="h-5 w-5 animate-spin text-emerald-500" /> <Loader2 className="h-5 w-5 animate-spin text-violet-500" />
) : tags.length === 0 ? ( ) : tags.length === 0 ? (
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
No tags found. Create tags in Settings. No tags found. Create tags in Settings.
@ -322,7 +322,7 @@ export function Step2SelectAudience({
onClick={() => toggleTag(tag.id)} onClick={() => toggleTag(tag.id)}
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${ className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
isSelected isSelected
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400' ? 'border-violet-500/30 bg-violet-500/10 text-violet-400'
: 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600' : 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600'
}`} }`}
> >
@ -343,7 +343,7 @@ export function Step2SelectAudience({
<div className="space-y-3 rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="space-y-3 rounded-xl border border-slate-800 bg-slate-900/50 p-4">
<p className="text-sm font-medium text-white">Custom Field Filter</p> <p className="text-sm font-medium text-white">Custom Field Filter</p>
{loadingFields ? ( {loadingFields ? (
<Loader2 className="h-5 w-5 animate-spin text-emerald-500" /> <Loader2 className="h-5 w-5 animate-spin text-violet-500" />
) : customFields.length === 0 ? ( ) : customFields.length === 0 ? (
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
No custom fields defined. Create one in Settings → Custom Fields. No custom fields defined. Create one in Settings → Custom Fields.
@ -353,7 +353,7 @@ export function Step2SelectAudience({
<select <select
value={audience.customField?.fieldId ?? ''} value={audience.customField?.fieldId ?? ''}
onChange={(e) => updateCustomField({ fieldId: e.target.value })} onChange={(e) => updateCustomField({ fieldId: e.target.value })}
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500"
> >
<option value="">Select field…</option> <option value="">Select field…</option>
{customFields.map((f) => ( {customFields.map((f) => (
@ -369,7 +369,7 @@ export function Step2SelectAudience({
operator: e.target.value as CustomFieldOperator, operator: e.target.value as CustomFieldOperator,
}) })
} }
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500"
> >
{OPERATOR_OPTIONS.map((op) => ( {OPERATOR_OPTIONS.map((op) => (
<option key={op.value} value={op.value}> <option key={op.value} value={op.value}>
@ -382,7 +382,7 @@ export function Step2SelectAudience({
value={audience.customField?.value ?? ''} value={audience.customField?.value ?? ''}
onChange={(e) => updateCustomField({ value: e.target.value })} onChange={(e) => updateCustomField({ value: e.target.value })}
placeholder="Value" placeholder="Value"
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none placeholder:text-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none placeholder:text-slate-500 focus:border-violet-500 focus:ring-1 focus:ring-violet-500"
/> />
</div> </div>
)} )}
@ -431,12 +431,12 @@ export function Step2SelectAudience({
<p className="mb-2 text-sm font-medium text-white">Audience Summary</p> <p className="mb-2 text-sm font-medium text-white">Audience Summary</p>
{loadingCount ? ( {loadingCount ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-emerald-500" /> <Loader2 className="h-4 w-4 animate-spin text-violet-500" />
<span className="text-xs text-slate-400">Calculating…</span> <span className="text-xs text-slate-400">Calculating…</span>
</div> </div>
) : estimatedCount !== null ? ( ) : estimatedCount !== null ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="h-4 w-4 text-emerald-400" /> <Users className="h-4 w-4 text-violet-400" />
<span className="text-sm text-white"> <span className="text-sm text-white">
{estimatedCount.toLocaleString()} {estimatedCount.toLocaleString()}
</span> </span>
@ -461,7 +461,7 @@ export function Step2SelectAudience({
<Button <Button
onClick={onNext} onClick={onNext}
disabled={!isValid} disabled={!isValid}
className="bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50" className="bg-violet-600 text-white hover:bg-violet-700 disabled:opacity-50"
> >
Next Next
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />

View File

@ -211,7 +211,7 @@ export function Step3Personalize({
className="rounded-xl border border-slate-800 bg-slate-900/50 p-4" className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"
> >
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<span className="inline-flex items-center rounded-md bg-emerald-500/10 px-2 py-0.5 text-xs font-mono font-medium text-emerald-400"> <span className="inline-flex items-center rounded-md bg-violet-500/10 px-2 py-0.5 text-xs font-mono font-medium text-violet-400">
{placeholder} {placeholder}
</span> </span>
</div> </div>
@ -313,16 +313,16 @@ export function Step3Personalize({
sees approximately what the recipient will see. */} sees approximately what the recipient will see. */}
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<Eye className="h-4 w-4 text-emerald-400" /> <Eye className="h-4 w-4 text-violet-400" />
<p className="text-sm font-medium text-white">Live Preview</p> <p className="text-sm font-medium text-white">Live Preview</p>
<span className="text-xs text-slate-500">({previewLabel})</span> <span className="text-xs text-slate-500">({previewLabel})</span>
{loadingPreview && ( {loadingPreview && (
<Loader2 className="h-3.5 w-3.5 animate-spin text-emerald-500" /> <Loader2 className="h-3.5 w-3.5 animate-spin text-violet-500" />
)} )}
</div> </div>
<div className="rounded-lg bg-[#0e1a12] p-3"> <div className="rounded-lg bg-[#0e1a12] p-3">
<div className="ml-auto max-w-[85%] rounded-lg bg-emerald-700/30 px-3 py-2 shadow-sm"> <div className="ml-auto max-w-[85%] rounded-lg bg-violet-700/30 px-3 py-2 shadow-sm">
<p className="whitespace-pre-wrap text-sm text-emerald-50"> <p className="whitespace-pre-wrap text-sm text-violet-50">
{previewText} {previewText}
</p> </p>
</div> </div>
@ -351,7 +351,7 @@ export function Step3Personalize({
<Button <Button
onClick={onNext} onClick={onNext}
disabled={unmappedKeys.length > 0} disabled={unmappedKeys.length > 0}
className="bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50" className="bg-violet-600 text-white hover:bg-violet-700 disabled:opacity-50"
> >
Next Next
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />

View File

@ -127,10 +127,10 @@ export function Step4ScheduleSend({
<p className="text-xs text-slate-400">Estimated Reach</p> <p className="text-xs text-slate-400">Estimated Reach</p>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{loadingReach ? ( {loadingReach ? (
<Loader2 className="h-3 w-3 animate-spin text-emerald-500" /> <Loader2 className="h-3 w-3 animate-spin text-violet-500" />
) : ( ) : (
<> <>
<Users className="h-3.5 w-3.5 text-emerald-400" /> <Users className="h-3.5 w-3.5 text-violet-400" />
<p className="font-medium text-white">{estimatedReach.toLocaleString()}</p> <p className="font-medium text-white">{estimatedReach.toLocaleString()}</p>
</> </>
)} )}
@ -151,11 +151,11 @@ export function Step4ScheduleSend({
onClick={() => setTiming('now')} onClick={() => setTiming('now')}
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${ className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
timing === 'now' timing === 'now'
? 'border-emerald-500 bg-emerald-500/5 ring-1 ring-emerald-500/30' ? 'border-violet-500 bg-violet-500/5 ring-1 ring-violet-500/30'
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700' : 'border-slate-800 bg-slate-900/50 hover:border-slate-700'
}`} }`}
> >
<Send className={`mt-0.5 h-4 w-4 ${timing === 'now' ? 'text-emerald-400' : 'text-slate-400'}`} /> <Send className={`mt-0.5 h-4 w-4 ${timing === 'now' ? 'text-violet-400' : 'text-slate-400'}`} />
<div> <div>
<p className="text-sm font-medium text-white">Send Immediately</p> <p className="text-sm font-medium text-white">Send Immediately</p>
<p className="mt-0.5 text-xs text-slate-400">Start sending right away</p> <p className="mt-0.5 text-xs text-slate-400">Start sending right away</p>
@ -180,17 +180,17 @@ export function Step4ScheduleSend({
{/* Processing overlay */} {/* Processing overlay */}
{isProcessing && ( {isProcessing && (
<div className="rounded-xl border border-emerald-500/20 bg-emerald-500/5 p-4"> <div className="rounded-xl border border-violet-500/20 bg-violet-500/5 p-4">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-emerald-500" /> <Loader2 className="h-4 w-4 animate-spin text-violet-500" />
<p className="text-sm font-medium text-white">Sending broadcast...</p> <p className="text-sm font-medium text-white">Sending broadcast...</p>
</div> </div>
<span className="text-xs font-medium text-emerald-400">{progress}%</span> <span className="text-xs font-medium text-violet-400">{progress}%</span>
</div> </div>
<div className="h-1.5 w-full rounded-full bg-slate-800"> <div className="h-1.5 w-full rounded-full bg-slate-800">
<div <div
className="h-1.5 rounded-full bg-emerald-500 transition-all duration-300" className="h-1.5 rounded-full bg-violet-500 transition-all duration-300"
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
/> />
</div> </div>
@ -226,7 +226,7 @@ export function Step4ScheduleSend({
render={ render={
<Button <Button
disabled={!name.trim() || isProcessing} disabled={!name.trim() || isProcessing}
className="bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50" className="bg-violet-600 text-white hover:bg-violet-700 disabled:opacity-50"
/> />
} }
> >
@ -257,7 +257,7 @@ export function Step4ScheduleSend({
setShowConfirm(false); setShowConfirm(false);
onSend(); onSend();
}} }}
className="bg-emerald-600 text-white hover:bg-emerald-700" className="bg-violet-600 text-white hover:bg-violet-700"
> >
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />
Confirm & Send Confirm & Send

View File

@ -331,7 +331,7 @@ export function ContactDetailView({
> >
{loading || !contact ? ( {loading || !contact ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="size-6 animate-spin text-emerald-500" /> <Loader2 className="size-6 animate-spin text-violet-500" />
</div> </div>
) : ( ) : (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
@ -339,7 +339,7 @@ export function ContactDetailView({
<SheetHeader className="p-4 border-b border-slate-700/50"> <SheetHeader className="p-4 border-b border-slate-700/50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="size-12 bg-slate-800 border border-slate-700"> <Avatar className="size-12 bg-slate-800 border border-slate-700">
<AvatarFallback className="bg-emerald-500/10 text-emerald-400 text-sm font-medium"> <AvatarFallback className="bg-violet-500/10 text-violet-400 text-sm font-medium">
{getInitials(contact.name)} {getInitials(contact.name)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
@ -353,12 +353,12 @@ export function ContactDetailView({
<div className="flex flex-wrap items-center gap-3 mt-1.5 text-xs text-slate-400"> <div className="flex flex-wrap items-center gap-3 mt-1.5 text-xs text-slate-400">
<button <button
onClick={copyPhone} onClick={copyPhone}
className="flex items-center gap-1 hover:text-emerald-400 transition-colors cursor-pointer" className="flex items-center gap-1 hover:text-violet-400 transition-colors cursor-pointer"
> >
<Phone className="size-3" /> <Phone className="size-3" />
{contact.phone} {contact.phone}
{copiedPhone ? ( {copiedPhone ? (
<Check className="size-3 text-emerald-400" /> <Check className="size-3 text-violet-400" />
) : ( ) : (
<Copy className="size-3" /> <Copy className="size-3" />
)} )}
@ -385,31 +385,31 @@ export function ContactDetailView({
<TabsList className="bg-slate-800/50 border-b border-slate-700 mx-4 mt-3"> <TabsList className="bg-slate-800/50 border-b border-slate-700 mx-4 mt-3">
<TabsTrigger <TabsTrigger
value="details" value="details"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
Details Details
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="tags" value="tags"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
Tags Tags
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="notes" value="notes"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
Notes Notes
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="custom" value="custom"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
Custom Fields Custom Fields
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="deals" value="deals"
className="data-active:bg-slate-800 data-active:text-emerald-400 text-slate-400" className="data-active:bg-slate-800 data-active:text-violet-400 text-slate-400"
> >
Deals Deals
</TabsTrigger> </TabsTrigger>
@ -455,7 +455,7 @@ export function ContactDetailView({
<Button <Button
onClick={saveDetails} onClick={saveDetails}
disabled={savingDetails} disabled={savingDetails}
className="bg-emerald-600 hover:bg-emerald-700 text-white w-full" className="bg-violet-600 hover:bg-violet-700 text-white w-full"
size="sm" size="sm"
> >
{savingDetails ? ( {savingDetails ? (
@ -489,7 +489,7 @@ export function ContactDetailView({
disabled={savingTags} disabled={savingTags}
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${ className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${
selected selected
? 'ring-2 ring-emerald-500 ring-offset-1 ring-offset-slate-900' ? 'ring-2 ring-violet-500 ring-offset-1 ring-offset-slate-900'
: 'opacity-50 hover:opacity-80' : 'opacity-50 hover:opacity-80'
}`} }`}
style={{ style={{
@ -519,7 +519,7 @@ export function ContactDetailView({
<Button <Button
onClick={addNote} onClick={addNote}
disabled={!newNote.trim() || savingNote} disabled={!newNote.trim() || savingNote}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
size="sm" size="sm"
> >
{savingNote ? ( {savingNote ? (
@ -605,7 +605,7 @@ export function ContactDetailView({
<Button <Button
onClick={saveCustomFields} onClick={saveCustomFields}
disabled={savingCustom} disabled={savingCustom}
className="bg-emerald-600 hover:bg-emerald-700 text-white w-full" className="bg-violet-600 hover:bg-violet-700 text-white w-full"
size="sm" size="sm"
> >
{savingCustom ? ( {savingCustom ? (
@ -623,7 +623,7 @@ export function ContactDetailView({
<TabsContent value="deals" className="flex-1 overflow-y-auto px-4 py-3"> <TabsContent value="deals" className="flex-1 overflow-y-auto px-4 py-3">
{loadingDeals ? ( {loadingDeals ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="size-5 animate-spin text-emerald-500" /> <Loader2 className="size-5 animate-spin text-violet-500" />
</div> </div>
) : deals.length === 0 ? ( ) : deals.length === 0 ? (
<p className="text-xs text-slate-500">No deals yet</p> <p className="text-xs text-slate-500">No deals yet</p>
@ -663,7 +663,7 @@ export function ContactDetailView({
<span <span
className={ className={
deal.status === 'won' deal.status === 'won'
? 'text-emerald-400' ? 'text-violet-400'
: 'text-red-400' : 'text-red-400'
} }
> >

View File

@ -245,7 +245,7 @@ export function ContactForm({
onClick={() => toggleTag(tag.id)} onClick={() => toggleTag(tag.id)}
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors cursor-pointer ${ className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors cursor-pointer ${
selected selected
? 'ring-2 ring-emerald-500 ring-offset-1 ring-offset-slate-900' ? 'ring-2 ring-violet-500 ring-offset-1 ring-offset-slate-900'
: 'opacity-60 hover:opacity-100' : 'opacity-60 hover:opacity-100'
}`} }`}
style={{ style={{
@ -274,7 +274,7 @@ export function ContactForm({
<Button <Button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
{saving && <Loader2 className="size-4 animate-spin" />} {saving && <Loader2 className="size-4 animate-spin" />}
{isEdit ? 'Update' : 'Create'} {isEdit ? 'Update' : 'Create'}

View File

@ -196,11 +196,11 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
{/* Upload area */} {/* Upload area */}
<div <div
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-slate-700 p-6 cursor-pointer hover:border-emerald-500/50 transition-colors" className="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-slate-700 p-6 cursor-pointer hover:border-violet-500/50 transition-colors"
> >
{file ? ( {file ? (
<> <>
<FileText className="size-8 text-emerald-400" /> <FileText className="size-8 text-violet-400" />
<p className="text-sm text-slate-300">{file.name}</p> <p className="text-sm text-slate-300">{file.name}</p>
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500">
{parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''} detected {parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''} detected
@ -269,7 +269,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
<p className="text-sm font-medium text-white">Import Complete</p> <p className="text-sm font-medium text-white">Import Complete</p>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{result.imported > 0 && ( {result.imported > 0 && (
<div className="flex items-center gap-1.5 text-emerald-400 text-sm"> <div className="flex items-center gap-1.5 text-violet-400 text-sm">
<CheckCircle className="size-4" /> <CheckCircle className="size-4" />
{result.imported} imported {result.imported} imported
</div> </div>
@ -299,7 +299,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
type="button" type="button"
disabled={parsedRows.length === 0 || importing} disabled={parsedRows.length === 0 || importing}
onClick={handleImport} onClick={handleImport}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
{importing && <Loader2 className="size-4 animate-spin" />} {importing && <Loader2 className="size-4 animate-spin" />}
Import {parsedRows.length > 0 ? `${parsedRows.length} Contacts` : ''} Import {parsedRows.length > 0 ? `${parsedRows.length} Contacts` : ''}

View File

@ -32,7 +32,7 @@ interface KindTheme {
const KIND_THEME: Record<ActivityKind, KindTheme> = { const KIND_THEME: Record<ActivityKind, KindTheme> = {
message: { icon: MessageSquare, badge: 'bg-blue-500/10 text-blue-400' }, message: { icon: MessageSquare, badge: 'bg-blue-500/10 text-blue-400' },
contact: { icon: UserPlus, badge: 'bg-emerald-500/10 text-emerald-400' }, contact: { icon: UserPlus, badge: 'bg-violet-500/10 text-violet-400' },
deal: { icon: Briefcase, badge: 'bg-violet-500/10 text-violet-400' }, deal: { icon: Briefcase, badge: 'bg-violet-500/10 text-violet-400' },
broadcast: { icon: Radio, badge: 'bg-amber-500/10 text-amber-400' }, broadcast: { icon: Radio, badge: 'bg-amber-500/10 text-amber-400' },
automation: { icon: Zap, badge: 'bg-rose-500/10 text-rose-400' }, automation: { icon: Zap, badge: 'bg-rose-500/10 text-rose-400' },
@ -59,7 +59,7 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
<h2 className="text-sm font-semibold text-white">Recent Activity</h2> <h2 className="text-sm font-semibold text-white">Recent Activity</h2>
<Link <Link
href="/inbox" href="/inbox"
className="text-xs font-medium text-emerald-400 hover:text-emerald-300" className="text-xs font-medium text-violet-400 hover:text-violet-300"
> >
View all → View all →
</Link> </Link>

View File

@ -87,7 +87,7 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
<footer className="flex items-center gap-4 border-t border-slate-800 px-5 py-3 text-xs text-slate-400"> <footer className="flex items-center gap-4 border-t border-slate-800 px-5 py-3 text-xs text-slate-400">
<LegendDot color="#3b82f6" label="Incoming" /> <LegendDot color="#3b82f6" label="Incoming" />
<LegendDot color="#10b981" label="Outgoing" /> <LegendDot color="#7c3aed" label="Outgoing" />
</footer> </footer>
</section> </section>
) )
@ -238,11 +238,11 @@ function LineSvg({
) : null, ) : null,
)} )}
{/* Outgoing polyline (emerald) */} {/* Outgoing polyline (violet) */}
<path <path
d={outgoingPath} d={outgoingPath}
fill="none" fill="none"
stroke="#10b981" stroke="#7c3aed"
strokeWidth={2} strokeWidth={2}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@ -269,7 +269,7 @@ function LineSvg({
strokeDasharray="3 3" strokeDasharray="3 3"
/> />
<circle cx={hoverX} cy={yFor(data[hover.idx].incoming)} r={3.5} fill="#3b82f6" /> <circle cx={hoverX} cy={yFor(data[hover.idx].incoming)} r={3.5} fill="#3b82f6" />
<circle cx={hoverX} cy={yFor(data[hover.idx].outgoing)} r={3.5} fill="#10b981" /> <circle cx={hoverX} cy={yFor(data[hover.idx].outgoing)} r={3.5} fill="#7c3aed" />
</g> </g>
)} )}
</svg> </svg>
@ -289,8 +289,8 @@ function LineSvg({
<span className="inline-block h-1.5 w-1.5 rounded-full bg-blue-500" /> <span className="inline-block h-1.5 w-1.5 rounded-full bg-blue-500" />
{hovered.incoming} incoming {hovered.incoming} incoming
</span> </span>
<span className="flex items-center gap-1.5 text-emerald-300"> <span className="flex items-center gap-1.5 text-violet-300">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-500" /> <span className="inline-block h-1.5 w-1.5 rounded-full bg-violet-500" />
{hovered.outgoing} outgoing {hovered.outgoing} outgoing
</span> </span>
</div> </div>

View File

@ -43,7 +43,7 @@ export function MetricCard({ title, value, icon: Icon, delta, subtitle }: Metric
function DeltaRow({ sign, label }: { sign: number; label: string }) { function DeltaRow({ sign, label }: { sign: number; label: string }) {
const tone = const tone =
sign > 0 sign > 0
? 'text-emerald-400' ? 'text-violet-400'
: sign < 0 : sign < 0
? 'text-red-400' ? 'text-red-400'
: 'text-slate-500' : 'text-slate-500'

View File

@ -16,7 +16,7 @@ interface Action {
} }
const ACTIONS: Action[] = [ const ACTIONS: Action[] = [
{ label: 'New Contact', href: '/contacts', icon: UserPlus, tint: 'text-emerald-400' }, { label: 'New Contact', href: '/contacts', icon: UserPlus, tint: 'text-violet-400' },
{ label: 'New Deal', href: '/pipelines', icon: Briefcase, tint: 'text-blue-400' }, { label: 'New Deal', href: '/pipelines', icon: Briefcase, tint: 'text-blue-400' },
{ label: 'New Broadcast', href: '/broadcasts/new', icon: Radio, tint: 'text-amber-400' }, { label: 'New Broadcast', href: '/broadcasts/new', icon: Radio, tint: 'text-amber-400' },
{ label: 'New Automation', href: '/automations/new', icon: Zap, tint: 'text-violet-400' }, { label: 'New Automation', href: '/automations/new', icon: Zap, tint: 'text-violet-400' },

View File

@ -158,7 +158,7 @@ function Bars({
width={barW} width={barW}
height={muted ? 2 : Math.max(1, h)} height={muted ? 2 : Math.max(1, h)}
rx={4} rx={4}
fill={muted ? 'rgb(51 65 85)' : '#10b981'} fill={muted ? 'rgb(51 65 85)' : '#7c3aed'}
opacity={muted ? 0.6 : 1} opacity={muted ? 0.6 : 1}
> >
<title> <title>

View File

@ -32,11 +32,17 @@ export function DocsShell({ pages, children }: DocsShellProps) {
> >
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />} {mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button> </button>
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home"> <Link
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500"> href="/"
className="flex items-center gap-2"
aria-label="CRM Template for WhatsApp home"
>
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500">
<MessageSquare className="h-4 w-4 text-white" /> <MessageSquare className="h-4 w-4 text-white" />
</span> </span>
<span className="text-lg font-semibold text-white">WaCRM</span> <span className="text-sm font-semibold text-white">
CRM Template for WhatsApp
</span>
<span className="hidden rounded-md border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-slate-400 sm:inline-block"> <span className="hidden rounded-md border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-slate-400 sm:inline-block">
Docs Docs
</span> </span>
@ -61,7 +67,7 @@ export function DocsShell({ pages, children }: DocsShellProps) {
</a> </a>
<Link <Link
href="/signup" href="/signup"
className="inline-flex items-center justify-center rounded-lg bg-emerald-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-emerald-400" className="inline-flex items-center justify-center rounded-lg bg-violet-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-violet-400"
> >
Get started Get started
</Link> </Link>

View File

@ -51,7 +51,7 @@ export function DocsSidebar({ pages, onNavigate }: DocsSidebarProps) {
className={cn( className={cn(
'block rounded-md px-3 py-1.5 transition-colors', 'block rounded-md px-3 py-1.5 transition-colors',
active active
? 'bg-emerald-500/10 text-emerald-300' ? 'bg-violet-500/10 text-violet-300'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-white', : 'text-slate-400 hover:bg-slate-800/60 hover:text-white',
)} )}
> >

View File

@ -156,7 +156,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
<Phone className="h-4 w-4 text-slate-500" /> <Phone className="h-4 w-4 text-slate-500" />
<span className="flex-1 text-left">{contact.phone}</span> <span className="flex-1 text-left">{contact.phone}</span>
{copied ? ( {copied ? (
<Check className="h-3 w-3 text-emerald-400" /> <Check className="h-3 w-3 text-violet-400" />
) : ( ) : (
<Copy className="h-3 w-3 text-slate-600" /> <Copy className="h-3 w-3 text-slate-600" />
)} )}
@ -259,11 +259,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
onChange={(e) => setNewNote(e.target.value)} onChange={(e) => setNewNote(e.target.value)}
placeholder="Add a note..." placeholder="Add a note..."
rows={2} rows={2}
className="flex-1 resize-none rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-emerald-500/50" className="flex-1 resize-none rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-violet-500/50"
/> />
<Button <Button
size="sm" size="sm"
className="h-auto bg-emerald-600 px-2 hover:bg-emerald-500" className="h-auto bg-violet-600 px-2 hover:bg-violet-500"
onClick={handleAddNote} onClick={handleAddNote}
disabled={!newNote.trim() || addingNote} disabled={!newNote.trim() || addingNote}
> >

View File

@ -24,7 +24,7 @@ interface ConversationListProps {
} }
const STATUS_COLORS: Record<ConversationStatus, string> = { const STATUS_COLORS: Record<ConversationStatus, string> = {
open: "bg-emerald-500", open: "bg-violet-500",
pending: "bg-amber-500", pending: "bg-amber-500",
closed: "bg-slate-500", closed: "bg-slate-500",
}; };
@ -145,7 +145,7 @@ export function ConversationList({
value={search} value={search}
onChange={handleSearchChange} onChange={handleSearchChange}
placeholder="Search conversations..." placeholder="Search conversations..."
className="border-slate-700 bg-slate-800 pl-9 text-sm text-white placeholder-slate-500 focus:border-emerald-500/50" className="border-slate-700 bg-slate-800 pl-9 text-sm text-white placeholder-slate-500 focus:border-violet-500/50"
/> />
</div> </div>
@ -165,7 +165,7 @@ export function ConversationList({
className={cn( className={cn(
"text-sm", "text-sm",
filter === opt.value filter === opt.value
? "text-emerald-400" ? "text-violet-400"
: "text-slate-300" : "text-slate-300"
)} )}
> >
@ -180,7 +180,7 @@ export function ConversationList({
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-emerald-500 border-t-transparent" /> <div className="h-5 w-5 animate-spin rounded-full border-2 border-violet-500 border-t-transparent" />
</div> </div>
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<div className="px-4 py-12 text-center"> <div className="px-4 py-12 text-center">
@ -233,7 +233,7 @@ function ConversationItem({
onClick={handleClick} onClick={handleClick}
className={cn( className={cn(
"flex w-full items-start gap-3 px-3 py-3 text-left transition-colors hover:bg-slate-800/50", "flex w-full items-start gap-3 px-3 py-3 text-left transition-colors hover:bg-slate-800/50",
isActive && "border-l-2 border-emerald-500 bg-slate-800/70" isActive && "border-l-2 border-violet-500 bg-slate-800/70"
)} )}
> >
{/* Avatar */} {/* Avatar */}
@ -263,7 +263,7 @@ function ConversationItem({
</p> </p>
<div className="flex shrink-0 items-center gap-1.5"> <div className="flex shrink-0 items-center gap-1.5">
{conversation.unread_count > 0 && ( {conversation.unread_count > 0 && (
<span className="flex h-4 min-w-4 items-center justify-center rounded-full bg-emerald-500 px-1 text-[10px] font-bold text-white"> <span className="flex h-4 min-w-4 items-center justify-center rounded-full bg-violet-500 px-1 text-[10px] font-bold text-white">
{conversation.unread_count} {conversation.unread_count}
</span> </span>
)} )}

View File

@ -93,7 +93,7 @@ function MediaImage({ url, alt }: { url: string; alt: string }) {
if (loading) { if (loading) {
return ( return (
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-slate-700"> <div className="flex h-40 w-60 items-center justify-center rounded-lg bg-slate-700">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-emerald-500 border-t-transparent" /> <div className="h-5 w-5 animate-spin rounded-full border-2 border-violet-500 border-t-transparent" />
</div> </div>
); );
} }
@ -185,7 +185,7 @@ function MessageContent({ message }: { message: Message }) {
case "template": case "template":
return ( return (
<div> <div>
<span className="mb-1 inline-flex items-center gap-1 rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400"> <span className="mb-1 inline-flex items-center gap-1 rounded bg-violet-500/20 px-1.5 py-0.5 text-[10px] font-medium text-violet-400">
<LayoutTemplate className="h-3 w-3" /> <LayoutTemplate className="h-3 w-3" />
Template Template
</span> </span>
@ -226,7 +226,7 @@ export function MessageBubble({ message }: MessageBubbleProps) {
className={cn( className={cn(
"relative max-w-[75%] rounded-2xl px-3 py-2", "relative max-w-[75%] rounded-2xl px-3 py-2",
isAgent isAgent
? "rounded-br-md bg-emerald-600 text-white" ? "rounded-br-md bg-violet-600 text-white"
: "rounded-bl-md bg-slate-800 text-slate-100" : "rounded-bl-md bg-slate-800 text-slate-100"
)} )}
> >

View File

@ -107,14 +107,14 @@ export function MessageComposer({
disabled={sessionExpired} disabled={sessionExpired}
rows={1} rows={1}
className={cn( className={cn(
"flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-emerald-500/50", "flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-violet-500/50",
sessionExpired && "cursor-not-allowed opacity-50" sessionExpired && "cursor-not-allowed opacity-50"
)} )}
/> />
<Button <Button
size="sm" size="sm"
className="h-9 w-9 shrink-0 bg-emerald-600 p-0 hover:bg-emerald-500 disabled:opacity-40" className="h-9 w-9 shrink-0 bg-violet-600 p-0 hover:bg-violet-500 disabled:opacity-40"
disabled={!text.trim() || sessionExpired || sending} disabled={!text.trim() || sessionExpired || sending}
onClick={handleSend} onClick={handleSend}
> >

View File

@ -67,7 +67,7 @@ function groupMessagesByDate(messages: Message[]) {
} }
const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string }[] = [ const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string }[] = [
{ label: "Open", value: "open", color: "text-emerald-400" }, { label: "Open", value: "open", color: "text-violet-400" },
{ label: "Pending", value: "pending", color: "text-amber-400" }, { label: "Pending", value: "pending", color: "text-amber-400" },
{ label: "Closed", value: "closed", color: "text-slate-400" }, { label: "Closed", value: "closed", color: "text-slate-400" },
]; ];
@ -319,7 +319,7 @@ export function MessageThread({
variant="outline" variant="outline"
className={cn( className={cn(
"ml-1 hidden gap-1 border-slate-700 text-[10px] sm:inline-flex sm:ml-2", "ml-1 hidden gap-1 border-slate-700 text-[10px] sm:inline-flex sm:ml-2",
sessionInfo.expired ? "text-red-400" : "text-emerald-400" sessionInfo.expired ? "text-red-400" : "text-violet-400"
)} )}
> >
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
@ -369,7 +369,7 @@ export function MessageThread({
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4"> <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-emerald-500 border-t-transparent" /> <div className="h-5 w-5 animate-spin rounded-full border-2 border-violet-500 border-t-transparent" />
</div> </div>
) : messages.length === 0 ? ( ) : messages.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12"> <div className="flex flex-col items-center justify-center py-12">

View File

@ -10,10 +10,10 @@ export function CtaBanner() {
return ( return (
<Section className="py-16 sm:py-20"> <Section className="py-16 sm:py-20">
<div <div
className="relative overflow-hidden rounded-2xl border border-emerald-500/20 bg-slate-900/70 px-6 py-16 text-center sm:px-12 sm:py-20" className="relative overflow-hidden rounded-2xl border border-violet-500/20 bg-slate-900/70 px-6 py-16 text-center sm:px-12 sm:py-20"
style={{ style={{
background: background:
'radial-gradient(800px circle at 50% -40%, rgb(16 185 129 / 0.18), transparent 60%), linear-gradient(to bottom, rgb(15 23 42 / 0.9), rgb(15 23 42 / 0.7))', 'radial-gradient(800px circle at 50% -40%, rgb(139 92 246 / 0.18), transparent 60%), linear-gradient(to bottom, rgb(15 23 42 / 0.9), rgb(15 23 42 / 0.7))',
}} }}
> >
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl"> <h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
@ -25,7 +25,7 @@ export function CtaBanner() {
<div className="mt-8 flex flex-wrap items-center justify-center gap-3"> <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<Link <Link
href="/signup" href="/signup"
className="inline-flex items-center gap-2 rounded-lg bg-emerald-500 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-emerald-400" className="inline-flex items-center gap-2 rounded-lg bg-violet-500 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-violet-400"
> >
Get started free Get started free
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />

View File

@ -31,7 +31,7 @@ export function FAQ() {
<ChevronDown <ChevronDown
className={cn( className={cn(
'h-4 w-4 flex-shrink-0 text-slate-500 transition-transform', 'h-4 w-4 flex-shrink-0 text-slate-500 transition-transform',
isOpen && 'rotate-180 text-emerald-400', isOpen && 'rotate-180 text-violet-400',
)} )}
/> />
</button> </button>

View File

@ -37,7 +37,7 @@ export function FeatureSpotlight({
)} )}
> >
<div> <div>
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-400"> <p className="text-xs font-semibold uppercase tracking-wider text-violet-400">
{eyebrow} {eyebrow}
</p> </p>
<h3 className="mt-3 text-2xl font-bold tracking-tight text-white sm:text-3xl"> <h3 className="mt-3 text-2xl font-bold tracking-tight text-white sm:text-3xl">
@ -50,7 +50,7 @@ export function FeatureSpotlight({
<ul className="mt-5 space-y-2.5"> <ul className="mt-5 space-y-2.5">
{bullets.map((b) => ( {bullets.map((b) => (
<li key={b} className="flex items-start gap-2.5 text-sm text-slate-300"> <li key={b} className="flex items-start gap-2.5 text-sm text-slate-300">
<Check className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400" /> <Check className="mt-0.5 h-4 w-4 flex-shrink-0 text-violet-400" />
<span>{b}</span> <span>{b}</span>
</li> </li>
))} ))}

View File

@ -32,7 +32,7 @@ const FEATURES: Feature[] = [
description: description:
'Tags, custom fields, notes, and automatic deduplication. Import existing contacts from CSV.', 'Tags, custom fields, notes, and automatic deduplication. Import existing contacts from CSV.',
icon: Users, icon: Users,
tint: 'text-emerald-400 bg-emerald-500/10', tint: 'text-violet-400 bg-violet-500/10',
}, },
{ {
title: 'Sales pipelines', title: 'Sales pipelines',
@ -70,7 +70,7 @@ export function FeaturesGrid() {
<SectionHeader <SectionHeader
eyebrow="Everything you need" eyebrow="Everything you need"
title="One toolkit for your WhatsApp business" title="One toolkit for your WhatsApp business"
description="Stop stitching together an inbox, a spreadsheet, and a broadcast tool. WaCRM brings them together — and talks to WhatsApp natively." description="Stop stitching together an inbox, a spreadsheet, and a broadcast tool. This CRM template for WhatsApp brings them together — and talks to WhatsApp natively."
/> />
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">

View File

@ -7,11 +7,17 @@ export function Footer() {
<footer className="border-t border-slate-800 bg-slate-950"> <footer className="border-t border-slate-800 bg-slate-950">
<div className="mx-auto grid w-full max-w-7xl grid-cols-2 gap-8 px-6 py-12 sm:grid-cols-5"> <div className="mx-auto grid w-full max-w-7xl grid-cols-2 gap-8 px-6 py-12 sm:grid-cols-5">
<div className="col-span-2 sm:col-span-2"> <div className="col-span-2 sm:col-span-2">
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home"> <Link
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500"> href="/"
className="flex items-center gap-2"
aria-label="CRM Template for WhatsApp home"
>
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500">
<MessageSquare className="h-4 w-4 text-white" /> <MessageSquare className="h-4 w-4 text-white" />
</span> </span>
<span className="text-lg font-semibold text-white">WaCRM</span> <span className="text-sm font-semibold text-white">
CRM Template for WhatsApp
</span>
</Link> </Link>
<p className="mt-3 max-w-sm text-sm text-slate-500"> <p className="mt-3 max-w-sm text-sm text-slate-500">
One toolkit for your WhatsApp business — shared inbox, pipelines, One toolkit for your WhatsApp business — shared inbox, pipelines,
@ -58,7 +64,7 @@ export function Footer() {
<div className="border-t border-slate-900"> <div className="border-t border-slate-900">
<div className="mx-auto flex w-full max-w-7xl flex-col items-start justify-between gap-3 px-6 py-5 text-xs text-slate-500 sm:flex-row sm:items-center"> <div className="mx-auto flex w-full max-w-7xl flex-col items-start justify-between gap-3 px-6 py-5 text-xs text-slate-500 sm:flex-row sm:items-center">
<span>© {year} WaCRM. All rights reserved.</span> <span>© {year} CRM Template for WhatsApp. All rights reserved.</span>
<span>Built on the official WhatsApp Business API.</span> <span>Built on the official WhatsApp Business API.</span>
</div> </div>
</div> </div>

View File

@ -17,20 +17,20 @@ export function Hero() {
className="pointer-events-none absolute inset-0 -z-10 opacity-60" className="pointer-events-none absolute inset-0 -z-10 opacity-60"
style={{ style={{
background: background:
'radial-gradient(800px circle at 70% -10%, rgb(16 185 129 / 0.18), transparent 60%), radial-gradient(600px circle at 10% 30%, rgb(59 130 246 / 0.08), transparent 60%)', 'radial-gradient(800px circle at 70% -10%, rgb(139 92 246 / 0.18), transparent 60%), radial-gradient(600px circle at 10% 30%, rgb(59 130 246 / 0.08), transparent 60%)',
}} }}
/> />
<div className="mx-auto grid w-full max-w-7xl grid-cols-1 items-center gap-12 px-6 py-20 sm:py-28 lg:grid-cols-2 lg:gap-16"> <div className="mx-auto grid w-full max-w-7xl grid-cols-1 items-center gap-12 px-6 py-20 sm:py-28 lg:grid-cols-2 lg:gap-16">
<div> <div>
<div className="inline-flex items-center gap-2 rounded-full border border-slate-800 bg-slate-900/60 px-3 py-1 text-xs text-slate-300"> <div className="inline-flex items-center gap-2 rounded-full border border-slate-800 bg-slate-900/60 px-3 py-1 text-xs text-slate-300">
<ShieldCheck className="h-3.5 w-3.5 text-emerald-400" /> <ShieldCheck className="h-3.5 w-3.5 text-violet-400" />
Built on the official WhatsApp® Business API Built on the official WhatsApp® Business API
</div> </div>
<h1 className="mt-5 text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl"> <h1 className="mt-5 text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
Run your WhatsApp business from{' '} Run your WhatsApp business from{' '}
<span className="text-emerald-400">one inbox.</span> <span className="text-violet-400">one inbox.</span>
</h1> </h1>
<p className="mt-5 max-w-xl text-base text-slate-400 sm:text-lg"> <p className="mt-5 max-w-xl text-base text-slate-400 sm:text-lg">
@ -42,7 +42,7 @@ export function Hero() {
<div className="mt-8 flex flex-wrap items-center gap-3"> <div className="mt-8 flex flex-wrap items-center gap-3">
<Link <Link
href="/signup" href="/signup"
className="inline-flex items-center gap-2 rounded-lg bg-emerald-500 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-emerald-400" className="inline-flex items-center gap-2 rounded-lg bg-violet-500 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-violet-400"
> >
Get started Get started
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
@ -57,15 +57,15 @@ export function Hero() {
<div className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-slate-500"> <div className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-slate-500">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> <span className="h-1.5 w-1.5 rounded-full bg-violet-500" />
No credit card required No credit card required
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> <span className="h-1.5 w-1.5 rounded-full bg-violet-500" />
Live in 30 minutes Live in 30 minutes
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> <span className="h-1.5 w-1.5 rounded-full bg-violet-500" />
Your data, your Supabase Your data, your Supabase
</span> </span>
</div> </div>

View File

@ -43,7 +43,7 @@ export function HowItWorks() {
className="relative rounded-xl border border-slate-800 bg-slate-900/40 p-6" className="relative rounded-xl border border-slate-800 bg-slate-900/40 p-6"
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400">
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
</div> </div>
<span <span

View File

@ -27,7 +27,9 @@ export function AnalyticsMock() {
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Dashboard — WaCRM</span> <span className="ml-3 text-[10px] text-slate-500">
Dashboard — CRM Template for WhatsApp
</span>
</div> </div>
<div className="space-y-3 p-4"> <div className="space-y-3 p-4">
@ -47,7 +49,7 @@ export function AnalyticsMock() {
</div> </div>
<div <div
className={`text-[10px] tabular-nums ${ className={`text-[10px] tabular-nums ${
s.positive ? 'text-emerald-400' : 'text-red-400' s.positive ? 'text-violet-400' : 'text-red-400'
}`} }`}
> >
{s.delta} {s.delta}
@ -67,7 +69,7 @@ export function AnalyticsMock() {
Incoming Incoming
</span> </span>
<span className="inline-flex items-center gap-1 text-[9px] text-slate-500"> <span className="inline-flex items-center gap-1 text-[9px] text-slate-500">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> <span className="h-1.5 w-1.5 rounded-full bg-violet-500" />
Outgoing Outgoing
</span> </span>
</div> </div>
@ -89,7 +91,7 @@ export function AnalyticsMock() {
strokeDasharray="3 3" strokeDasharray="3 3"
/> />
))} ))}
<path d={path(outgoing)} fill="none" stroke="#10b981" strokeWidth={1.5} /> <path d={path(outgoing)} fill="none" stroke="#7c3aed" strokeWidth={1.5} />
<path d={path(incoming)} fill="none" stroke="#3b82f6" strokeWidth={1.5} /> <path d={path(incoming)} fill="none" stroke="#3b82f6" strokeWidth={1.5} />
</svg> </svg>
</div> </div>

View File

@ -19,8 +19,8 @@ export function AutomationMock() {
kind: 'action', kind: 'action',
label: 'Send "Hi! 👋 Thanks for reaching out."', label: 'Send "Hi! 👋 Thanks for reaching out."',
icon: MessageSquare, icon: MessageSquare,
accent: 'border-l-emerald-500', accent: 'border-l-violet-500',
badgeClass: 'bg-emerald-500/10 text-emerald-400', badgeClass: 'bg-violet-500/10 text-violet-400',
kindLabel: 'ACTION', kindLabel: 'ACTION',
}, },
{ {
@ -47,7 +47,9 @@ export function AutomationMock() {
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Automations — WaCRM</span> <span className="ml-3 text-[10px] text-slate-500">
Automations — CRM Template for WhatsApp
</span>
</div> </div>
{/* Dotted canvas — matches the real flow builder's background. */} {/* Dotted canvas — matches the real flow builder's background. */}

View File

@ -14,7 +14,9 @@ export function InboxMock() {
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Inbox — WaCRM</span> <span className="ml-3 text-[10px] text-slate-500">
Inbox — CRM Template for WhatsApp
</span>
</div> </div>
<div className="grid grid-cols-[180px_1fr] lg:grid-cols-[220px_1fr]"> <div className="grid grid-cols-[180px_1fr] lg:grid-cols-[220px_1fr]">
@ -33,16 +35,16 @@ export function InboxMock() {
<div <div
key={i} key={i}
className={`flex items-start gap-2 px-3 py-2.5 ${ className={`flex items-start gap-2 px-3 py-2.5 ${
c.active ? 'border-l-2 border-emerald-500 bg-slate-800/60' : 'border-l-2 border-transparent' c.active ? 'border-l-2 border-violet-500 bg-slate-800/60' : 'border-l-2 border-transparent'
}`} }`}
> >
<span className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-[10px] font-medium text-emerald-400"> <span className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-violet-500/10 text-[10px] font-medium text-violet-400">
{c.name[0]} {c.name[0]}
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="truncate text-xs font-medium text-white">{c.name}</span> <span className="truncate text-xs font-medium text-white">{c.name}</span>
{c.unread && <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />} {c.unread && <span className="h-1.5 w-1.5 rounded-full bg-violet-500" />}
</div> </div>
<p className="truncate text-[10px] text-slate-500">{c.preview}</p> <p className="truncate text-[10px] text-slate-500">{c.preview}</p>
</div> </div>
@ -53,14 +55,14 @@ export function InboxMock() {
{/* Thread */} {/* Thread */}
<div className="flex min-h-[280px] flex-col bg-slate-950"> <div className="flex min-h-[280px] flex-col bg-slate-950">
<div className="flex items-center gap-2 border-b border-slate-800 px-4 py-2.5"> <div className="flex items-center gap-2 border-b border-slate-800 px-4 py-2.5">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500/10 text-[10px] font-medium text-emerald-400"> <span className="flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/10 text-[10px] font-medium text-violet-400">
A A
</span> </span>
<div> <div>
<div className="text-xs font-medium text-white">Aisha</div> <div className="text-xs font-medium text-white">Aisha</div>
<div className="text-[10px] text-slate-500">+44 7700 900123</div> <div className="text-[10px] text-slate-500">+44 7700 900123</div>
</div> </div>
<div className="ml-auto inline-flex items-center gap-1 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] text-emerald-300"> <div className="ml-auto inline-flex items-center gap-1 rounded-full border border-violet-500/30 bg-violet-500/10 px-2 py-0.5 text-[10px] text-violet-300">
Open Open
</div> </div>
</div> </div>
@ -69,7 +71,7 @@ export function InboxMock() {
<div className="mr-auto max-w-[75%] rounded-2xl rounded-bl-sm bg-slate-800 px-3 py-2 text-xs text-slate-200"> <div className="mr-auto max-w-[75%] rounded-2xl rounded-bl-sm bg-slate-800 px-3 py-2 text-xs text-slate-200">
Hi! Is the kit available in large? Hi! Is the kit available in large?
</div> </div>
<div className="ml-auto max-w-[75%] rounded-2xl rounded-br-sm bg-emerald-500 px-3 py-2 text-xs text-white"> <div className="ml-auto max-w-[75%] rounded-2xl rounded-br-sm bg-violet-500 px-3 py-2 text-xs text-white">
Yes — shipping today 📦 Yes — shipping today 📦
</div> </div>
<div className="mr-auto max-w-[75%] rounded-2xl rounded-bl-sm bg-slate-800 px-3 py-2 text-xs text-slate-200"> <div className="mr-auto max-w-[75%] rounded-2xl rounded-bl-sm bg-slate-800 px-3 py-2 text-xs text-slate-200">
@ -85,7 +87,7 @@ export function InboxMock() {
type="button" type="button"
tabIndex={-1} tabIndex={-1}
aria-hidden aria-hidden
className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500 text-white" className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500 text-white"
> >
<MessageSquare className="h-3.5 w-3.5" /> <MessageSquare className="h-3.5 w-3.5" />
</button> </button>

View File

@ -23,7 +23,7 @@ export function PipelineMock() {
}, },
{ {
name: 'Won', name: 'Won',
color: 'bg-emerald-500', color: 'bg-violet-500',
deals: [{ title: 'Lagoon Spa', value: '$3,200' }], deals: [{ title: 'Lagoon Spa', value: '$3,200' }],
}, },
] ]
@ -34,7 +34,9 @@ export function PipelineMock() {
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" /> <span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Pipelines — WaCRM</span> <span className="ml-3 text-[10px] text-slate-500">
Pipelines — CRM Template for WhatsApp
</span>
</div> </div>
<div className="grid grid-cols-3 gap-3 p-4"> <div className="grid grid-cols-3 gap-3 p-4">

View File

@ -60,11 +60,17 @@ export function LandingNav() {
)} )}
> >
<div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6"> <div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6">
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home"> <Link
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500"> href="/"
className="flex items-center gap-2"
aria-label="CRM Template for WhatsApp home"
>
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500">
<MessageSquare className="h-4 w-4 text-white" /> <MessageSquare className="h-4 w-4 text-white" />
</span> </span>
<span className="text-lg font-semibold text-white">WaCRM</span> <span className="text-sm font-semibold text-white">
CRM Template for WhatsApp
</span>
</Link> </Link>
<nav className="hidden items-center gap-8 md:flex"> <nav className="hidden items-center gap-8 md:flex">
@ -145,7 +151,7 @@ function NavCtas({ auth, mobile = false }: { auth: AuthState; mobile?: boolean }
) )
const primary = cn( const primary = cn(
btnBase, btnBase,
'bg-emerald-500 text-white hover:bg-emerald-400', 'bg-violet-500 text-white hover:bg-violet-400',
mobile && 'justify-center', mobile && 'justify-center',
) )

View File

@ -12,7 +12,7 @@ export function OpenSource() {
<SectionHeader <SectionHeader
eyebrow="Open source" eyebrow="Open source"
title="Fork it, brand it, host it" title="Fork it, brand it, host it"
description="WaCRM is a template you can take and make your own. Grab the source on GitHub and self-host — we recommend Hostinger Managed Node.js Hosting for a zero-ops deploy." description="This CRM template for WhatsApp is a template you can take and make your own. Grab the source on GitHub and self-host — we recommend Hostinger Managed Node.js Hosting for a zero-ops deploy."
/> />
<div className="mx-auto grid max-w-5xl grid-cols-1 gap-5 md:grid-cols-2"> <div className="mx-auto grid max-w-5xl grid-cols-1 gap-5 md:grid-cols-2">
@ -32,7 +32,7 @@ export function OpenSource() {
Clone or fork the repository, tweak the code, ship your own build. Clone or fork the repository, tweak the code, ship your own build.
MIT-style freedom with the full CRM underneath. MIT-style freedom with the full CRM underneath.
</p> </p>
<span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-emerald-400 transition-colors group-hover:text-emerald-300"> <span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-violet-400 transition-colors group-hover:text-violet-300">
ArnasDon/wacrm ArnasDon/wacrm
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" /> <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</span> </span>
@ -42,23 +42,23 @@ export function OpenSource() {
href={HOSTINGER_URL} href={HOSTINGER_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group flex flex-col rounded-xl border border-emerald-500/20 bg-slate-900/40 p-6 transition-colors hover:border-emerald-500/40 hover:bg-slate-900/70" className="group flex flex-col rounded-xl border border-violet-500/20 bg-slate-900/40 p-6 transition-colors hover:border-violet-500/40 hover:bg-slate-900/70"
> >
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400">
<Server className="h-5 w-5" /> <Server className="h-5 w-5" />
</div> </div>
<h3 className="mt-4 flex flex-wrap items-center gap-2 text-base font-semibold text-white"> <h3 className="mt-4 flex flex-wrap items-center gap-2 text-base font-semibold text-white">
Deploy on Hostinger Deploy on Hostinger
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-emerald-400"> <span className="rounded-full bg-violet-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-violet-400">
Recommended Recommended
</span> </span>
</h3> </h3>
<p className="mt-1.5 text-sm leading-relaxed text-slate-400"> <p className="mt-1.5 text-sm leading-relaxed text-slate-400">
Best fit for this template — connect your forked repo to Best fit for this template — connect your forked repo to
Managed Node.js Hosting and WaCRM is live in a few minutes. No Managed Node.js Hosting and your CRM is live in a few minutes. No
servers to patch. servers to patch.
</p> </p>
<span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-emerald-400 transition-colors group-hover:text-emerald-300"> <span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-violet-400 transition-colors group-hover:text-violet-300">
Managed Node.js Hosting Managed Node.js Hosting
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" /> <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</span> </span>
@ -70,7 +70,7 @@ export function OpenSource() {
href="/docs" href="/docs"
className="group inline-flex items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/40 px-5 py-3 text-sm font-medium text-slate-200 transition-colors hover:border-slate-700 hover:bg-slate-900/70 hover:text-white" className="group inline-flex items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/40 px-5 py-3 text-sm font-medium text-slate-200 transition-colors hover:border-slate-700 hover:bg-slate-900/70 hover:text-white"
> >
<BookOpen className="h-4 w-4 text-emerald-400" /> <BookOpen className="h-4 w-4 text-violet-400" />
Read the full documentation Read the full documentation
<ArrowRight className="h-4 w-4 text-slate-400 transition-transform group-hover:translate-x-0.5 group-hover:text-slate-200" /> <ArrowRight className="h-4 w-4 text-slate-400 transition-transform group-hover:translate-x-0.5 group-hover:text-slate-200" />
</Link> </Link>

View File

@ -54,7 +54,7 @@ export function SectionHeader({
return ( return (
<div className={cn(base, max, 'mb-12')}> <div className={cn(base, max, 'mb-12')}>
{eyebrow && ( {eyebrow && (
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-emerald-400"> <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-violet-400">
{eyebrow} {eyebrow}
</p> </p>
)} )}

View File

@ -80,7 +80,7 @@ export function Header({ onOpenSidebar }: HeaderProps) {
alt={profile.full_name ?? "Avatar"} alt={profile.full_name ?? "Avatar"}
/> />
) : null} ) : null}
<AvatarFallback className="bg-emerald-500/10 text-sm font-medium text-emerald-500"> <AvatarFallback className="bg-violet-500/10 text-sm font-medium text-violet-500">
{initial} {initial}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>

View File

@ -111,10 +111,12 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
close button is hidden since the sidebar is always-visible. */} close button is hidden since the sidebar is always-visible. */}
<div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-slate-800 px-4"> <div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-slate-800 px-4">
<Link href="/dashboard" className="flex items-center gap-2"> <Link href="/dashboard" className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500">
<MessageSquare className="h-4 w-4 text-white" /> <MessageSquare className="h-4 w-4 text-white" />
</div> </div>
<span className="text-lg font-semibold text-white">WaCRM</span> <span className="text-sm font-semibold text-white">
CRM Template for WhatsApp
</span>
</Link> </Link>
<button <button
type="button" type="button"
@ -145,7 +147,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
// Taller on mobile so fingers can hit the row reliably (≥44px). // Taller on mobile so fingers can hit the row reliably (≥44px).
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2", "flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
isActive isActive
? "bg-emerald-500/10 text-emerald-500" ? "bg-violet-500/10 text-violet-500"
: "text-slate-400 hover:bg-slate-800 hover:text-white", : "text-slate-400 hover:bg-slate-800 hover:text-white",
)} )}
> >
@ -156,8 +158,8 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
aria-label={`${totalUnread} unread conversation${totalUnread === 1 ? "" : "s"}`} aria-label={`${totalUnread} unread conversation${totalUnread === 1 ? "" : "s"}`}
className="relative flex h-2 w-2" className="relative flex h-2 w-2"
> >
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" /> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-violet-500" />
</span> </span>
)} )}
</Link> </Link>
@ -178,7 +180,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2", "flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
isActive isActive
? "bg-emerald-500/10 text-emerald-500" ? "bg-violet-500/10 text-violet-500"
: "text-slate-400 hover:bg-slate-800 hover:text-white", : "text-slate-400 hover:bg-slate-800 hover:text-white",
)} )}
> >
@ -202,7 +204,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
alt={profile.full_name ?? "Avatar"} alt={profile.full_name ?? "Avatar"}
/> />
) : null} ) : null}
<AvatarFallback className="bg-emerald-500/10 text-sm font-medium text-emerald-500"> <AvatarFallback className="bg-violet-500/10 text-sm font-medium text-violet-500">
{profile?.full_name?.charAt(0)?.toUpperCase() ?? {profile?.full_name?.charAt(0)?.toUpperCase() ??
profile?.email?.charAt(0)?.toUpperCase() ?? profile?.email?.charAt(0)?.toUpperCase() ??
"U"} "U"}

View File

@ -65,7 +65,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
{deal.title} {deal.title}
</h4> </h4>
{deal.status === "won" && ( {deal.status === "won" && (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-emerald-500/15 px-2 py-0.5 text-[10px] font-semibold text-emerald-400"> <span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-violet-500/15 px-2 py-0.5 text-[10px] font-semibold text-violet-400">
<Check className="h-3 w-3" /> <Check className="h-3 w-3" />
Won Won
</span> </span>
@ -87,7 +87,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
</div> </div>
<div className="mt-2 flex items-center justify-between"> <div className="mt-2 flex items-center justify-between">
<span className="text-sm font-bold text-emerald-400"> <span className="text-sm font-bold text-violet-400">
{formatCurrency(deal.value, deal.currency)} {formatCurrency(deal.value, deal.currency)}
</span> </span>
{deal.expected_close_date && ( {deal.expected_close_date && (
@ -102,7 +102,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
<div className="mt-2 flex items-center justify-end"> <div className="mt-2 flex items-center justify-end">
<span <span
title={assigneeLabel} title={assigneeLabel}
className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-500/15 text-[10px] font-semibold text-emerald-400" className="flex h-5 w-5 items-center justify-center rounded-full bg-violet-500/15 text-[10px] font-semibold text-violet-400"
> >
{initials(assigneeLabel)} {initials(assigneeLabel)}
</span> </span>

View File

@ -264,7 +264,7 @@ export function DealForm({
<select <select
value={contactId} value={contactId}
onChange={(e) => setContactId(e.target.value)} onChange={(e) => setContactId(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500"
> >
<option value="">Select a contact</option> <option value="">Select a contact</option>
{contacts.map((c) => ( {contacts.map((c) => (
@ -277,7 +277,7 @@ export function DealForm({
{linkedConversation && ( {linkedConversation && (
<Link <Link
href="/inbox" href="/inbox"
className="mt-1 inline-flex items-center gap-1.5 self-start rounded-md bg-emerald-500/10 px-2 py-1 text-xs text-emerald-400 hover:bg-emerald-500/20" className="mt-1 inline-flex items-center gap-1.5 self-start rounded-md bg-violet-500/10 px-2 py-1 text-xs text-violet-400 hover:bg-violet-500/20"
> >
<MessageSquare className="h-3 w-3" /> <MessageSquare className="h-3 w-3" />
Link to Conversation Link to Conversation
@ -304,7 +304,7 @@ export function DealForm({
<select <select
value={currency} value={currency}
onChange={(e) => setCurrency(e.target.value)} onChange={(e) => setCurrency(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-emerald-500" className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-violet-500"
> >
<option value="USD">USD</option> <option value="USD">USD</option>
<option value="EUR">EUR</option> <option value="EUR">EUR</option>
@ -328,7 +328,7 @@ export function DealForm({
<select <select
value={stageId} value={stageId}
onChange={(e) => setStageId(e.target.value)} onChange={(e) => setStageId(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-emerald-500" className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-violet-500"
> >
{stages.map((s) => ( {stages.map((s) => (
<option key={s.id} value={s.id}> <option key={s.id} value={s.id}>
@ -343,7 +343,7 @@ export function DealForm({
<select <select
value={assignedTo} value={assignedTo}
onChange={(e) => setAssignedTo(e.target.value)} onChange={(e) => setAssignedTo(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-emerald-500" className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-violet-500"
> >
<option value="">Unassigned</option> <option value="">Unassigned</option>
{profiles.map((p) => ( {profiles.map((p) => (
@ -374,7 +374,7 @@ export function DealForm({
type="button" type="button"
onClick={() => handleStatusChange("won")} onClick={() => handleStatusChange("won")}
disabled={!!statusAction || deal.status === "won"} disabled={!!statusAction || deal.status === "won"}
className="flex-1 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50" className="flex-1 bg-violet-600 text-white hover:bg-violet-700 disabled:opacity-50"
> >
{statusAction === "won" ? ( {statusAction === "won" ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
@ -428,7 +428,7 @@ export function DealForm({
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={saving || !title.trim() || !contactId || !stageId} disabled={saving || !title.trim() || !contactId || !stageId}
className="flex-1 bg-emerald-600 text-white hover:bg-emerald-700" className="flex-1 bg-violet-600 text-white hover:bg-violet-700"
> >
{saving ? "Saving..." : deal ? "Save Changes" : "Create Deal"} {saving ? "Saving..." : deal ? "Save Changes" : "Create Deal"}
</Button> </Button>

View File

@ -107,7 +107,7 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) {
tooltip="Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included." tooltip="Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included."
/> />
<Metric <Metric
icon={<DollarSign className="h-4 w-4 text-emerald-400" />} icon={<DollarSign className="h-4 w-4 text-violet-400" />}
label="Pipeline Value" label="Pipeline Value"
value={formatCurrency(stats.totalValue)} value={formatCurrency(stats.totalValue)}
tooltip="Sum of the dollar values of all deals in this pipeline, excluding deals marked as Lost." tooltip="Sum of the dollar values of all deals in this pipeline, excluding deals marked as Lost."
@ -125,7 +125,7 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) {
tooltip="Expected revenue: each open deal's value × its stage probability. First stage ≈ 10%, stages progress up to 90%, Won = 100%. Lost deals are excluded." tooltip="Expected revenue: each open deal's value × its stage probability. First stage ≈ 10%, stages progress up to 90%, Won = 100%. Lost deals are excluded."
/> />
<Metric <Metric
icon={<Trophy className="h-4 w-4 text-emerald-400" />} icon={<Trophy className="h-4 w-4 text-violet-400" />}
label="Won This Month" label="Won This Month"
value={String(stats.wonThisMonth)} value={String(stats.wonThisMonth)}
tooltip="Deals marked as Won since the first day of the current month." tooltip="Deals marked as Won since the first day of the current month."

View File

@ -206,7 +206,7 @@ function StageColumn({
ref={setNodeRef} ref={setNodeRef}
className={`mt-3 flex flex-1 flex-col gap-2 rounded-lg transition-all ${ className={`mt-3 flex flex-1 flex-col gap-2 rounded-lg transition-all ${
isOver isOver
? "bg-emerald-500/5 outline outline-2 outline-dashed outline-emerald-400 outline-offset-2" ? "bg-violet-500/5 outline outline-2 outline-dashed outline-violet-400 outline-offset-2"
: "" : ""
}`} }`}
> >

View File

@ -349,7 +349,7 @@ export function PipelineSettings({
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={saving || !name.trim()} disabled={saving || !name.trim()}
className="bg-emerald-600 text-white hover:bg-emerald-700" className="bg-violet-600 text-white hover:bg-violet-700"
> >
{saving ? "Saving..." : "Save Changes"} {saving ? "Saving..." : "Save Changes"}
</Button> </Button>

View File

@ -84,7 +84,7 @@ export function PasswordForm() {
<Card className="bg-slate-900/40 border-slate-800"> <Card className="bg-slate-900/40 border-slate-800">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-white"> <CardTitle className="flex items-center gap-2 text-white">
<KeyRound className="size-4 text-emerald-400" /> <KeyRound className="size-4 text-violet-400" />
Password Password
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">

View File

@ -214,7 +214,7 @@ export function ProfileForm() {
<CardHeader> <CardHeader>
<CardTitle className="text-white">Profile</CardTitle> <CardTitle className="text-white">Profile</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
How you show up across WaCRM. Your avatar and name appear in the How you show up across the app. Your avatar and name appear in the
header, sidebar, and anywhere your teammates see you. header, sidebar, and anywhere your teammates see you.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@ -227,7 +227,7 @@ export function ProfileForm() {
{currentAvatar ? ( {currentAvatar ? (
<AvatarImage src={currentAvatar} alt={fullName || 'Avatar'} /> <AvatarImage src={currentAvatar} alt={fullName || 'Avatar'} />
) : null} ) : null}
<AvatarFallback className="bg-emerald-500/10 text-base text-emerald-400"> <AvatarFallback className="bg-violet-500/10 text-base text-violet-400">
{initial} {initial}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>

View File

@ -52,13 +52,12 @@ export function SessionsCard() {
<Card className="bg-slate-900/40 border-slate-800"> <Card className="bg-slate-900/40 border-slate-800">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-white"> <CardTitle className="flex items-center gap-2 text-white">
<LogOut className="size-4 text-emerald-400" /> <LogOut className="size-4 text-violet-400" />
Active sessions Active sessions
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
Sign out of every device where you&apos;re logged in to WaCRM — Sign out of every device where you&apos;re logged in — including
including this one. Useful if you lost a laptop or shared your this one. Useful if you lost a laptop or shared your password.
password.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@ -142,7 +142,7 @@ export function TagManager() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="size-6 animate-spin text-emerald-500" /> <Loader2 className="size-6 animate-spin text-violet-500" />
</div> </div>
); );
} }
@ -160,7 +160,7 @@ export function TagManager() {
setSelectedColor(PRESET_COLORS[3].value); setSelectedColor(PRESET_COLORS[3].value);
setDialogOpen(true); setDialogOpen(true);
}} }}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
<Plus className="size-4" /> <Plus className="size-4" />
New Tag New Tag
@ -281,7 +281,7 @@ export function TagManager() {
<Button <Button
onClick={handleCreate} onClick={handleCreate}
disabled={saving} disabled={saving}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
{saving ? ( {saving ? (
<> <>

View File

@ -40,7 +40,7 @@ const categoryColors: Record<string, string> = {
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
Draft: 'bg-slate-600/20 text-slate-400 border-slate-600/30', Draft: 'bg-slate-600/20 text-slate-400 border-slate-600/30',
Pending: 'bg-yellow-600/20 text-yellow-400 border-yellow-600/30', Pending: 'bg-yellow-600/20 text-yellow-400 border-yellow-600/30',
Approved: 'bg-emerald-600/20 text-emerald-400 border-emerald-600/30', Approved: 'bg-violet-600/20 text-violet-400 border-violet-600/30',
Rejected: 'bg-red-600/20 text-red-400 border-red-600/30', Rejected: 'bg-red-600/20 text-red-400 border-red-600/30',
}; };
@ -246,7 +246,7 @@ export function TemplateManager() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="size-6 animate-spin text-emerald-500" /> <Loader2 className="size-6 animate-spin text-violet-500" />
</div> </div>
); );
} }
@ -280,7 +280,7 @@ export function TemplateManager() {
setForm(emptyForm); setForm(emptyForm);
setDialogOpen(true); setDialogOpen(true);
}} }}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
<Plus className="size-4" /> <Plus className="size-4" />
New Template New Template
@ -456,7 +456,7 @@ export function TemplateManager() {
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
{saving ? ( {saving ? (
<> <>

View File

@ -265,7 +265,7 @@ export function WhatsAppConfig() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="size-6 animate-spin text-emerald-500" /> <Loader2 className="size-6 animate-spin text-violet-500" />
</div> </div>
); );
} }
@ -315,7 +315,7 @@ export function WhatsAppConfig() {
<Alert className="bg-slate-900 border-slate-700"> <Alert className="bg-slate-900 border-slate-700">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{connectionStatus === 'connected' ? ( {connectionStatus === 'connected' ? (
<CheckCircle2 className="size-4 text-emerald-500" /> <CheckCircle2 className="size-4 text-violet-500" />
) : ( ) : (
<XCircle className="size-4 text-red-500" /> <XCircle className="size-4 text-red-500" />
)} )}
@ -444,7 +444,7 @@ export function WhatsAppConfig() {
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="bg-emerald-600 hover:bg-emerald-700 text-white" className="bg-violet-600 hover:bg-violet-700 text-white"
> >
{saving ? ( {saving ? (
<> <>
@ -510,13 +510,13 @@ export function WhatsAppConfig() {
<AccordionItem className="border-slate-700"> <AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-emerald-600 text-xs font-bold text-white">1</span> <span className="flex size-5 items-center justify-center rounded-full bg-violet-600 text-xs font-bold text-white">1</span>
Create a Meta App Create a Meta App
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-slate-400"> <AccordionContent className="text-slate-400">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to <span className="text-emerald-400">developers.facebook.com</span></li> <li>Go to <span className="text-violet-400">developers.facebook.com</span></li>
<li>Click &quot;My Apps&quot; and then &quot;Create App&quot;</li> <li>Click &quot;My Apps&quot; and then &quot;Create App&quot;</li>
<li>Select &quot;Business&quot; as the app type</li> <li>Select &quot;Business&quot; as the app type</li>
<li>Fill in app details and create</li> <li>Fill in app details and create</li>
@ -527,7 +527,7 @@ export function WhatsAppConfig() {
<AccordionItem className="border-slate-700"> <AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-emerald-600 text-xs font-bold text-white">2</span> <span className="flex size-5 items-center justify-center rounded-full bg-violet-600 text-xs font-bold text-white">2</span>
Add WhatsApp Product Add WhatsApp Product
</span> </span>
</AccordionTrigger> </AccordionTrigger>
@ -543,7 +543,7 @@ export function WhatsAppConfig() {
<AccordionItem className="border-slate-700"> <AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-emerald-600 text-xs font-bold text-white">3</span> <span className="flex size-5 items-center justify-center rounded-full bg-violet-600 text-xs font-bold text-white">3</span>
Get API Credentials Get API Credentials
</span> </span>
</AccordionTrigger> </AccordionTrigger>
@ -560,7 +560,7 @@ export function WhatsAppConfig() {
<AccordionItem className="border-slate-700"> <AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-emerald-600 text-xs font-bold text-white">4</span> <span className="flex size-5 items-center justify-center rounded-full bg-violet-600 text-xs font-bold text-white">4</span>
Configure Webhooks Configure Webhooks
</span> </span>
</AccordionTrigger> </AccordionTrigger>
@ -581,7 +581,7 @@ export function WhatsAppConfig() {
href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started" href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm text-emerald-400 hover:text-emerald-300 transition-colors" className="inline-flex items-center gap-1.5 text-sm text-violet-400 hover:text-violet-300 transition-colors"
> >
<ExternalLink className="size-3.5" /> <ExternalLink className="size-3.5" />
Meta WhatsApp API Documentation Meta WhatsApp API Documentation

View File

@ -15,9 +15,9 @@ function Switch({
data-slot="switch" data-slot="switch"
className={cn( className={cn(
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors", "inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950",
"disabled:cursor-not-allowed disabled:opacity-50", "disabled:cursor-not-allowed disabled:opacity-50",
"data-[checked]:bg-emerald-500 data-[unchecked]:bg-slate-700", "data-[checked]:bg-violet-500 data-[unchecked]:bg-slate-700",
className, className,
)} )}
{...props} {...props}

View File

@ -21,7 +21,7 @@ export const TRIGGER_META: Record<AutomationTriggerType, TriggerMeta> = {
}, },
new_contact_created: { new_contact_created: {
label: 'New Contact', label: 'New Contact',
pillClass: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300', pillClass: 'border-violet-500/30 bg-violet-500/10 text-violet-300',
}, },
conversation_assigned: { conversation_assigned: {
label: 'Conversation Assigned', label: 'Conversation Assigned',

View File

@ -36,7 +36,7 @@ export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
}, },
sent: { sent: {
label: "Sent", label: "Sent",
classes: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", classes: "bg-violet-500/10 text-violet-400 border-violet-500/20",
}, },
failed: { failed: {
label: "Failed", label: "Failed",
@ -55,11 +55,11 @@ export const recipientStatusConfig: Record<RecipientStatus, StatusDisplay> = {
}, },
delivered: { delivered: {
label: "Delivered", label: "Delivered",
classes: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", classes: "bg-violet-500/10 text-violet-400 border-violet-500/20",
}, },
read: { read: {
label: "Read", label: "Read",
classes: "bg-emerald-500/10 text-emerald-300 border-emerald-500/20", classes: "bg-violet-500/10 text-violet-300 border-violet-500/20",
}, },
replied: { replied: {
label: "Replied", label: "Replied",

View File

@ -22,7 +22,7 @@ const ORDER: Record<string, { order: number; section: string; description: strin
'getting-started': { 'getting-started': {
order: 1, order: 1,
section: 'Setup', section: 'Setup',
description: 'Fork, install, run WaCRM locally in about 15 minutes.', description: 'Fork, install, run the template locally in about 15 minutes.',
}, },
'supabase-setup': { 'supabase-setup': {
order: 2, order: 2,
@ -37,12 +37,12 @@ const ORDER: Record<string, { order: number; section: string; description: strin
'environment-variables': { 'environment-variables': {
order: 4, order: 4,
section: 'Setup', section: 'Setup',
description: 'Full reference for every env var WaCRM reads.', description: 'Full reference for every env var the app reads.',
}, },
'deployment-hostinger': { 'deployment-hostinger': {
order: 5, order: 5,
section: 'Deploy', section: 'Deploy',
description: 'Ship WaCRM on Hostinger Managed Node.js Hosting.', description: 'Ship the template on Hostinger Managed Node.js Hosting.',
}, },
'automations-and-cron': { 'automations-and-cron': {
order: 6, order: 6,
@ -71,9 +71,9 @@ const CLASSES = {
ul: 'my-3 pl-6 list-disc marker:text-slate-500 text-slate-300 leading-relaxed space-y-1.5', ul: 'my-3 pl-6 list-disc marker:text-slate-500 text-slate-300 leading-relaxed space-y-1.5',
ol: 'my-3 pl-6 list-decimal marker:text-slate-500 text-slate-300 leading-relaxed space-y-1.5', ol: 'my-3 pl-6 list-decimal marker:text-slate-500 text-slate-300 leading-relaxed space-y-1.5',
li: 'pl-1', li: 'pl-1',
blockquote: 'my-4 rounded-r-lg border-l-4 border-emerald-500 bg-emerald-500/5 px-4 py-3 text-slate-200', blockquote: 'my-4 rounded-r-lg border-l-4 border-violet-500 bg-violet-500/5 px-4 py-3 text-slate-200',
hr: 'my-8 border-t border-slate-800', hr: 'my-8 border-t border-slate-800',
a: 'text-emerald-400 underline decoration-transparent underline-offset-2 transition-colors hover:decoration-emerald-500/60', a: 'text-violet-400 underline decoration-transparent underline-offset-2 transition-colors hover:decoration-violet-500/60',
codespan: 'rounded-md border border-slate-700 bg-slate-800 px-1.5 py-0.5 text-[0.85em] font-mono text-slate-100', codespan: 'rounded-md border border-slate-700 bg-slate-800 px-1.5 py-0.5 text-[0.85em] font-mono text-slate-100',
pre: 'my-5 overflow-x-auto rounded-lg border border-slate-800 bg-slate-900/80 p-4 text-[0.825rem] leading-relaxed font-mono text-slate-100', pre: 'my-5 overflow-x-auto rounded-lg border border-slate-800 bg-slate-900/80 p-4 text-[0.825rem] leading-relaxed font-mono text-slate-100',
table: 'my-5 block w-full overflow-x-auto rounded-lg border border-slate-800 text-sm', table: 'my-5 block w-full overflow-x-auto rounded-lg border border-slate-800 text-sm',

View File

@ -11,7 +11,7 @@ export interface FaqItem {
export const FAQ_ITEMS: FaqItem[] = [ export const FAQ_ITEMS: FaqItem[] = [
{ {
q: 'Do I need my own WhatsApp Business API access?', q: 'Do I need my own WhatsApp Business API access?',
a: 'Yes. WaCRM plugs into your existing Meta WhatsApp Business setup — you bring the phone number and access token, we provide the CRM tooling around it. Any Meta-approved BSP (Business Solution Provider) works.', a: 'Yes. This CRM template for WhatsApp plugs into your existing Meta WhatsApp Business setup — you bring the phone number and access token, we provide the CRM tooling around it. Any Meta-approved BSP (Business Solution Provider) works.',
}, },
{ {
q: 'Can my whole team share one WhatsApp number?', q: 'Can my whole team share one WhatsApp number?',

View File

@ -9,13 +9,13 @@
export const SITE_URL = export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, '') || 'https://wacrm.tech' process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, '') || 'https://wacrm.tech'
export const SITE_NAME = 'WaCRM' export const SITE_NAME = 'CRM Template for WhatsApp'
export const SITE_TAGLINE = 'Run your WhatsApp® business from one inbox' export const SITE_TAGLINE = 'Run your WhatsApp® business from one inbox'
/** Default description — reused by layout metadata and structured data. */ /** Default description — reused by layout metadata and structured data. */
export const SITE_DESCRIPTION = export const SITE_DESCRIPTION =
'WaCRM is a WhatsApp® CRM for small teams: shared inbox, contact hub, sales pipelines, broadcasts, and no-code automations — built on the official WhatsApp Business API.' 'A CRM template for WhatsApp® built for small teams: shared inbox, contact hub, sales pipelines, broadcasts, and no-code automations — built on the official WhatsApp Business API.'
/** /**
* Keyword targets. Search engines largely ignore the meta keywords * Keyword targets. Search engines largely ignore the meta keywords
@ -40,7 +40,7 @@ export const OG_IMAGE_ALT = `${SITE_NAME} — ${SITE_TAGLINE}`
/** Organization info surfaced in JSON-LD. */ /** Organization info surfaced in JSON-LD. */
export const ORG_INFO = { export const ORG_INFO = {
name: SITE_NAME, name: SITE_NAME,
legalName: 'WaCRM', legalName: 'CRM Template for WhatsApp',
url: SITE_URL, url: SITE_URL,
logo: `${SITE_URL}/icon`, logo: `${SITE_URL}/icon`,
} }