mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
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:
commit
1f2849df75
4
.github/CODE_OF_CONDUCT.md
vendored
4
.github/CODE_OF_CONDUCT.md
vendored
@ -11,8 +11,8 @@ that standard.
|
||||
If you witness or experience behaviour that violates the Code, please
|
||||
report it privately to the project maintainer:
|
||||
|
||||
- Email: **a.donauskas@hostinger.com** with `[WaCRM conduct]` in the
|
||||
subject.
|
||||
- Email: **a.donauskas@hostinger.com** with `[CRM template conduct]` in
|
||||
the subject.
|
||||
|
||||
Reports are handled confidentially. Expect an acknowledgement within
|
||||
72 hours and a decision on next steps within a week.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,5 +1,5 @@
|
||||
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] "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -6,6 +6,6 @@ contact_links:
|
||||
- name: Setup / "how do I..." questions
|
||||
url: https://github.com/ArnasDon/wacrm/blob/main/docs/README.md
|
||||
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
|
||||
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.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -8,7 +8,7 @@ body:
|
||||
value: |
|
||||
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
|
||||
requests end up as *"build this in your fork"* rather than
|
||||
landing here. That's the point of a template.
|
||||
@ -29,7 +29,7 @@ body:
|
||||
id: problem
|
||||
attributes:
|
||||
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: |
|
||||
When a broadcast ends I have no way to see which recipients
|
||||
didn't reply so I can follow up manually.
|
||||
|
||||
6
.github/SECURITY.md
vendored
6
.github/SECURITY.md
vendored
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@ -12,7 +12,7 @@ Instead, please report privately via one of:
|
||||
|
||||
- [GitHub Security Advisories](https://github.com/ArnasDon/wacrm/security/advisories/new)
|
||||
(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:
|
||||
|
||||
@ -59,4 +59,4 @@ action against anyone who:
|
||||
- Gives us reasonable time to respond before any public disclosure.
|
||||
- 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.
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@ -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
|
||||
upstream PRs tend to land (security, correctness, docs) vs. which
|
||||
belong in a fork (new features, stack swaps, opinionated refactors).
|
||||
|
||||
@ -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:
|
||||
|
||||
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
|
||||
|
||||
- Rebrand. The WaCRM name, favicon, and `wacrm.tech` URL belong to the
|
||||
upstream project; please swap them for your own before putting your
|
||||
deployment in front of users.
|
||||
- Rebrand. The "CRM Template for WhatsApp" name, favicon, and
|
||||
`wacrm.tech` URL belong to the upstream project; please swap them
|
||||
for your own before putting your deployment in front of users.
|
||||
- Keep the MIT [`LICENSE`](./LICENSE) file — that's how the template's
|
||||
permissions travel with the code. Attribution in a `README` section
|
||||
is appreciated but not required.
|
||||
|
||||
12
README.md
12
README.md
@ -1,10 +1,10 @@
|
||||
# WaCRM
|
||||
# CRM Template for WhatsApp
|
||||
|
||||
> Self-hostable WhatsApp® CRM template — shared inbox, contacts, sales
|
||||
> pipelines, broadcasts, and no-code automations. Fork it, brand it,
|
||||
> host it.
|
||||
> Self-hostable CRM template for WhatsApp® — shared inbox, contacts,
|
||||
> sales pipelines, broadcasts, and no-code automations. Fork it, brand
|
||||
> it, host it.
|
||||
|
||||
[](./LICENSE)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/ArnasDon/wacrm/actions/workflows/ci.yml)
|
||||
[](https://nextjs.org)
|
||||
[](https://supabase.com)
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
## 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,
|
||||
your data. No SaaS lock-in, no seat pricing, no trust dance.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# WaCRM documentation
|
||||
# Documentation
|
||||
|
||||
WaCRM is a self-hostable WhatsApp® CRM built on Next.js and Supabase. These
|
||||
docs walk you from a freshly forked repo to a production deploy.
|
||||
A self-hostable CRM template for WhatsApp® built on Next.js and Supabase.
|
||||
These docs walk you from a freshly forked repo to a production deploy.
|
||||
|
||||
## Reading order
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
steps run inline — the exception is the **Wait** step, which parks the
|
||||
execution in `automation_pending_executions` until its due time.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Deploy on Hostinger
|
||||
|
||||
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
|
||||
flow from hPanel. You deploy from your fork without ever opening an SSH
|
||||
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 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 WaCRM marketing page.
|
||||
on the marketing page.
|
||||
|
||||
SSL is provisioned automatically. If you used a subdomain, Hostinger's
|
||||
AutoSSL usually takes a minute or two; until then the site may serve the
|
||||
@ -139,7 +139,7 @@ idempotent.
|
||||
|
||||
## 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:
|
||||
|
||||
- Long-running background workers beyond what the single Next.js process
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
## 1. Fork and clone
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
# Supabase setup
|
||||
|
||||
WaCRM uses Supabase for Postgres, authentication, row-level security (RLS),
|
||||
and (optionally) storage. You need one Supabase project per WaCRM
|
||||
deployment.
|
||||
This template uses Supabase for Postgres, authentication, row-level security (RLS),
|
||||
and (optionally) storage. You need one Supabase project per deployment.
|
||||
|
||||
## 1. Create the project
|
||||
|
||||
@ -75,7 +74,7 @@ emails link back correctly.
|
||||
|
||||
## 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.
|
||||
No bucket setup is required for a default install.
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ Set `NEXT_PUBLIC_SITE_URL` **and** add the same origin to Supabase →
|
||||
### Webhook verification fails in Meta
|
||||
|
||||
- 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:
|
||||
`curl 'https://crm.example.com/api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=<token>&hub.challenge=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
|
||||
|
||||
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
|
||||
`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.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
`access_token` — there is no shared app-level token. This page walks
|
||||
through getting those values and wiring the webhook.
|
||||
@ -45,9 +45,9 @@ generate a **System User** access token:
|
||||
- `whatsapp_business_messaging`
|
||||
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**.
|
||||
3. Fill in:
|
||||
- **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.
|
||||
- **Verify token** — any random string you make up; you will paste the
|
||||
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.
|
||||
|
||||
## 5. Configure the webhook in Meta
|
||||
@ -68,7 +68,7 @@ Under **WhatsApp → Configuration → Webhook**:
|
||||
- Production: `https://<your-domain>/api/whatsapp/webhook`.
|
||||
- **Verify token**: the same string you entered in step 4.
|
||||
- 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
|
||||
verify token matches and the callback URL is publicly reachable.
|
||||
|
||||
@ -86,7 +86,7 @@ sync on the broadcast UI.
|
||||
## 6. Signature verification (required)
|
||||
|
||||
Set `META_APP_SECRET` to your app's **App Secret** (Meta for Developers →
|
||||
App Settings → Basic). 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
|
||||
not set** — anyone who knew the webhook URL could otherwise spoof
|
||||
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
|
||||
a registered tester.
|
||||
2. Watch the WaCRM Inbox — the message should appear within a second or
|
||||
two.
|
||||
3. Reply from WaCRM; the recipient gets a real WhatsApp message.
|
||||
2. Watch the Inbox — the message should appear within a second or two.
|
||||
3. Reply from the app; the recipient gets a real WhatsApp message.
|
||||
|
||||
If nothing shows up, see [troubleshooting.md](./troubleshooting.md).
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "wacrm",
|
||||
"version": "0.1.0",
|
||||
"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",
|
||||
"author": "Arnas Donauskas",
|
||||
"homepage": "https://wacrm.tech",
|
||||
|
||||
@ -46,8 +46,8 @@ export default function ForgotPasswordPage() {
|
||||
<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">
|
||||
<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">
|
||||
<CheckCircle className="h-6 w-6 text-emerald-500" />
|
||||
<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-violet-500" />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-white">
|
||||
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">
|
||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
||||
<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">
|
||||
<MessageSquare className="h-6 w-6 text-emerald-500" />
|
||||
<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-violet-500" />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-white">Reset password</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
@ -104,14 +104,14 @@ export default function ForgotPasswordPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
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>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
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"}
|
||||
</Button>
|
||||
|
||||
@ -47,12 +47,12 @@ export default function LoginPage() {
|
||||
<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">
|
||||
<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">
|
||||
<MessageSquare className="h-6 w-6 text-emerald-500" />
|
||||
<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-violet-500" />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-white">Welcome back</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
Sign in to your WaCRM account
|
||||
Sign in to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -74,7 +74,7 @@ export default function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
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>
|
||||
|
||||
@ -85,7 +85,7 @@ export default function LoginPage() {
|
||||
</Label>
|
||||
<Link
|
||||
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?
|
||||
</Link>
|
||||
@ -97,14 +97,14 @@ export default function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
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>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
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"}
|
||||
</Button>
|
||||
@ -114,7 +114,7 @@ export default function LoginPage() {
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="text-emerald-500 hover:text-emerald-400"
|
||||
className="text-violet-500 hover:text-violet-400"
|
||||
>
|
||||
Create account
|
||||
</Link>
|
||||
|
||||
@ -66,8 +66,8 @@ export default function SignupPage() {
|
||||
<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">
|
||||
<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">
|
||||
<CheckCircle className="h-6 w-6 text-emerald-500" />
|
||||
<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-violet-500" />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-white">
|
||||
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">
|
||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
||||
<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">
|
||||
<MessageSquare className="h-6 w-6 text-emerald-500" />
|
||||
<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-violet-500" />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-white">Create account</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
Get started with WaCRM
|
||||
Get started with CRM Template for WhatsApp
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -124,7 +124,7 @@ export default function SignupPage() {
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
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>
|
||||
|
||||
@ -139,7 +139,7 @@ export default function SignupPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
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>
|
||||
|
||||
@ -154,7 +154,7 @@ export default function SignupPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
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>
|
||||
|
||||
@ -169,14 +169,14 @@ export default function SignupPage() {
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
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>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
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"}
|
||||
</Button>
|
||||
@ -186,7 +186,7 @@ export default function SignupPage() {
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-emerald-500 hover:text-emerald-400"
|
||||
className="text-violet-500 hover:text-violet-400"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
|
||||
@ -54,7 +54,7 @@ export default function EditAutomationPage({
|
||||
<p className="text-sm text-red-400">{error}</p>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -65,7 +65,7 @@ export default function EditAutomationPage({
|
||||
if (!initial) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ export default function AutomationLogsPage({
|
||||
if (!automation || logs === null) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@ -167,7 +167,7 @@ export default function AutomationLogsPage({
|
||||
function StatusBadge({ status }: { status: AutomationLog["status"] }) {
|
||||
const classes =
|
||||
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"
|
||||
? "border-amber-500/30 bg-amber-500/10 text-amber-300"
|
||||
: "border-red-500/30 bg-red-500/10 text-red-300"
|
||||
@ -190,7 +190,7 @@ function StepRow({ result }: { result: AutomationLogStepResult }) {
|
||||
<span
|
||||
className={cn(
|
||||
"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
|
||||
>
|
||||
|
||||
@ -146,7 +146,7 @@ export default function AutomationsPage() {
|
||||
if (automations === null) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@ -164,7 +164,7 @@ export default function AutomationsPage() {
|
||||
</div>
|
||||
<Button
|
||||
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" />
|
||||
Create Automation
|
||||
@ -182,9 +182,9 @@ export default function AutomationsPage() {
|
||||
<button
|
||||
key={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" />
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-white">{t.name}</div>
|
||||
@ -198,8 +198,8 @@ export default function AutomationsPage() {
|
||||
|
||||
{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-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10">
|
||||
<Zap className="h-6 w-6 text-emerald-500" />
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-500/10">
|
||||
<Zap className="h-6 w-6 text-violet-500" />
|
||||
</div>
|
||||
<p className="mt-3 text-sm font-medium text-white">No automations yet</p>
|
||||
<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">
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<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
|
||||
>
|
||||
<Zap className="h-5 w-5 text-emerald-400" />
|
||||
<Zap className="h-5 w-5 text-violet-400" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
@ -292,8 +292,8 @@ function AutomationCard({
|
||||
</span>
|
||||
{automation.is_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="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
||||
<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-violet-500" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -246,7 +246,7 @@ export default function BroadcastDetailPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -265,7 +265,7 @@ export default function BroadcastDetailPage() {
|
||||
const status = getBroadcastStatus(broadcast.status);
|
||||
|
||||
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: 'Read', value: broadcast.read_count, color: 'bg-blue-500' },
|
||||
{ label: 'Replied', value: broadcast.replied_count, color: 'bg-indigo-500' },
|
||||
@ -361,7 +361,7 @@ export default function BroadcastDetailPage() {
|
||||
value={broadcast.sent_count}
|
||||
total={broadcast.total_recipients}
|
||||
icon={<Send className="h-4 w-4" />}
|
||||
color="bg-emerald-500/10 text-emerald-400"
|
||||
color="bg-violet-500/10 text-violet-400"
|
||||
/>
|
||||
<StatCard
|
||||
label="Delivered"
|
||||
@ -423,7 +423,7 @@ export default function BroadcastDetailPage() {
|
||||
<DropdownMenuItem
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={
|
||||
statusFilter === 'all' ? 'text-emerald-400' : 'text-slate-300'
|
||||
statusFilter === 'all' ? 'text-violet-400' : 'text-slate-300'
|
||||
}
|
||||
>
|
||||
All statuses
|
||||
@ -434,7 +434,7 @@ export default function BroadcastDetailPage() {
|
||||
onClick={() => setStatusFilter(s)}
|
||||
className={
|
||||
statusFilter === s
|
||||
? 'text-emerald-400'
|
||||
? 'text-violet-400'
|
||||
: 'text-slate-300'
|
||||
}
|
||||
>
|
||||
|
||||
@ -140,9 +140,9 @@ export default function NewBroadcastPage() {
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium transition-all ${
|
||||
isCompleted
|
||||
? 'bg-emerald-500 text-white'
|
||||
? 'bg-violet-500 text-white'
|
||||
: 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'
|
||||
}`}
|
||||
>
|
||||
@ -150,7 +150,7 @@ export default function NewBroadcastPage() {
|
||||
</div>
|
||||
<span
|
||||
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}
|
||||
@ -159,7 +159,7 @@ export default function NewBroadcastPage() {
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={`mx-3 h-px flex-1 ${
|
||||
index < currentStep ? 'bg-emerald-500' : 'bg-slate-800'
|
||||
index < currentStep ? 'bg-violet-500' : 'bg-slate-800'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -35,7 +35,7 @@ function RateCell({
|
||||
}: {
|
||||
value: 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;
|
||||
}) {
|
||||
const pct = percent(value, total);
|
||||
@ -128,7 +128,7 @@ export default function BroadcastsPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -154,7 +154,7 @@ export default function BroadcastsPage() {
|
||||
aria-label="Broadcast in progress"
|
||||
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>{`
|
||||
.broadcast-indeterminate-bar {
|
||||
width: 33%;
|
||||
@ -183,7 +183,7 @@ export default function BroadcastsPage() {
|
||||
</div>
|
||||
<Button
|
||||
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" />
|
||||
New Broadcast
|
||||
@ -199,7 +199,7 @@ export default function BroadcastsPage() {
|
||||
</p>
|
||||
<Button
|
||||
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" />
|
||||
New Broadcast
|
||||
@ -243,7 +243,7 @@ export default function BroadcastsPage() {
|
||||
<RateCell
|
||||
value={broadcast.delivered_count}
|
||||
total={broadcast.total_recipients}
|
||||
color="bg-emerald-500"
|
||||
color="bg-violet-500"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
|
||||
@ -226,7 +226,7 @@ export default function ContactsPage() {
|
||||
</Button>
|
||||
<Button
|
||||
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" />
|
||||
Add Contact
|
||||
@ -269,7 +269,7 @@ export default function ContactsPage() {
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={7} className="text-center py-12">
|
||||
<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>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@ -29,7 +29,7 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-slate-950">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -302,7 +302,7 @@ export default function PipelinesPage() {
|
||||
<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"
|
||||
>
|
||||
<GitBranch className="h-4 w-4 text-emerald-500" />
|
||||
<GitBranch className="h-4 w-4 text-violet-500" />
|
||||
<span className="font-semibold">
|
||||
{selectedPipeline?.name ?? "Select Pipeline"}
|
||||
</span>
|
||||
@ -323,7 +323,7 @@ export default function PipelinesPage() {
|
||||
onClick={() => setSelectedPipelineId(p.id)}
|
||||
className={
|
||||
p.id === selectedPipelineId
|
||||
? "text-emerald-400"
|
||||
? "text-violet-400"
|
||||
: "text-slate-300"
|
||||
}
|
||||
>
|
||||
@ -357,7 +357,7 @@ export default function PipelinesPage() {
|
||||
<Button
|
||||
onClick={() => handleAddDeal()}
|
||||
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" />
|
||||
Add Deal
|
||||
@ -377,7 +377,7 @@ export default function PipelinesPage() {
|
||||
</p>
|
||||
<Button
|
||||
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" />
|
||||
Create Pipeline
|
||||
@ -428,7 +428,7 @@ export default function PipelinesPage() {
|
||||
<Button
|
||||
onClick={handleCreatePipeline}
|
||||
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"}
|
||||
</Button>
|
||||
|
||||
@ -48,28 +48,28 @@ export default function SettingsPage() {
|
||||
<TabsList className="bg-slate-900 border border-slate-700">
|
||||
<TabsTrigger
|
||||
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" />
|
||||
Profile
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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" />
|
||||
WhatsApp Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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" />
|
||||
Templates
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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" />
|
||||
Tags
|
||||
|
||||
@ -11,11 +11,11 @@ import { decrypt } from '@/lib/whatsapp/encryption'
|
||||
* template locally, try to broadcast with it, and hit Meta's error
|
||||
* #132001 "Template name does not exist in the translation" — because
|
||||
* 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)
|
||||
* 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.
|
||||
*
|
||||
* Scope:
|
||||
|
||||
@ -37,7 +37,7 @@ export default async function DocPage({ params }: DocPageProps) {
|
||||
|
||||
return (
|
||||
<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}
|
||||
</p>
|
||||
<div
|
||||
|
||||
@ -3,9 +3,12 @@ import { DocsShell } from '@/components/docs/shell'
|
||||
import { listDocs } from '@/lib/docs/content'
|
||||
|
||||
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:
|
||||
'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({
|
||||
|
||||
@ -12,11 +12,11 @@ export default async function DocsIndexPage() {
|
||||
|
||||
return (
|
||||
<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
|
||||
</p>
|
||||
<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>
|
||||
<p className="mt-4 text-base leading-relaxed text-slate-400">
|
||||
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>
|
||||
)}
|
||||
<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
|
||||
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.18 0.01 260);
|
||||
--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);
|
||||
--secondary: oklch(0.22 0.01 260);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
@ -66,8 +66,8 @@
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.28 0.01 260);
|
||||
--input: oklch(0.28 0.01 260);
|
||||
--ring: oklch(0.696 0.17 162);
|
||||
--chart-1: oklch(0.696 0.17 162);
|
||||
--ring: oklch(0.526 0.247 293);
|
||||
--chart-1: oklch(0.526 0.247 293);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
@ -75,12 +75,12 @@
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.16 0.01 260);
|
||||
--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-accent: oklch(0.22 0.01 260);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--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 {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
// Replaces the default Next.js favicon with the WaCRM mark — emerald
|
||||
// rounded square + white chat-bubble glyph — matching the sidebar
|
||||
// logo in `src/components/layout/sidebar.tsx`. Next.js renders this
|
||||
// at build time and auto-injects <link rel="icon"> into <head>.
|
||||
// Replaces the default Next.js favicon with the brand mark — Hostinger
|
||||
// violet rounded square + white chat-square glyph — matching the
|
||||
// sidebar logo in `src/components/layout/sidebar.tsx`. Next.js renders
|
||||
// this at build time and auto-injects <link rel="icon"> into <head>.
|
||||
//
|
||||
// This route takes precedence over src/app/favicon.ico, which is the
|
||||
// Next.js default and can stay on disk harmlessly (or be removed).
|
||||
@ -22,7 +22,7 @@ export default function Icon() {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#10b981", // emerald-500
|
||||
background: "#7c3aed", // violet-600 (Hostinger-aligned purple)
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
|
||||
@ -38,7 +38,7 @@ export default async function Image() {
|
||||
justifyContent: 'center',
|
||||
padding: 80,
|
||||
background:
|
||||
'linear-gradient(135deg, #020617 0%, #0f172a 55%, #064e3b 100%)',
|
||||
'linear-gradient(135deg, #020617 0%, #0f172a 55%, #2e1065 100%)',
|
||||
color: 'white',
|
||||
fontFamily: 'sans-serif',
|
||||
}}
|
||||
@ -49,7 +49,7 @@ export default async function Image() {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 14,
|
||||
background: '#10b981',
|
||||
background: '#7c3aed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@ -78,7 +78,7 @@ export default async function Image() {
|
||||
}}
|
||||
>
|
||||
<span>Run your WhatsApp® business from </span>
|
||||
<span style={{ color: '#34d399' }}>one inbox.</span>
|
||||
<span style={{ color: '#a78bfa' }}>one inbox.</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@ -5,7 +5,7 @@ import { SITE_URL } from '@/lib/seo/site-config'
|
||||
* robots.txt served at /robots.txt. We explicitly disallow every
|
||||
* gated area so crawlers don't spend their budget on pages they'll
|
||||
* bounce off. Auth pages are closed off too — no SEO value, and
|
||||
* we don't want "Sign in to WaCRM" competing with the landing page
|
||||
* we don't want a "Sign in" page competing with the landing page
|
||||
* in SERPs.
|
||||
*/
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
|
||||
@ -77,17 +77,17 @@ interface StepMeta {
|
||||
}
|
||||
|
||||
const STEP_META: Record<AutomationStepType, StepMeta> = {
|
||||
send_message: { label: "Send Message", icon: MessageSquare, border: "border-l-emerald-500" },
|
||||
send_template: { label: "Send Template", icon: FileText, border: "border-l-emerald-500" },
|
||||
add_tag: { label: "Add Tag", icon: Tag, border: "border-l-emerald-500" },
|
||||
remove_tag: { label: "Remove Tag", icon: TagIcon, border: "border-l-emerald-500" },
|
||||
assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-emerald-500" },
|
||||
update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-emerald-500" },
|
||||
create_deal: { label: "Create Deal", icon: Briefcase, 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-violet-500" },
|
||||
add_tag: { label: "Add Tag", icon: Tag, border: "border-l-violet-500" },
|
||||
remove_tag: { label: "Remove Tag", icon: TagIcon, border: "border-l-violet-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-violet-500" },
|
||||
create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-violet-500" },
|
||||
wait: { label: "Wait", icon: Hourglass, border: "border-l-slate-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" },
|
||||
close_conversation: { label: "Close Conversation", icon: CircleSlash, 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-violet-500" },
|
||||
}
|
||||
|
||||
const ADDABLE_STEPS: AutomationStepType[] = [
|
||||
@ -275,7 +275,7 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
|
||||
<Button
|
||||
onClick={save}
|
||||
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}
|
||||
{isEditing ? "Save" : "Save Draft"}
|
||||
@ -356,7 +356,7 @@ function TriggerCard({
|
||||
<select
|
||||
value={type}
|
||||
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) => (
|
||||
<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
|
||||
// cards. Two-column grid returns on sm+.
|
||||
<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} />
|
||||
</BranchColumn>
|
||||
<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 />
|
||||
<DropdownMenu>
|
||||
<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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
|
||||
@ -48,7 +48,7 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -88,7 +88,7 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
|
||||
onClick={() => onSelect(template)}
|
||||
className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@ -123,7 +123,7 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
|
||||
<Button
|
||||
onClick={onNext}
|
||||
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
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
@ -279,14 +279,14 @@ export function Step2SelectAudience({
|
||||
}
|
||||
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
|
||||
isSelected
|
||||
? 'bg-emerald-500/10 text-emerald-400'
|
||||
? 'bg-violet-500/10 text-violet-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">
|
||||
<p className="mb-3 text-sm font-medium text-white">Select Tags</p>
|
||||
{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 ? (
|
||||
<p className="text-xs text-slate-400">
|
||||
No tags found. Create tags in Settings.
|
||||
@ -322,7 +322,7 @@ export function Step2SelectAudience({
|
||||
onClick={() => toggleTag(tag.id)}
|
||||
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@ -343,7 +343,7 @@ export function Step2SelectAudience({
|
||||
<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>
|
||||
{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 ? (
|
||||
<p className="text-xs text-slate-400">
|
||||
No custom fields defined. Create one in Settings → Custom Fields.
|
||||
@ -353,7 +353,7 @@ export function Step2SelectAudience({
|
||||
<select
|
||||
value={audience.customField?.fieldId ?? ''}
|
||||
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>
|
||||
{customFields.map((f) => (
|
||||
@ -369,7 +369,7 @@ export function Step2SelectAudience({
|
||||
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) => (
|
||||
<option key={op.value} value={op.value}>
|
||||
@ -382,7 +382,7 @@ export function Step2SelectAudience({
|
||||
value={audience.customField?.value ?? ''}
|
||||
onChange={(e) => updateCustomField({ value: e.target.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>
|
||||
)}
|
||||
@ -431,12 +431,12 @@ export function Step2SelectAudience({
|
||||
<p className="mb-2 text-sm font-medium text-white">Audience Summary</p>
|
||||
{loadingCount ? (
|
||||
<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>
|
||||
</div>
|
||||
) : estimatedCount !== null ? (
|
||||
<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">
|
||||
{estimatedCount.toLocaleString()}
|
||||
</span>
|
||||
@ -461,7 +461,7 @@ export function Step2SelectAudience({
|
||||
<Button
|
||||
onClick={onNext}
|
||||
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
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
@ -211,7 +211,7 @@ export function Step3Personalize({
|
||||
className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@ -313,16 +313,16 @@ export function Step3Personalize({
|
||||
sees approximately what the recipient will see. */}
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
||||
<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>
|
||||
<span className="text-xs text-slate-500">({previewLabel})</span>
|
||||
{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 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">
|
||||
<p className="whitespace-pre-wrap text-sm text-emerald-50">
|
||||
<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-violet-50">
|
||||
{previewText}
|
||||
</p>
|
||||
</div>
|
||||
@ -351,7 +351,7 @@ export function Step3Personalize({
|
||||
<Button
|
||||
onClick={onNext}
|
||||
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
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
@ -127,10 +127,10 @@ export function Step4ScheduleSend({
|
||||
<p className="text-xs text-slate-400">Estimated Reach</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
@ -151,11 +151,11 @@ export function Step4ScheduleSend({
|
||||
onClick={() => setTiming('now')}
|
||||
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
@ -180,17 +180,17 @@ export function Step4ScheduleSend({
|
||||
|
||||
{/* Processing overlay */}
|
||||
{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="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>
|
||||
</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 className="h-1.5 w-full rounded-full bg-slate-800">
|
||||
<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}%` }}
|
||||
/>
|
||||
</div>
|
||||
@ -226,7 +226,7 @@ export function Step4ScheduleSend({
|
||||
render={
|
||||
<Button
|
||||
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);
|
||||
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" />
|
||||
Confirm & Send
|
||||
|
||||
@ -331,7 +331,7 @@ export function ContactDetailView({
|
||||
>
|
||||
{loading || !contact ? (
|
||||
<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 className="flex flex-col h-full">
|
||||
@ -339,7 +339,7 @@ export function ContactDetailView({
|
||||
<SheetHeader className="p-4 border-b border-slate-700/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<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)}
|
||||
</AvatarFallback>
|
||||
</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">
|
||||
<button
|
||||
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" />
|
||||
{contact.phone}
|
||||
{copiedPhone ? (
|
||||
<Check className="size-3 text-emerald-400" />
|
||||
<Check className="size-3 text-violet-400" />
|
||||
) : (
|
||||
<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">
|
||||
<TabsTrigger
|
||||
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
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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
|
||||
</TabsTrigger>
|
||||
@ -455,7 +455,7 @@ export function ContactDetailView({
|
||||
<Button
|
||||
onClick={saveDetails}
|
||||
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"
|
||||
>
|
||||
{savingDetails ? (
|
||||
@ -489,7 +489,7 @@ export function ContactDetailView({
|
||||
disabled={savingTags}
|
||||
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${
|
||||
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'
|
||||
}`}
|
||||
style={{
|
||||
@ -519,7 +519,7 @@ export function ContactDetailView({
|
||||
<Button
|
||||
onClick={addNote}
|
||||
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"
|
||||
>
|
||||
{savingNote ? (
|
||||
@ -605,7 +605,7 @@ export function ContactDetailView({
|
||||
<Button
|
||||
onClick={saveCustomFields}
|
||||
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"
|
||||
>
|
||||
{savingCustom ? (
|
||||
@ -623,7 +623,7 @@ export function ContactDetailView({
|
||||
<TabsContent value="deals" className="flex-1 overflow-y-auto px-4 py-3">
|
||||
{loadingDeals ? (
|
||||
<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>
|
||||
) : deals.length === 0 ? (
|
||||
<p className="text-xs text-slate-500">No deals yet</p>
|
||||
@ -663,7 +663,7 @@ export function ContactDetailView({
|
||||
<span
|
||||
className={
|
||||
deal.status === 'won'
|
||||
? 'text-emerald-400'
|
||||
? 'text-violet-400'
|
||||
: 'text-red-400'
|
||||
}
|
||||
>
|
||||
|
||||
@ -245,7 +245,7 @@ export function ContactForm({
|
||||
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 ${
|
||||
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'
|
||||
}`}
|
||||
style={{
|
||||
@ -274,7 +274,7 @@ export function ContactForm({
|
||||
<Button
|
||||
type="submit"
|
||||
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" />}
|
||||
{isEdit ? 'Update' : 'Create'}
|
||||
|
||||
@ -196,11 +196,11 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
{/* Upload area */}
|
||||
<div
|
||||
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 ? (
|
||||
<>
|
||||
<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-xs text-slate-500">
|
||||
{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>
|
||||
<div className="flex items-center gap-4">
|
||||
{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" />
|
||||
{result.imported} imported
|
||||
</div>
|
||||
@ -299,7 +299,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
type="button"
|
||||
disabled={parsedRows.length === 0 || importing}
|
||||
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" />}
|
||||
Import {parsedRows.length > 0 ? `${parsedRows.length} Contacts` : ''}
|
||||
|
||||
@ -32,7 +32,7 @@ interface KindTheme {
|
||||
|
||||
const KIND_THEME: Record<ActivityKind, KindTheme> = {
|
||||
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' },
|
||||
broadcast: { icon: Radio, badge: 'bg-amber-500/10 text-amber-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>
|
||||
<Link
|
||||
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 →
|
||||
</Link>
|
||||
|
||||
@ -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">
|
||||
<LegendDot color="#3b82f6" label="Incoming" />
|
||||
<LegendDot color="#10b981" label="Outgoing" />
|
||||
<LegendDot color="#7c3aed" label="Outgoing" />
|
||||
</footer>
|
||||
</section>
|
||||
)
|
||||
@ -238,11 +238,11 @@ function LineSvg({
|
||||
) : null,
|
||||
)}
|
||||
|
||||
{/* Outgoing polyline (emerald) */}
|
||||
{/* Outgoing polyline (violet) */}
|
||||
<path
|
||||
d={outgoingPath}
|
||||
fill="none"
|
||||
stroke="#10b981"
|
||||
stroke="#7c3aed"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@ -269,7 +269,7 @@ function LineSvg({
|
||||
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].outgoing)} r={3.5} fill="#10b981" />
|
||||
<circle cx={hoverX} cy={yFor(data[hover.idx].outgoing)} r={3.5} fill="#7c3aed" />
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
@ -289,8 +289,8 @@ function LineSvg({
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-blue-500" />
|
||||
{hovered.incoming} incoming
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5 text-emerald-300">
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||
<span className="flex items-center gap-1.5 text-violet-300">
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-violet-500" />
|
||||
{hovered.outgoing} outgoing
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -43,7 +43,7 @@ export function MetricCard({ title, value, icon: Icon, delta, subtitle }: Metric
|
||||
function DeltaRow({ sign, label }: { sign: number; label: string }) {
|
||||
const tone =
|
||||
sign > 0
|
||||
? 'text-emerald-400'
|
||||
? 'text-violet-400'
|
||||
: sign < 0
|
||||
? 'text-red-400'
|
||||
: 'text-slate-500'
|
||||
|
||||
@ -16,7 +16,7 @@ interface 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 Broadcast', href: '/broadcasts/new', icon: Radio, tint: 'text-amber-400' },
|
||||
{ label: 'New Automation', href: '/automations/new', icon: Zap, tint: 'text-violet-400' },
|
||||
|
||||
@ -158,7 +158,7 @@ function Bars({
|
||||
width={barW}
|
||||
height={muted ? 2 : Math.max(1, h)}
|
||||
rx={4}
|
||||
fill={muted ? 'rgb(51 65 85)' : '#10b981'}
|
||||
fill={muted ? 'rgb(51 65 85)' : '#7c3aed'}
|
||||
opacity={muted ? 0.6 : 1}
|
||||
>
|
||||
<title>
|
||||
|
||||
@ -32,11 +32,17 @@ export function DocsShell({ pages, children }: DocsShellProps) {
|
||||
>
|
||||
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</button>
|
||||
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500">
|
||||
<Link
|
||||
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" />
|
||||
</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">
|
||||
Docs
|
||||
</span>
|
||||
@ -61,7 +67,7 @@ export function DocsShell({ pages, children }: DocsShellProps) {
|
||||
</a>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
|
||||
@ -51,7 +51,7 @@ export function DocsSidebar({ pages, onNavigate }: DocsSidebarProps) {
|
||||
className={cn(
|
||||
'block rounded-md px-3 py-1.5 transition-colors',
|
||||
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',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -156,7 +156,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
||||
<Phone className="h-4 w-4 text-slate-500" />
|
||||
<span className="flex-1 text-left">{contact.phone}</span>
|
||||
{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" />
|
||||
)}
|
||||
@ -259,11 +259,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
placeholder="Add a note..."
|
||||
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
|
||||
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}
|
||||
disabled={!newNote.trim() || addingNote}
|
||||
>
|
||||
|
||||
@ -24,7 +24,7 @@ interface ConversationListProps {
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<ConversationStatus, string> = {
|
||||
open: "bg-emerald-500",
|
||||
open: "bg-violet-500",
|
||||
pending: "bg-amber-500",
|
||||
closed: "bg-slate-500",
|
||||
};
|
||||
@ -145,7 +145,7 @@ export function ConversationList({
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
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>
|
||||
|
||||
@ -165,7 +165,7 @@ export function ConversationList({
|
||||
className={cn(
|
||||
"text-sm",
|
||||
filter === opt.value
|
||||
? "text-emerald-400"
|
||||
? "text-violet-400"
|
||||
: "text-slate-300"
|
||||
)}
|
||||
>
|
||||
@ -180,7 +180,7 @@ export function ConversationList({
|
||||
<ScrollArea className="flex-1">
|
||||
{loading ? (
|
||||
<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>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="px-4 py-12 text-center">
|
||||
@ -233,7 +233,7 @@ function ConversationItem({
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"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 */}
|
||||
@ -263,7 +263,7 @@ function ConversationItem({
|
||||
</p>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@ -93,7 +93,7 @@ function MediaImage({ url, alt }: { url: string; alt: string }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -185,7 +185,7 @@ function MessageContent({ message }: { message: Message }) {
|
||||
case "template":
|
||||
return (
|
||||
<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" />
|
||||
Template
|
||||
</span>
|
||||
@ -226,7 +226,7 @@ export function MessageBubble({ message }: MessageBubbleProps) {
|
||||
className={cn(
|
||||
"relative max-w-[75%] rounded-2xl px-3 py-2",
|
||||
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"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -107,14 +107,14 @@ export function MessageComposer({
|
||||
disabled={sessionExpired}
|
||||
rows={1}
|
||||
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"
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
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}
|
||||
onClick={handleSend}
|
||||
>
|
||||
|
||||
@ -67,7 +67,7 @@ function groupMessagesByDate(messages: Message[]) {
|
||||
}
|
||||
|
||||
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: "Closed", value: "closed", color: "text-slate-400" },
|
||||
];
|
||||
@ -319,7 +319,7 @@ export function MessageThread({
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"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" />
|
||||
@ -369,7 +369,7 @@ export function MessageThread({
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4">
|
||||
{loading ? (
|
||||
<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>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
|
||||
@ -10,10 +10,10 @@ export function CtaBanner() {
|
||||
return (
|
||||
<Section className="py-16 sm:py-20">
|
||||
<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={{
|
||||
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">
|
||||
@ -25,7 +25,7 @@ export function CtaBanner() {
|
||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<Link
|
||||
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
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
@ -31,7 +31,7 @@ export function FAQ() {
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'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>
|
||||
|
||||
@ -37,7 +37,7 @@ export function FeatureSpotlight({
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
</p>
|
||||
<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">
|
||||
{bullets.map((b) => (
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@ -32,7 +32,7 @@ const FEATURES: Feature[] = [
|
||||
description:
|
||||
'Tags, custom fields, notes, and automatic deduplication. Import existing contacts from CSV.',
|
||||
icon: Users,
|
||||
tint: 'text-emerald-400 bg-emerald-500/10',
|
||||
tint: 'text-violet-400 bg-violet-500/10',
|
||||
},
|
||||
{
|
||||
title: 'Sales pipelines',
|
||||
@ -70,7 +70,7 @@ export function FeaturesGrid() {
|
||||
<SectionHeader
|
||||
eyebrow="Everything you need"
|
||||
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">
|
||||
|
||||
@ -7,11 +7,17 @@ export function Footer() {
|
||||
<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="col-span-2 sm:col-span-2">
|
||||
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500">
|
||||
<Link
|
||||
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" />
|
||||
</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>
|
||||
<p className="mt-3 max-w-sm text-sm text-slate-500">
|
||||
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="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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,20 +17,20 @@ export function Hero() {
|
||||
className="pointer-events-none absolute inset-0 -z-10 opacity-60"
|
||||
style={{
|
||||
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>
|
||||
<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
|
||||
</div>
|
||||
|
||||
<h1 className="mt-5 text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
|
||||
Run your WhatsApp business from{' '}
|
||||
<span className="text-emerald-400">one inbox.</span>
|
||||
<span className="text-violet-400">one inbox.</span>
|
||||
</h1>
|
||||
|
||||
<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">
|
||||
<Link
|
||||
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
|
||||
<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">
|
||||
<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
|
||||
</span>
|
||||
<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
|
||||
</span>
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -43,7 +43,7 @@ export function HowItWorks() {
|
||||
className="relative rounded-xl border border-slate-800 bg-slate-900/40 p-6"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<span
|
||||
|
||||
@ -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="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 className="space-y-3 p-4">
|
||||
@ -47,7 +49,7 @@ export function AnalyticsMock() {
|
||||
</div>
|
||||
<div
|
||||
className={`text-[10px] tabular-nums ${
|
||||
s.positive ? 'text-emerald-400' : 'text-red-400'
|
||||
s.positive ? 'text-violet-400' : 'text-red-400'
|
||||
}`}
|
||||
>
|
||||
{s.delta}
|
||||
@ -67,7 +69,7 @@ export function AnalyticsMock() {
|
||||
Incoming
|
||||
</span>
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
@ -89,7 +91,7 @@ export function AnalyticsMock() {
|
||||
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} />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@ -19,8 +19,8 @@ export function AutomationMock() {
|
||||
kind: 'action',
|
||||
label: 'Send "Hi! 👋 Thanks for reaching out."',
|
||||
icon: MessageSquare,
|
||||
accent: 'border-l-emerald-500',
|
||||
badgeClass: 'bg-emerald-500/10 text-emerald-400',
|
||||
accent: 'border-l-violet-500',
|
||||
badgeClass: 'bg-violet-500/10 text-violet-400',
|
||||
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="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>
|
||||
|
||||
{/* Dotted canvas — matches the real flow builder's background. */}
|
||||
|
||||
@ -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="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 className="grid grid-cols-[180px_1fr] lg:grid-cols-[220px_1fr]">
|
||||
@ -33,16 +35,16 @@ export function InboxMock() {
|
||||
<div
|
||||
key={i}
|
||||
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]}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<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>
|
||||
<p className="truncate text-[10px] text-slate-500">{c.preview}</p>
|
||||
</div>
|
||||
@ -53,14 +55,14 @@ export function InboxMock() {
|
||||
{/* Thread */}
|
||||
<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">
|
||||
<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
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-white">Aisha</div>
|
||||
<div className="text-[10px] text-slate-500">+44 7700 900123</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
|
||||
</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">
|
||||
Hi! Is the kit available in large?
|
||||
</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 📦
|
||||
</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">
|
||||
@ -85,7 +87,7 @@ export function InboxMock() {
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
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" />
|
||||
</button>
|
||||
|
||||
@ -23,7 +23,7 @@ export function PipelineMock() {
|
||||
},
|
||||
{
|
||||
name: 'Won',
|
||||
color: 'bg-emerald-500',
|
||||
color: 'bg-violet-500',
|
||||
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="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 className="grid grid-cols-3 gap-3 p-4">
|
||||
|
||||
@ -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">
|
||||
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500">
|
||||
<Link
|
||||
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" />
|
||||
</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>
|
||||
|
||||
<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(
|
||||
btnBase,
|
||||
'bg-emerald-500 text-white hover:bg-emerald-400',
|
||||
'bg-violet-500 text-white hover:bg-violet-400',
|
||||
mobile && 'justify-center',
|
||||
)
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ export function OpenSource() {
|
||||
<SectionHeader
|
||||
eyebrow="Open source"
|
||||
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">
|
||||
@ -32,7 +32,7 @@ export function OpenSource() {
|
||||
Clone or fork the repository, tweak the code, ship your own build.
|
||||
MIT-style freedom with the full CRM underneath.
|
||||
</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
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
@ -42,23 +42,23 @@ export function OpenSource() {
|
||||
href={HOSTINGER_URL}
|
||||
target="_blank"
|
||||
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" />
|
||||
</div>
|
||||
<h3 className="mt-4 flex flex-wrap items-center gap-2 text-base font-semibold text-white">
|
||||
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
|
||||
</span>
|
||||
</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-slate-400">
|
||||
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.
|
||||
</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
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
@ -70,7 +70,7 @@ export function OpenSource() {
|
||||
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"
|
||||
>
|
||||
<BookOpen className="h-4 w-4 text-emerald-400" />
|
||||
<BookOpen className="h-4 w-4 text-violet-400" />
|
||||
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" />
|
||||
</Link>
|
||||
|
||||
@ -54,7 +54,7 @@ export function SectionHeader({
|
||||
return (
|
||||
<div className={cn(base, max, 'mb-12')}>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -80,7 +80,7 @@ export function Header({ onOpenSidebar }: HeaderProps) {
|
||||
alt={profile.full_name ?? "Avatar"}
|
||||
/>
|
||||
) : 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}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
@ -111,10 +111,12 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
||||
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">
|
||||
<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" />
|
||||
</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>
|
||||
<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).
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
||||
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",
|
||||
)}
|
||||
>
|
||||
@ -156,8 +158,8 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
||||
aria-label={`${totalUnread} unread conversation${totalUnread === 1 ? "" : "s"}`}
|
||||
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="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
||||
<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-violet-500" />
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
@ -178,7 +180,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
||||
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",
|
||||
)}
|
||||
>
|
||||
@ -202,7 +204,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
||||
alt={profile.full_name ?? "Avatar"}
|
||||
/>
|
||||
) : 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?.email?.charAt(0)?.toUpperCase() ??
|
||||
"U"}
|
||||
|
||||
@ -65,7 +65,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
|
||||
{deal.title}
|
||||
</h4>
|
||||
{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" />
|
||||
Won
|
||||
</span>
|
||||
@ -87,7 +87,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
|
||||
</div>
|
||||
|
||||
<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)}
|
||||
</span>
|
||||
{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">
|
||||
<span
|
||||
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)}
|
||||
</span>
|
||||
|
||||
@ -264,7 +264,7 @@ export function DealForm({
|
||||
<select
|
||||
value={contactId}
|
||||
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>
|
||||
{contacts.map((c) => (
|
||||
@ -277,7 +277,7 @@ export function DealForm({
|
||||
{linkedConversation && (
|
||||
<Link
|
||||
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" />
|
||||
Link to Conversation
|
||||
@ -304,7 +304,7 @@ export function DealForm({
|
||||
<select
|
||||
value={currency}
|
||||
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="EUR">EUR</option>
|
||||
@ -328,7 +328,7 @@ export function DealForm({
|
||||
<select
|
||||
value={stageId}
|
||||
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) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
@ -343,7 +343,7 @@ export function DealForm({
|
||||
<select
|
||||
value={assignedTo}
|
||||
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>
|
||||
{profiles.map((p) => (
|
||||
@ -374,7 +374,7 @@ export function DealForm({
|
||||
type="button"
|
||||
onClick={() => handleStatusChange("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" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@ -428,7 +428,7 @@ export function DealForm({
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
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"}
|
||||
</Button>
|
||||
|
||||
@ -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."
|
||||
/>
|
||||
<Metric
|
||||
icon={<DollarSign className="h-4 w-4 text-emerald-400" />}
|
||||
icon={<DollarSign className="h-4 w-4 text-violet-400" />}
|
||||
label="Pipeline Value"
|
||||
value={formatCurrency(stats.totalValue)}
|
||||
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."
|
||||
/>
|
||||
<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"
|
||||
value={String(stats.wonThisMonth)}
|
||||
tooltip="Deals marked as Won since the first day of the current month."
|
||||
|
||||
@ -206,7 +206,7 @@ function StageColumn({
|
||||
ref={setNodeRef}
|
||||
className={`mt-3 flex flex-1 flex-col gap-2 rounded-lg transition-all ${
|
||||
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"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -349,7 +349,7 @@ export function PipelineSettings({
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
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"}
|
||||
</Button>
|
||||
|
||||
@ -84,7 +84,7 @@ export function PasswordForm() {
|
||||
<Card className="bg-slate-900/40 border-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<KeyRound className="size-4 text-emerald-400" />
|
||||
<KeyRound className="size-4 text-violet-400" />
|
||||
Password
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
|
||||
@ -214,7 +214,7 @@ export function ProfileForm() {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Profile</CardTitle>
|
||||
<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.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@ -227,7 +227,7 @@ export function ProfileForm() {
|
||||
{currentAvatar ? (
|
||||
<AvatarImage src={currentAvatar} alt={fullName || 'Avatar'} />
|
||||
) : null}
|
||||
<AvatarFallback className="bg-emerald-500/10 text-base text-emerald-400">
|
||||
<AvatarFallback className="bg-violet-500/10 text-base text-violet-400">
|
||||
{initial}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
@ -52,13 +52,12 @@ export function SessionsCard() {
|
||||
<Card className="bg-slate-900/40 border-slate-800">
|
||||
<CardHeader>
|
||||
<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
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
Sign out of every device where you're logged in to WaCRM —
|
||||
including this one. Useful if you lost a laptop or shared your
|
||||
password.
|
||||
Sign out of every device where you're logged in — including
|
||||
this one. Useful if you lost a laptop or shared your password.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@ -142,7 +142,7 @@ export function TagManager() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -160,7 +160,7 @@ export function TagManager() {
|
||||
setSelectedColor(PRESET_COLORS[3].value);
|
||||
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" />
|
||||
New Tag
|
||||
@ -281,7 +281,7 @@ export function TagManager() {
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={saving}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
|
||||
@ -40,7 +40,7 @@ const categoryColors: Record<string, string> = {
|
||||
const statusColors: Record<string, string> = {
|
||||
Draft: 'bg-slate-600/20 text-slate-400 border-slate-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',
|
||||
};
|
||||
|
||||
@ -246,7 +246,7 @@ export function TemplateManager() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -280,7 +280,7 @@ export function TemplateManager() {
|
||||
setForm(emptyForm);
|
||||
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" />
|
||||
New Template
|
||||
@ -456,7 +456,7 @@ export function TemplateManager() {
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
|
||||
@ -265,7 +265,7 @@ export function WhatsAppConfig() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -315,7 +315,7 @@ export function WhatsAppConfig() {
|
||||
<Alert className="bg-slate-900 border-slate-700">
|
||||
<div className="flex items-center gap-2">
|
||||
{connectionStatus === 'connected' ? (
|
||||
<CheckCircle2 className="size-4 text-emerald-500" />
|
||||
<CheckCircle2 className="size-4 text-violet-500" />
|
||||
) : (
|
||||
<XCircle className="size-4 text-red-500" />
|
||||
)}
|
||||
@ -444,7 +444,7 @@ export function WhatsAppConfig() {
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
@ -510,13 +510,13 @@ export function WhatsAppConfig() {
|
||||
<AccordionItem className="border-slate-700">
|
||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
||||
<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
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-slate-400">
|
||||
<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 "My Apps" and then "Create App"</li>
|
||||
<li>Select "Business" as the app type</li>
|
||||
<li>Fill in app details and create</li>
|
||||
@ -527,7 +527,7 @@ export function WhatsAppConfig() {
|
||||
<AccordionItem className="border-slate-700">
|
||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
||||
<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
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
@ -543,7 +543,7 @@ export function WhatsAppConfig() {
|
||||
<AccordionItem className="border-slate-700">
|
||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
||||
<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
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
@ -560,7 +560,7 @@ export function WhatsAppConfig() {
|
||||
<AccordionItem className="border-slate-700">
|
||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
||||
<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
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
@ -581,7 +581,7 @@ export function WhatsAppConfig() {
|
||||
href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started"
|
||||
target="_blank"
|
||||
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" />
|
||||
Meta WhatsApp API Documentation
|
||||
|
||||
@ -15,9 +15,9 @@ function Switch({
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"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",
|
||||
"data-[checked]:bg-emerald-500 data-[unchecked]:bg-slate-700",
|
||||
"data-[checked]:bg-violet-500 data-[unchecked]:bg-slate-700",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -21,7 +21,7 @@ export const TRIGGER_META: Record<AutomationTriggerType, TriggerMeta> = {
|
||||
},
|
||||
new_contact_created: {
|
||||
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: {
|
||||
label: 'Conversation Assigned',
|
||||
|
||||
@ -36,7 +36,7 @@ export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
|
||||
},
|
||||
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: {
|
||||
label: "Failed",
|
||||
@ -55,11 +55,11 @@ export const recipientStatusConfig: Record<RecipientStatus, StatusDisplay> = {
|
||||
},
|
||||
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: {
|
||||
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: {
|
||||
label: "Replied",
|
||||
|
||||
@ -22,7 +22,7 @@ const ORDER: Record<string, { order: number; section: string; description: strin
|
||||
'getting-started': {
|
||||
order: 1,
|
||||
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': {
|
||||
order: 2,
|
||||
@ -37,12 +37,12 @@ const ORDER: Record<string, { order: number; section: string; description: strin
|
||||
'environment-variables': {
|
||||
order: 4,
|
||||
section: 'Setup',
|
||||
description: 'Full reference for every env var WaCRM reads.',
|
||||
description: 'Full reference for every env var the app reads.',
|
||||
},
|
||||
'deployment-hostinger': {
|
||||
order: 5,
|
||||
section: 'Deploy',
|
||||
description: 'Ship WaCRM on Hostinger Managed Node.js Hosting.',
|
||||
description: 'Ship the template on Hostinger Managed Node.js Hosting.',
|
||||
},
|
||||
'automations-and-cron': {
|
||||
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',
|
||||
ol: 'my-3 pl-6 list-decimal marker:text-slate-500 text-slate-300 leading-relaxed space-y-1.5',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
|
||||
@ -11,7 +11,7 @@ export interface FaqItem {
|
||||
export const FAQ_ITEMS: FaqItem[] = [
|
||||
{
|
||||
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?',
|
||||
|
||||
@ -9,13 +9,13 @@
|
||||
export const SITE_URL =
|
||||
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'
|
||||
|
||||
/** Default description — reused by layout metadata and structured data. */
|
||||
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
|
||||
@ -40,7 +40,7 @@ export const OG_IMAGE_ALT = `${SITE_NAME} — ${SITE_TAGLINE}`
|
||||
/** Organization info surfaced in JSON-LD. */
|
||||
export const ORG_INFO = {
|
||||
name: SITE_NAME,
|
||||
legalName: 'WaCRM',
|
||||
legalName: 'CRM Template for WhatsApp',
|
||||
url: SITE_URL,
|
||||
logo: `${SITE_URL}/icon`,
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user