Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
.codex-next-dev-*.log

# local browser automation artifacts
.claude-playwright/

# env files (can opt-in for committing if needed)
.env*
Expand Down
3 changes: 2 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"createAccount": "Create account"
},
"Sidebar": {
"title": "CRM Template for WhatsApp",
"title": "Decizyon CRM",
"operationalCommand": "Operational command",
"dashboard": "Dashboard",
"inbox": "Inbox",
"notifications": "Notifications",
Expand Down
3 changes: 2 additions & 1 deletion messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"createAccount": "계정 만들기"
},
"Sidebar": {
"title": "WhatsApp용 CRM 템플릿",
"title": "Decizyon CRM",
"operationalCommand": "운영 커맨드",
"dashboard": "대시보드",
"inbox": "인박스",
"notifications": "알림",
Expand Down
Binary file added public/brand/decizyon-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/brand/decizyon-tech-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 11 additions & 4 deletions src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
Expand All @@ -13,7 +14,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { MessageSquare, CheckCircle, ArrowLeft } from "lucide-react";
import { CheckCircle, ArrowLeft } from "lucide-react";

export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
Expand Down Expand Up @@ -77,10 +78,16 @@ export default function ForgotPasswordPage() {
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<MessageSquare className="h-6 w-6 text-primary" />
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-card shadow-sm">
<Image
src="/brand/decizyon-icon.png"
alt="Decizyon"
width={34}
height={34}
priority
/>
</div>
<CardTitle className="text-xl text-foreground">Reset password</CardTitle>
<CardTitle className="font-heading text-xl text-foreground">Reset password</CardTitle>
<CardDescription className="text-muted-foreground">
Enter your email and we&apos;ll send you a reset link
</CardDescription>
Expand Down
15 changes: 11 additions & 4 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Suspense, useState } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { createClient } from "@/lib/supabase/client";
Expand All @@ -15,7 +16,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { MessageSquare, UsersRound } from "lucide-react";
import { UsersRound } from "lucide-react";

// `useSearchParams` opts the component out of static prerendering
// unless it sits under a Suspense boundary. We split the form into
Expand Down Expand Up @@ -78,14 +79,20 @@ function LoginPageInner() {
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-card shadow-sm">
{inviteToken ? (
<UsersRound className="h-6 w-6 text-primary" />
) : (
<MessageSquare className="h-6 w-6 text-primary" />
<Image
src="/brand/decizyon-icon.png"
alt="Decizyon"
width={34}
height={34}
priority
/>
)}
</div>
<CardTitle className="text-xl text-foreground">
<CardTitle className="font-heading text-xl text-foreground">
{inviteToken ? t('titleAccept') : t('titleWelcome')}
</CardTitle>
<CardDescription className="text-muted-foreground">
Expand Down
15 changes: 11 additions & 4 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { Suspense, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
Expand All @@ -14,7 +15,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { MessageSquare, CheckCircle, UsersRound } from "lucide-react";
import { CheckCircle, UsersRound } from "lucide-react";

// `useSearchParams` opts the component out of static prerendering
// unless wrapped in Suspense — same pattern as /login.
Expand Down Expand Up @@ -131,14 +132,20 @@ function SignupPageInner() {
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-card shadow-sm">
{inviteToken ? (
<UsersRound className="h-6 w-6 text-primary" />
) : (
<MessageSquare className="h-6 w-6 text-primary" />
<Image
src="/brand/decizyon-icon.png"
alt="Decizyon"
width={34}
height={34}
priority
/>
)}
</div>
<CardTitle className="text-xl text-foreground">
<CardTitle className="font-heading text-xl text-foreground">
{inviteToken ? "Create account & join" : "Create account"}
</CardTitle>
<CardDescription className="text-muted-foreground">
Expand Down
31 changes: 22 additions & 9 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@

:root,
html[data-mode="dark"] {
--background: oklch(0.13 0.01 260);
--foreground: oklch(0.985 0 0);
--card: oklch(0.18 0.01 260);
--card-2: oklch(0.205 0.01 260);
--background: #061b22;
--foreground: #f7fafa;
--card: #082a32;
--card-2: #0c3640;
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.18 0.01 260);
--popover-foreground: oklch(0.985 0 0);
Expand All @@ -117,10 +117,10 @@ html[data-mode="dark"] {
}

html[data-mode="light"] {
--background: oklch(0.99 0.002 260);
--foreground: oklch(0.21 0.01 260);
--card: oklch(1 0 0);
--card-2: oklch(0.985 0.002 260);
--background: #f7fafa;
--foreground: #08202e;
--card: #ffffff;
--card-2: #eef5f5;
--card-foreground: oklch(0.21 0.01 260);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.21 0.01 260);
Expand Down Expand Up @@ -148,6 +148,19 @@ html[data-mode="light"] {
/* ---- ACCENT: primary color ---- */

:root,
html[data-theme="decizyon"] {
--primary: #073b45;
--primary-foreground: #ffffff;
--primary-hover: #0b4f59;
--primary-soft: rgb(7 59 69 / 0.1);
--primary-soft-2: rgb(21 159 153 / 0.16);
--ring: #159f99;
--chart-1: #073b45;
--sidebar-primary: #073b45;
--sidebar-primary-foreground: #ffffff;
--sidebar-ring: #159f99;
}

html[data-theme="violet"] {
--primary: oklch(0.526 0.247 293);
--primary-foreground: oklch(0.985 0 0);
Expand Down Expand Up @@ -227,4 +240,4 @@ html[data-theme="rose"] {
html {
@apply font-sans;
}
}
}
8 changes: 4 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ const inter = Inter({

export const metadata: Metadata = {
title: {
default: "wacrm",
template: "%s — wacrm",
default: "Decizyon CRM",
template: "%s | Decizyon CRM",
},
description: "Self-hostable CRM template for WhatsApp.",
description: "CRM operacional para WhatsApp com identidade Decizyon.",
robots: {
index: false,
follow: false,
},
icons: {
icon: [{ url: "/icon" }],
icon: [{ url: "/brand/decizyon-icon.png" }],
},
formatDetection: {
email: false,
Expand Down
11 changes: 8 additions & 3 deletions src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,14 @@ export function Header({ onOpenSidebar }: HeaderProps) {
>
<Menu className="h-5 w-5" />
</button>
<h1 className="truncate text-base font-semibold text-foreground sm:text-lg">
{t(titleKey as string)}
</h1>
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase text-muted-foreground sm:text-[12px]">
Decizyon CRM
</p>
<h1 className="truncate text-lg font-heading font-semibold text-foreground sm:text-xl">
{t(titleKey as string)}
</h1>
</div>
</div>

<div className="flex items-center gap-1 sm:gap-2">
Expand Down
22 changes: 17 additions & 5 deletions src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -188,12 +189,23 @@ 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-border 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-primary text-primary-foreground">
<MessageSquare className="h-4 w-4" />
<div className="flex h-10 w-10 items-center justify-center rounded-lg border border-primary/15 bg-primary-soft text-primary">
<Image
src="/brand/decizyon-icon.png"
alt=""
width={28}
height={28}
priority
/>
</div>
<div className="min-w-0">
<p className="truncate font-heading text-sm font-semibold text-foreground">
Decizyon CRM
</p>
<p className="text-xs text-muted-foreground">
{t("operationalCommand")}
</p>
</div>
<span className="text-sm font-semibold text-foreground">
{t("title")}
</span>
</Link>
<button
type="button"
Expand Down
8 changes: 5 additions & 3 deletions src/lib/currency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
formatCurrencyShort,
} from "./currency";

const GROUPED_1234 = /1[,.]234/;

describe("formatCurrency", () => {
it("formats whole amounts with no minor units", () => {
// Use a non-breaking-space-tolerant check: Intl may insert NBSP.
const out = formatCurrency(1234, "USD");
expect(out).toContain("1,234");
expect(out).toMatch(GROUPED_1234);
expect(out).not.toContain(".00");
});

Expand All @@ -30,13 +32,13 @@ describe("formatCurrency", () => {
// Intl is lenient here — it uses the code as the symbol.
const out = formatCurrency(1234, "ZZZ");
expect(out).toContain("ZZZ");
expect(out).toContain("1,234");
expect(out).toMatch(GROUPED_1234);
});

it("never throws on a structurally invalid code (no DB CHECK on deals.currency)", () => {
for (const bad of ["United States", "US", "USDD", "12", "u$d"]) {
expect(() => formatCurrency(1234, bad)).not.toThrow();
expect(formatCurrency(1234, bad)).toContain("1,234");
expect(formatCurrency(1234, bad)).toMatch(GROUPED_1234);
}
});

Expand Down
12 changes: 6 additions & 6 deletions src/lib/dashboard/date-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,17 @@ describe("lastNDayKeys", () => {

describe("mondayIndex", () => {
it("maps Monday → 0 and Sunday → 6", () => {
expect(mondayIndex(new Date("2026-05-18"))).toBe(0); // Mon
expect(mondayIndex(new Date("2026-05-19"))).toBe(1); // Tue
expect(mondayIndex(new Date("2026-05-23"))).toBe(5); // Sat
expect(mondayIndex(new Date("2026-05-24"))).toBe(6); // Sun
expect(mondayIndex(new Date(2026, 4, 18))).toBe(0); // Mon
expect(mondayIndex(new Date(2026, 4, 19))).toBe(1); // Tue
expect(mondayIndex(new Date(2026, 4, 23))).toBe(5); // Sat
expect(mondayIndex(new Date(2026, 4, 24))).toBe(6); // Sun
});

it("aligns with DOW_SHORT_MON_FIRST labels", () => {
expect(DOW_SHORT_MON_FIRST[mondayIndex(new Date("2026-05-18"))]).toBe(
expect(DOW_SHORT_MON_FIRST[mondayIndex(new Date(2026, 4, 18))]).toBe(
"Mon",
);
expect(DOW_SHORT_MON_FIRST[mondayIndex(new Date("2026-05-24"))]).toBe(
expect(DOW_SHORT_MON_FIRST[mondayIndex(new Date(2026, 4, 24))]).toBe(
"Sun",
);
});
Expand Down
11 changes: 9 additions & 2 deletions src/lib/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

export const THEME_IDS = [
"decizyon",
"violet",
"emerald",
"cobalt",
Expand All @@ -23,9 +24,9 @@ export const THEME_IDS = [

export type ThemeId = (typeof THEME_IDS)[number];

export const DEFAULT_THEME: ThemeId = "violet";
export const DEFAULT_THEME: ThemeId = "decizyon";

export const STORAGE_KEY = "wacrm.theme";
export const STORAGE_KEY = "decizyon.theme";

/**
* MODE — the light/dark dimension, orthogonal to the accent theme.
Expand Down Expand Up @@ -67,6 +68,12 @@ export interface ThemeMeta {
}

export const THEMES: ReadonlyArray<ThemeMeta> = [
{
id: "decizyon",
name: "Decizyon",
tagline: "Operational CRM identity with deep teal and aqua accents.",
swatch: "#073b45",
},
{
id: "violet",
name: "Violet",
Expand Down