Add 'apps/web/' from commit '1e7174fa604b11e7c3983cd8ad01c596f6e77e96'

git-subtree-dir: apps/web
git-subtree-mainline: 068a51b46b
git-subtree-split: 1e7174fa60
This commit is contained in:
2026-08-16 21:42:59 -04:00
350 changed files with 62192 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
# beenvoice-web architecture
Dense reference for the Next.js web application and API. Package manager: **Bun**. Database: **PostgreSQL** via Drizzle ORM.
**Repository:** [git.soconnor.dev/soconnor/beenvoice-web](https://git.soconnor.dev/soconnor/beenvoice-web)
## Stack
| Layer | Technology |
|-------|------------|
| Framework | Next.js 16 App Router (`src/app/`) |
| API | tRPC 11 (`/api/trpc`), SuperJSON transformer |
| ORM | Drizzle + `pg` pool |
| Auth | better-auth (email/password, optional Authentik OIDC, Expo plugin for mobile) |
| UI | shadcn/ui, Tailwind CSS v4, Radix primitives |
| Email | Resend |
| PDF | `@react-pdf/renderer` |
## Request flow
```
Browser / Mobile / MCP client
├─► /api/auth/* → better-auth handler (session cookies)
├─► /api/trpc/* → createContext() → appRouter
│ ├─ Bearer / x-api-key → api-key auth
│ └─ else → better-auth session
├─► /api/mcp → API key only → JSON-RPC tools → tRPC caller
├─► /api/i/[token]/pdf → public invoice PDF
└─► /dashboard/* → RSC + client components (session required in UI)
```
**Context** (`src/server/api/trpc.ts`): `protectedProcedure` requires `ctx.session.user`. API-key auth sets `authSource: "api-key"`; `apiKeys.*` mutations require session (cannot manage keys with a key).
## Directory layout
```
src/
├── app/ # Routes (pages + route handlers)
│ ├── api/
│ │ ├── auth/ # better-auth catch-all + custom register/reset REST
│ │ ├── trpc/[trpc]/ # tRPC HTTP adapter
│ │ ├── mcp/ # MCP over HTTP (API key)
│ │ ├── i/[token]/pdf/ # Public PDF
│ │ └── cron/ # Recurring invoice generation (CRON_SECRET)
│ ├── auth/ # sign-in, register, forgot/reset password
│ ├── dashboard/ # Authenticated app shell
│ └── i/[token]/ # Public invoice view
├── components/ # Shared UI (ui/, forms/, layout/, data/)
├── hooks/
├── lib/ # auth.ts, pdf-export, email templates, branding
├── server/
│ ├── api/
│ │ ├── root.ts # appRouter composition
│ │ ├── trpc.ts # procedures, context, timing middleware (dev)
│ │ ├── api-keys.ts
│ │ └── routers/ # one file per domain
│ └── db/
│ ├── schema.ts # all tables (prefix beenvoice_)
│ ├── index.ts # drizzle + pool
│ └── migrate.ts
├── trpc/ # react.tsx (client), server.ts (RSC)
├── env.js # @t3-oss/env-nextjs validation
└── styles/globals.css
drizzle/ # SQL migrations (00000014+)
```
## tRPC routers
Root: `src/server/api/root.ts`. All routers use Zod input validation.
| Namespace | File | Key procedures |
|-----------|------|----------------|
| `clients` | `routers/clients.ts` | getAll, getById, create, update, delete |
| `businesses` | `routers/businesses.ts` | getAll, getById, getDefault, create, update, delete, setDefault, getEmailConfig, updateEmailConfig |
| `invoices` | `routers/invoices.ts` | getAll, getBillable, getById, create, update, delete, updateStatus, bulk*, previewPdf, public token, **getByPublicToken** (public), sendReminder |
| `payments` | `routers/payments.ts` | getByInvoice, create, delete |
| `expenses` | `routers/expenses.ts` | getAll, getById, create, update, delete |
| `invoiceTemplates` | `routers/invoiceTemplates.ts` | CRUD by template type |
| `recurringInvoices` | `routers/recurring-invoices.ts` | CRUD, pause/resume, generateNow; cron helper `generateDueRecurringInvoices` |
| `timeEntries` | `routers/time-entries.ts` | getAll, getRunning, clockIn, updateRunning, clockOut, create, update, delete, getSummary |
| `dashboard` | `routers/dashboard.ts` | getStats |
| `email` | `routers/email.ts` | sendInvoice |
| `settings` | `routers/settings.ts` | profile, theme, animation prefs, export/import data, admin account roles |
| `apiKeys` | `routers/apiKeys.ts` | list, create, revoke (session-only) |
### Time clock semantics
- **One running entry per user** — partial unique index on `(createdById)` where `endedAt IS NULL`.
- `clockIn` — optional client, invoice, rate, backdated `startedAt`; resolves rate from input → client default → business default.
- `clockOut` — optional description update; computes hours; if `invoiceId` set, appends line item; else tries latest open invoice for client.
- Outcomes: `linked_to_invoice`, `saved_no_invoice`, `saved_no_client`, `zero_hours`.
## Database schema
Single file: `src/server/db/schema.ts`. Table names use `pgTableCreator` → prefix `beenvoice_`.
### Auth & platform
| Table | Notes |
|-------|-------|
| `beenvoice_user` | Core user; role for admin features |
| `beenvoice_account` | OAuth/credential accounts (better-auth) |
| `beenvoice_session` | Sessions; unique token |
| `beenvoice_verification_token` | Email verification / reset |
| `beenvoice_api_key` | `bv_` prefix keys; SHA-256 hash stored |
| `beenvoice_sso_provider` | OIDC/SAML config per user |
| `beenvoice_platform_setting` | Singleton (`id = global`) branding/PDF/appearance |
### Domain
| Table | FKs | Notes |
|-------|-----|-------|
| `beenvoice_client` | `createdById` → user | defaultHourlyRate, currency |
| `beenvoice_business` | `createdById` | Resend config, `isDefault` |
| `beenvoice_invoice` | client, business?, user | status draft/sent/paid; `publicToken` |
| `beenvoice_invoice_item` | invoice (cascade) | position ordering |
| `beenvoice_invoice_payment` | invoice, user | payment method enum |
| `beenvoice_expense` | business?, client?, invoice? | billable flags |
| `beenvoice_invoice_template` | user | notes/terms templates |
| `beenvoice_recurring_invoice` | client, business?, user | schedule, `nextDueAt` |
| `beenvoice_recurring_invoice_item` | recurring (cascade) | |
| `beenvoice_time_entry` | client?, invoice?, user | `endedAt` null = running |
Migrations: `bun run db:generate``drizzle/`; apply with `db:push` (dev) or `db:migrate` (prod script).
## Authentication
**Server**`src/lib/auth.ts`:
- `betterAuth` + `drizzleAdapter` (users, sessions, accounts, verification)
- Plugins: `@better-auth/expo` (mobile SecureStore cookies), `nextCookies()`, optional `genericOAuth` (Authentik)
- Email/password with bcrypt (12 rounds); `DISABLE_SIGNUPS=true` blocks registration (custom `/api/auth/register` and better-auth `disableSignUp`)
- `trustedOrigins`: `BETTER_AUTH_URL`, `NEXT_PUBLIC_APP_URL`, `beenvoice://`, `exp://`, plus Authentik origin when configured
**Web client**`src/lib/auth-client.ts`: `createAuthClient` + `genericOAuthClient`.
**Routes**:
- `src/app/api/auth/[...all]/route.ts` — better-auth handler
- Custom REST: `register`, `forgot-password`, `reset-password`, `validate-reset-token` (used by mobile and legacy flows)
**Session cookies**: `better-auth.session_token` or `__Secure-better-auth.session_token` in production.
## Mobile API contract
The Expo app (`beenvoice-app`) does **not** use API keys. It:
1. Calls the same tRPC endpoints with `Authorization` cookie header from `authClient.getCookie()`.
2. Stores session per account in SecureStore via `@better-auth/expo` (`storagePrefix`: `beenvoice:guest` or `beenvoice:auth:{accountId}`).
3. Requires `trustedOrigins` and matching `BETTER_AUTH_URL` for the host the device can reach.
Ensure `src/lib/auth.ts` keeps the `expo()` plugin enabled.
## MCP (machine clients)
`POST /api/mcp` — JSON-RPC 2.0, protocol `2025-11-25`.
- **Auth**: API key only (`Authorization: Bearer bv_…` or `x-api-key`). Session cookies rejected.
- **Tools**: ~50 tools mirroring tRPC (invoices, clients, time clock, expenses, etc.)
- Implemented in `src/app/api/mcp/route.ts`; delegates to `createCaller(createContext)`.
API keys: format `bv_<base64url>`; stored as SHA-256 hash (`src/server/api/api-keys.ts`).
## Environment variables
Validated in `src/env.js`. See `.env.example`.
| Variable | Required | Notes |
|----------|----------|-------|
| `DATABASE_URL` | yes | PostgreSQL connection string |
| `AUTH_SECRET` | prod | `openssl rand -base64 32` |
| `BETTER_AUTH_URL` | yes | Public URL of API (no trailing path) |
| `NEXT_PUBLIC_APP_URL` | yes | Browser-facing URL |
| `DB_DISABLE_SSL` | local | `true` for Docker dev DB |
| `RESEND_API_KEY`, `RESEND_DOMAIN` | optional | Email; blank disables send |
| `AUTHENTIK_*` | optional | OIDC SSO |
| `DISABLE_SIGNUPS` | optional | `true` blocks registration; use string `true`/`false` (parsed in `src/env.js`) |
| `CRON_SECRET` | cron route | Protects `/api/cron/generate-recurring` |
| `NEXT_PUBLIC_BRAND_*` | optional | Build-time white-label defaults |
## Docker
| File | Use |
|------|-----|
| `docker-compose.yml` | Deploy: `app` + `db` (Postgres internal); copy `.env.example``.env` |
| `docker-compose.dev.yml` | Local dev: Postgres only, port `${POSTGRES_PORT:-5432}` |
App image built from `Dockerfile`. Container `CMD`: `bun migrate.ts && bun run start` (migrations then `next start` on port 3000). Docker builds run `next build` on Node 22 (not Bun) to avoid arm64 worker crashes; runtime stays on Bun. Docker builds disable React Compiler and use `experimental.webpackMemoryOptimizations` to reduce peak RAM.
Set `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` to the public hostname before deploy. Rebuild the image when changing `NEXT_PUBLIC_*` build-time vars.
**Deploy / update:** `git pull && ./scripts/docker-deploy.sh` (or `docker compose up -d --build`). Plain `docker compose up -d` reuses the local `beenvoice:local` image and does not include pulled code. The deploy script tags images as `beenvoice:<git-sha>`.
## Scripts
```bash
bun run dev # next dev --turbo
bun run build # production build
bun run db:push # push schema (dev)
bun run db:migrate # run migrations
bun run db:studio # Drizzle Studio
bun run check # eslint + tsc
```
## Public / unauthenticated surfaces
- `invoices.getByPublicToken` (tRPC publicProcedure)
- `/i/[token]` page and `/api/i/[token]/pdf`
- Auth REST endpoints for register/reset
## Related docs
- [forms-guide.md](./forms-guide.md), [UI_UNIFORMITY_GUIDE.md](./UI_UNIFORMITY_GUIDE.md)
- [data-table-responsive-guide.md](./data-table-responsive-guide.md)
- [email-features.md](./email-features.md)
- Mobile companion: `../beenvoice-app/docs/ARCHITECTURE.md`
+137
View File
@@ -0,0 +1,137 @@
# Coolify deployment — beenvoice + Garage
beenvoice stores receipt files in S3-compatible storage when `S3_BUCKET`, `S3_ACCESS_KEY`, and `S3_SECRET_KEY` are set. [Garage](https://garagehq.deuxfleurs.fr/) is the default on self-hosted Coolify (~50100 MB RAM vs MinIO's ~500 MB+).
## Why `getaddrinfo ENOTFOUND garage` happens
Docker DNS resolves service names **only inside the same Docker network**.
| Setup | Does `http://garage:3900` work? |
|-------|--------------------------------|
| Single Compose stack (app + garage together) | Yes — Compose service name `garage` |
| beenvoice **Application** + Garage **separate Compose** | **No** — each resource has its own network by default |
| Application + Garage with shared destination network + correct hostname | Yes — hostname is usually **`garage-<resource-uuid>`**, not bare `garage` |
| Application + Garage via **public domain** (`SERVICE_URL_GARAGE_3900`) | Yes — no Docker DNS needed |
Setting `S3_ENDPOINT=http://garage:3900` on a standalone beenvoice Application fails because the app container is not on the Garage stack's network. Node returns `ENOTFOUND garage`.
Also avoid `http://localhost:3900` inside the app container — that points at the app itself, not Garage.
---
## Quick fix — keep beenvoice as Application + separate Garage compose
Use this if you are **not** migrating to a single Compose stack today.
### Path A — public Garage URL (recommended, works without shared Docker network)
This is the most reliable fix when beenvoice is a Coolify **Application** (Dockerfile) and Garage is a separate Compose resource.
1. **Update the Garage stack** to the latest `docker-compose.coolify-garage.yml` from this repo (includes `SERVICE_FQDN_GARAGE_3900`) and **redeploy** the Garage resource.
2. In the **Garage Compose resource** → assign a domain for **port 3900** (e.g. `s3.yourdomain.com`). Coolify generates TLS via Traefik/Caddy.
3. Open the Garage resource **Environment** tab and copy **`SERVICE_URL_GARAGE_3900`** (e.g. `https://s3.yourdomain.com`).
4. On the **beenvoice Application** → Environment:
```env
S3_ENDPOINT=https://s3.yourdomain.com
S3_BUCKET=beenvoice-receipts
S3_ACCESS_KEY=<same as GARAGE_DEFAULT_ACCESS_KEY / S3_ACCESS_KEY on Garage stack>
S3_SECRET_KEY=<same as GARAGE_DEFAULT_SECRET_KEY / S3_SECRET_KEY on Garage stack>
S3_REGION=garage
```
5. **Redeploy beenvoice** (restart is not enough after env changes on some Coolify versions — trigger a full redeploy).
`S3_FORCE_PATH_STYLE` defaults to on when `S3_ENDPOINT` is set (required for Garage behind a reverse proxy). Only set `S3_FORCE_PATH_STYLE=false` if you use AWS S3 with virtual-hosted-style buckets.
### Path B — internal Docker DNS (same destination, no public Garage domain)
Use when you want S3 API traffic to stay on the Docker network.
1. Put beenvoice Application and Garage Compose in the **same Coolify project** and **same destination** (server/network).
2. **Garage Compose resource****Advanced** → enable **Connect to Predefined Network****redeploy Garage**.
3. **beenvoice Application****Advanced** → enable **Connect to Predefined Network** (same destination) → **redeploy beenvoice**.
4. Find the Garage resource **UUID** (in the Coolify URL, e.g. `.../service/abc123def456`, or env `COOLIFY_RESOURCE_UUID` on the Garage container).
5. Set on beenvoice Application:
```env
S3_ENDPOINT=http://garage-<GARAGE_RESOURCE_UUID>:3900
```
Example: resource UUID `k8w2o0g4s0g8``S3_ENDPOINT=http://garage-k8w2o0g4s0g8:3900`.
**Do not use bare `garage`** unless you verified it resolves from inside the beenvoice container (recent Coolify versions may also register the short service name when both sides use Connect to Predefined Network — if `wget http://garage:3900` fails, use the `garage-<uuid>` form or Path A).
6. Match credentials and bucket:
```env
S3_BUCKET=beenvoice-receipts
S3_ACCESS_KEY=<S3_ACCESS_KEY on Garage stack>
S3_SECRET_KEY=<S3_SECRET_KEY on Garage stack>
S3_REGION=garage
```
---
## Recommended long-term — one Compose stack
Deploy **[`docker-compose.coolify.yml`](../docker-compose.coolify.yml)** as **one** Coolify **Docker Compose** resource (app + Postgres + Garage). This is the lowest-friction production layout on Coolify.
1. Coolify → **New Resource****Docker Compose**
2. Point at this repo; compose file: **`docker-compose.coolify.yml`**
3. Set env vars from [`.env.example`](../.env.example): `AUTH_SECRET`, `POSTGRES_PASSWORD`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, etc.
4. Assign a domain to the **`app`** service (Coolify fills `SERVICE_URL_APP` / `BETTER_AUTH_URL` automatically).
5. **Do not** override `S3_ENDPOINT` — the compose file sets `S3_ENDPOINT=http://garage:3900` on the shared network.
6. Redeploy.
Alternative: [`docker-compose.yml`](../docker-compose.yml) works the same way; `docker-compose.coolify.yml` adds Coolify magic vars (`SERVICE_FQDN_APP`) and omits host port bindings for db/Garage.
### Migrating from Application + external Postgres + Garage (or legacy MinIO)
| Current | Action |
|---------|--------|
| beenvoice Application | Remove after Compose stack is live |
| Separate Postgres | Dump/restore into stack `db`, or keep external DB and delete the `db` service from the compose file |
| Garage / MinIO compose | Remove after data migrated (rclone) or re-point receipts (new bucket) |
| Env vars | Move `AUTH_SECRET`, Resend, Authentik, etc. to the Compose resource env |
**Migrating from MinIO:** Garage uses port **3900** (not 9000) and Garage-format access keys (`GK…`). Update `S3_ENDPOINT`, `S3_REGION=garage`, and credentials. Receipt blobs in the old MinIO volume are not auto-migrated.
---
## Compose file reference
| File | Purpose |
|------|---------|
| [`docker-compose.coolify.yml`](../docker-compose.coolify.yml) | **Recommended** — full stack for one Coolify Compose resource |
| [`docker-compose.yml`](../docker-compose.yml) | Full stack (local/VPS); also valid on Coolify |
| [`docker-compose.coolify-garage.yml`](../docker-compose.coolify-garage.yml) | Garage only; pair with beenvoice Application (Path A or B above) |
Do **not** add `networks: coolify: external: true` unless you know the exact external network name on your server. Coolify v4 uses **destinations**; network names are often UUID-based. Prefer the UI **Connect to Predefined Network** toggle over hard-coding `coolify` in compose.
---
## Checklist (Application + separate Garage)
- [ ] Garage stack redeployed with current `docker-compose.coolify-garage.yml`
- [ ] **Path A:** domain on port 3900 + `S3_ENDPOINT` = `SERVICE_URL_GARAGE_3900`
**or Path B:** Connect to Predefined Network on **both** resources + `S3_ENDPOINT=http://garage-<uuid>:3900`
- [ ] `S3_ENDPOINT` is **not** `http://garage:3900`, **not** `localhost`
- [ ] `S3_ACCESS_KEY` / `S3_SECRET_KEY` match the Garage stack env
- [ ] `S3_BUCKET` exists (Garage `--default-bucket` creates `beenvoice-receipts` on first start)
- [ ] Redeployed beenvoice after env or network changes
## Verify from the beenvoice container
```bash
# Shell into beenvoice app container on the Coolify server
docker exec -it <beenvoice-container> sh
# Path A — public URL (403/404 on root is fine — confirms DNS + TLS)
wget -qO- "https://s3.yourdomain.com" || curl -sf "https://s3.yourdomain.com"
# Path B — internal host from S3_ENDPOINT
wget -qO- "http://garage-<uuid>:3900" || curl -sf "http://garage-<uuid>:3900"
```
If this fails with "bad address" or timeout, fix networking / `S3_ENDPOINT` before debugging app code. On first S3 use, the app logs a hint if DNS fails or if `S3_ENDPOINT` still uses bare `garage` in production.
+36
View File
@@ -0,0 +1,36 @@
# beenvoice-web documentation
**Repository:** [git.soconnor.dev/soconnor/beenvoice-web](https://git.soconnor.dev/soconnor/beenvoice-web)
## Core
| Document | Description |
|----------|-------------|
| [ARCHITECTURE.md](./ARCHITECTURE.md) | Server stack, tRPC routers, schema, auth, MCP, Docker, mobile API contract |
| [../README.md](../README.md) | Install, scripts, deployment |
| [COOLIFY.md](./COOLIFY.md) | Coolify + Garage networking (`ENOTFOUND garage`) |
## UI & product guides
| Document | Description |
|----------|-------------|
| [forms-guide.md](./forms-guide.md) | Form patterns |
| [UI_UNIFORMITY_GUIDE.md](./UI_UNIFORMITY_GUIDE.md) | Visual consistency |
| [breadcrumbs-guide.md](./breadcrumbs-guide.md) | Navigation breadcrumbs |
| [data-table-responsive-guide.md](./data-table-responsive-guide.md) | Responsive tables |
| [data-table-improvements.md](./data-table-improvements.md) | Table enhancements |
| [RESPONSIVE_TABLE_EXAMPLES.md](./RESPONSIVE_TABLE_EXAMPLES.md) | Table examples |
| [email-features.md](./email-features.md) | Email composer / delivery |
## Mobile
| Document | Description |
|----------|-------------|
| [../../beenvoice-app/docs/ARCHITECTURE.md](../../beenvoice-app/docs/ARCHITECTURE.md) | Expo app architecture |
| [../../beenvoice-app/README.md](../../beenvoice-app/README.md) | Mobile setup |
## Workspace
| Document | Description |
|----------|-------------|
| [../../README.md](../../README.md) | Meta repo layout, full-stack quick start |
+138
View File
@@ -0,0 +1,138 @@
# Responsive Table Examples
This document shows how tables adapt across different screen sizes in the beenvoice application.
## Mobile View (< 640px)
### Invoices Table
- **Visible**: Invoice number, client name, amount, status, actions
- **Hidden**: Issue date, due date (shown on detail view)
- **Features**: Compact spacing, smaller buttons, simplified pagination
### Clients Table
- **Visible**: Name with email, actions
- **Hidden**: Phone, address, created date
- **Icon**: Hidden on mobile to save space
### Businesses Table
- **Visible**: Name with email, actions
- **Hidden**: Phone, address, tax ID, website
- **Icon**: Hidden on mobile to save space
## Tablet View (640px - 1024px)
### Invoices Table
- **Added**: Issue date column
- **Still Hidden**: Due date (less critical than issue date)
- **Features**: Search bar expands, column visibility toggle appears
### Clients Table
- **Added**: Phone column, client icon
- **Still Hidden**: Address, created date
- **Features**: Better spacing, full search functionality
### Businesses Table
- **Added**: Phone column, business icon
- **Still Hidden**: Address, tax ID
- **Features**: Website links become visible
## Desktop View (> 1024px)
### All Tables
- **Full Features**: All columns visible
- **Enhanced**:
- Full pagination controls with page size selector
- Column visibility toggle
- Advanced filters
- Comfortable spacing
- All metadata visible
## Code Examples
### Responsive Column Definition
```tsx
// Hide on mobile, show on tablet and up
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => (
<span className="hidden md:inline">{row.original.phone || "—"}</span>
),
}
// Hide on mobile and tablet, show on desktop
{
id: "address",
header: "Address",
cell: ({ row }) => (
<span className="hidden lg:inline">{formatAddress(row.original)}</span>
),
}
```
### Responsive Cell Content
```tsx
// Icon hidden on mobile
<div className="flex items-center gap-3">
<div className="hidden rounded-lg bg-status-info-muted p-2 sm:flex">
<UserPlus className="h-4 w-4 text-status-info" />
</div>
<div className="min-w-0">
<p className="truncate font-medium">{client.name}</p>
<p className="truncate text-sm text-muted-foreground">
{client.email || "—"}
</p>
</div>
</div>
```
### Responsive Actions
```tsx
// Compact action buttons that work on all screen sizes
<div className="flex items-center justify-end gap-1">
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
```
## Filter Bar Behavior
### Mobile
- Search input takes full width
- Filter dropdowns stack vertically
- Column visibility hidden
- Clear filters button visible when filters active
### Tablet+
- Search input limited to max-width
- Filter dropdowns in horizontal row
- Column visibility toggle appears
- All controls in single row
## Pagination Behavior
### Mobile
- Simplified page indicator (1/5 format)
- Compact button spacing
- Page size selector with smaller text
### Desktop
- Full "Page 1 of 5" text
- Comfortable button spacing
- First/Last page buttons visible
- Entries count with detailed information
## Best Practices
1. **Priority Content**: Always show the most important data on mobile
2. **Progressive Enhancement**: Add columns as screen size increases
3. **Touch Targets**: Maintain 44px minimum touch targets on mobile
4. **Text Truncation**: Use `truncate` class for long text in narrow columns
5. **Icon Usage**: Hide decorative icons on mobile, keep functional ones
6. **Testing**: Always test at 375px (iPhone SE), 768px (iPad), and 1440px (Desktop)
+324
View File
@@ -0,0 +1,324 @@
# UI Uniformity Guide for beenvoice
## Overview
This guide documents the unified component system implemented across the beenvoice application to ensure consistent UI/UX patterns. The system follows a hierarchical approach where:
1. **CSS Variables** (in `globals.css`) define the design tokens
2. **UI Components** (in `components/ui`) consume these variables
3. **Pages** use components with minimal additional styling
## Design System Principles
### 1. Variable-Based Theming
All colors, spacing, and other design tokens are defined as CSS variables in `globals.css`:
- Brand colors: `--brand-primary`, `--brand-secondary`
- Status colors: `--status-success`, `--status-warning`, `--status-error`, `--status-info`
- Semantic colors: `--background`, `--foreground`, `--muted`, etc.
### 2. Component Composition
Complex UI patterns are built from smaller, reusable components rather than duplicating code.
### 3. Minimal Page-Level Styling
Pages should primarily compose pre-built components and avoid custom Tailwind classes where possible.
## Core Unified Components
### Page Layout Components
#### `PageContent`
Wraps page content with consistent spacing:
```tsx
<PageContent spacing="default">
{/* Page sections */}
</PageContent>
```
#### `PageSection`
Groups related content with optional title and actions:
```tsx
<PageSection
title="Section Title"
description="Optional description"
actions={<Button>Action</Button>}
>
{/* Section content */}
</PageSection>
```
#### `PageGrid`
Responsive grid layout with preset column options:
```tsx
<PageGrid columns={3} gap="default">
{/* Grid items */}
</PageGrid>
```
### Data Display Components
#### `DataTable`
Unified table component using @tanstack/react-table with floating card design:
```tsx
import { ColumnDef } from "@tanstack/react-table";
import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table";
import { PageSection } from "~/components/ui/page-layout";
const columns: ColumnDef<DataType>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => {
const name = row.getValue("name") as string;
return <div className="font-medium">{name}</div>;
}
},
{
id: "actions",
cell: ({ row }) => {
const item = row.original;
return (
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Edit className="h-3.5 w-3.5" />
</Button>
);
}
}
];
const filterableColumns = [
{
id: "status",
title: "Status",
options: [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" }
]
}
];
// Wrap in PageSection for title/description
<PageSection
title="Table Title"
description="Optional description"
>
<DataTable
columns={columns}
data={data}
searchPlaceholder="Search by name..."
filterableColumns={filterableColumns}
/>
</PageSection>
```
Features:
- **Floating Card Design**: Three separate cards for filter bar, table content, and pagination
- **Filter Bar Card**: Minimal padding (p-3) with global search and column filters
- **Table Content Card**: Clean borders with overflow handling
- **Pagination Card**: Compact controls with page size selector
- **Responsive Design**: Mobile-optimized with hidden columns on smaller screens
- **Tight Appearance**: Compact spacing with smaller action buttons
- **Sorting**: Visual indicators with proper arrow directions
- **Column Visibility**: Toggle columns (hidden on mobile)
- **Dark Mode**: Consistent styling across light/dark themes
- **Loading States**: DataTableSkeleton component with matching card structure
#### `StatsCard`
Displays statistics with consistent styling:
```tsx
<StatsCard
title="Total Revenue"
value="$10,000"
icon={DollarSign}
description="From 50 invoices"
variant="success"
/>
```
#### `QuickActionCard`
Interactive cards for navigation or actions:
```tsx
<QuickActionCard
title="Create Invoice"
description="Start a new invoice"
icon={Plus}
variant="success"
>
<Link href="/invoices/new">
<div className="h-full w-full" />
</Link>
</QuickActionCard>
```
### Feedback Components
#### `EmptyState`
Consistent empty state displays:
```tsx
<EmptyState
icon={<FileText className="h-8 w-8" />}
title="No invoices yet"
description="Create your first invoice to get started"
action={<Button>Create Invoice</Button>}
/>
```
## Component Variants
### Color Variants
Most components support these variants:
- `default` - Uses default theme colors
- `success` - Green color scheme for positive states
- `warning` - Orange/amber for warnings
- `error` - Red for errors or destructive actions
- `info` - Blue for informational content
### Size Variants
- `sm` - Small size
- `default` - Normal size
- `lg` - Large size
## Usage Examples
### Standard Page Structure
```tsx
export default function ExamplePage() {
return (
<PageContent>
<PageHeader
title="Page Title"
description="Page description"
variant="gradient"
>
<Button variant="brand">
Primary Action
</Button>
</PageHeader>
<PageSection>
<PageGrid columns={4}>
<StatsCard {...statsProps} />
</PageGrid>
</PageSection>
<PageSection
title="Data Table Title"
description="Table description"
>
<DataTable {...tableProps} />
</PageSection>
</PageContent>
);
}
```
### Consistent Button Usage
```tsx
// Primary actions
<Button variant="brand">Create New</Button>
// Secondary actions
<Button variant="outline">Cancel</Button>
// Destructive actions
<Button variant="destructive">Delete</Button>
// Icon-only actions
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
```
## Styling Guidelines
### Do's
- ✅ Use predefined color variables from globals.css
- ✅ Compose existing UI components
- ✅ Use semantic variant names (success, error, etc.)
- ✅ Follow the established spacing patterns
- ✅ Use the PageLayout components for structure
### Don'ts
- ❌ Add custom colors directly in components
- ❌ Create one-off table or card implementations
- ❌ Override component styles with important flags
- ❌ Use arbitrary spacing values
- ❌ Mix different UI patterns on the same page
## Migration Checklist
When updating a page to use the unified system:
1. Replace custom tables with `DataTable` using @tanstack/react-table ColumnDef
2. Replace statistics displays with `StatsCard`
3. Replace action cards with `QuickActionCard`
4. Wrap content in `PageContent` and `PageSection`
5. Use `PageGrid` for responsive layouts
6. Replace custom empty states with `EmptyState`
7. Update buttons to use the `brand` variant for primary actions
8. Remove page-specific color classes
9. Use `DataTableColumnHeader` for sortable column headers
10. Use `DataTableSkeleton` for loading states
## Color System Reference
### Brand Colors
- Primary: Green (`#16a34a` / `oklch(0.646 0.222 164.25)`)
- Secondary: Teal/cyan shades
- Gradients: Use `bg-brand-gradient` class
### Status Colors
- Success: Green shades
- Warning: Amber/orange shades
- Error: Red shades
- Info: Blue shades
### Semantic Colors
- Background: White/dark gray
- Foreground: Black/white text
- Muted: Gray shades for secondary content
- Border: Light gray borders
## Component Documentation
For detailed component APIs and props, refer to:
- `/src/components/ui/data-table.tsx` - TanStack Table-based data table with sorting, filtering, and pagination
- `/src/components/ui/stats-card.tsx` - Statistics display cards
- `/src/components/ui/quick-action-card.tsx` - Interactive action cards
- `/src/components/ui/page-layout.tsx` - Page structure components
### DataTable Props
- `columns`: ColumnDef array from @tanstack/react-table
- `data`: Array of data to display
- `searchPlaceholder?`: Placeholder text for search input
- `showColumnVisibility?`: Show/hide column visibility toggle (default: true)
- `showPagination?`: Show/hide pagination controls (default: true)
- `showSearch?`: Show/hide search input (default: true)
- `pageSize?`: Number of items per page (default: 10)
- `filterableColumns?`: Array of column filters with options
Note: `title` and `description` should be provided via the wrapping `PageSection` component for consistent spacing and typography.
### Responsive Table Guidelines
- Use `hidden sm:flex` classes for icons in table cells
- Use `hidden md:inline` for less important columns on mobile
- Use `min-w-0` and `truncate` for text that might overflow
- Keep action buttons small with `h-8 w-8 p-0` sizing
- Test tables at all breakpoints (mobile, tablet, desktop)
## Future Considerations
1. **Form Components**: Create unified form field components
2. **Modal Patterns**: Standardize modal and dialog usage
3. **Loading States**: Create consistent skeleton loaders
4. **Animation**: Define standard transition patterns
5. **Icons**: Establish icon usage guidelines
## Maintenance
To maintain UI consistency:
1. Always check for existing components before creating new ones
2. Update this guide when adding new unified components
3. Review PRs for adherence to these patterns
4. Refactor pages that deviate from the system
+198
View File
@@ -0,0 +1,198 @@
# Dynamic Breadcrumbs Guide
## Overview
The breadcrumb system in beenvoice automatically generates navigation trails based on the current URL path. It features intelligent pluralization, proper capitalization, and dynamic resource name fetching.
## Key Features
### 1. Automatic Pluralization
The breadcrumb system intelligently handles singular and plural forms:
- **List pages** (e.g., `/dashboard/businesses`) → "Businesses"
- **Detail pages** (e.g., `/dashboard/businesses/[id]`) → "Business"
- **New pages** (e.g., `/dashboard/businesses/new`) → "Business" (singular context)
### 2. Smart Capitalization
All route segments are automatically capitalized:
- `businesses` → "Businesses"
- `clients` → "Clients"
- `invoices` → "Invoices"
### 3. Dynamic Resource Names
Instead of showing UUIDs, breadcrumbs fetch and display actual resource names:
- `/dashboard/clients/123e4567-e89b-12d3-a456-426614174000` → "Dashboard / Clients / John Doe"
- `/dashboard/invoices/987fcdeb-51a2-43f1-b321-123456789abc` → "Dashboard / Invoices / INV-2024-001"
### 4. Context-Aware Labels
Special pages are handled intelligently:
- **Edit pages**: Show the resource name instead of "Edit" as the last breadcrumb
- **New pages**: Show "New" as the last breadcrumb
- **Import/Export pages**: Show appropriate action labels
## Implementation Details
### Pluralization Rules
The system uses a comprehensive pluralization utility (`src/lib/pluralize.ts`) that handles:
```typescript
// Common business terms
business businesses
client clients
invoice invoices
category categories
company companies
// General rules
- Words ending in 's', 'ss', 'sh', 'ch', 'x', 'z' add 'es'
- Words ending in consonant + 'y' change to 'ies'
- Words ending in 'f' or 'fe' change to 'ves'
- Default add 's'
```
### Resource Fetching
The breadcrumbs automatically detect resource IDs and fetch the appropriate data:
```typescript
// Detects UUID patterns in the URL
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
// Fetches data based on resource type
- Clients: Shows client name
- Invoices: Shows invoice number or formatted date
- Businesses: Shows business name
```
### Loading States
While fetching resource data, breadcrumbs show loading skeletons:
```tsx
<Skeleton className="inline-block h-5 w-24 align-middle" />
```
## Usage Examples
### Basic List Page
**URL**: `/dashboard/clients`
**Breadcrumbs**: Dashboard / Clients
### Resource Detail Page
**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000`
**Breadcrumbs**: Dashboard / Clients / Jane Smith
### Resource Edit Page
**URL**: `/dashboard/businesses/550e8400-e29b-41d4-a716-446655440000/edit`
**Breadcrumbs**: Dashboard / Businesses / Acme Corp
*(Note: "Edit" is hidden when showing the resource name)*
### New Resource Page
**URL**: `/dashboard/invoices/new`
**Breadcrumbs**: Dashboard / Invoices / New
### Nested Resources
**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000/invoices`
**Breadcrumbs**: Dashboard / Clients / John Doe / Invoices
## Customization
### Adding New Resource Types
To add a new resource type, update the pluralization rules:
```typescript
// In src/lib/pluralize.ts
const PLURALIZATION_RULES = {
// ... existing rules
product: { singular: "Product", plural: "Products" },
service: { singular: "Service", plural: "Services" },
};
```
### Custom Resource Labels
For resources that need custom display logic, add to the breadcrumb component:
```typescript
// For invoices, show invoice number instead of ID
if (prevSegment === "invoices") {
label = invoice.invoiceNumber || format(new Date(invoice.issueDate), "MMM dd, yyyy");
}
```
### Special Segments
Add new special segments to the `SPECIAL_SEGMENTS` object:
```typescript
const SPECIAL_SEGMENTS = {
new: "New",
edit: "Edit",
import: "Import",
export: "Export",
duplicate: "Duplicate",
archive: "Archive",
};
```
## Best Practices
1. **Consistent Naming**: Use consistent URL patterns across your app
- List pages: `/dashboard/[resource]`
- Detail pages: `/dashboard/[resource]/[id]`
- Actions: `/dashboard/[resource]/[id]/[action]`
2. **Resource Fetching**: Only fetch data when needed
- Check resource type before enabling queries
- Use proper loading states
3. **Error Handling**: Handle cases where resources don't exist
- Show fallback text or maintain UUID display
- Don't break the breadcrumb trail
4. **Performance**: Breadcrumb queries are lightweight
- Only fetch minimal data (id, name)
- Use React Query caching effectively
## API Integration
The breadcrumb component integrates with tRPC routers:
```typescript
// Each resource router should have a getById method
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
// Return resource with at least id and name/title
})
```
## Accessibility
- Breadcrumbs use semantic HTML with proper ARIA labels
- Each segment is a link except the current page
- Proper keyboard navigation support
- Screen reader friendly with role="navigation"
## Responsive Design
- Breadcrumbs wrap on smaller screens
- Font sizes adjust: `text-sm sm:text-base`
- Separators scale appropriately
- Loading skeletons match text size
## Migration from Static Breadcrumbs
If migrating from hardcoded breadcrumbs:
1. Remove static breadcrumb definitions
2. Ensure URLs follow consistent patterns
3. Add getById methods to resource routers
4. Update imports to use `DashboardBreadcrumbs`
The dynamic system will automatically generate appropriate breadcrumbs based on the URL structure.
+154
View File
@@ -0,0 +1,154 @@
# Data Table Improvements Summary
## Overview
The data table component has been significantly improved to address padding, scaling, and responsiveness issues. The tables now provide a cleaner, more compact appearance while maintaining excellent usability across all device sizes.
## Key Improvements Made
### 1. Tighter, More Consistent Padding
**Before:**
- Inconsistent padding across different table sections
- Excessive vertical padding making tables feel loose
- Cards had default py-6 padding that was too spacious
**After:**
- Table cells: `py-1.5` (mobile) / `py-2` (desktop) - reduced from `py-2.5` / `py-3`
- Table headers: `h-9` (mobile) / `h-10` (desktop) - reduced from `h-10` / `h-12`
- Filter/pagination cards: `py-2` with `px-3` horizontal padding
- Table card: `p-0` to wrap content tightly
### 2. Improved Responsive Column Handling
**Before:**
```tsx
// Cells would hide but headers remained visible
cell: ({ row }) => (
<span className="hidden md:inline">{row.original.phone}</span>
),
```
**After:**
```tsx
// Both header and cell hide together
cell: ({ row }) => row.original.phone || "—",
meta: {
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
```
### 3. Better Small Card Appearance
- Filter card: Compact `py-2` padding with proper horizontal spacing
- Pagination card: Matching `py-2` padding for consistency
- Content aligned properly within smaller card boundaries
- Removed excessive gaps between elements
- Search box now has consistent padding without extra bottom spacing on mobile
### 4. Responsive Font Sizing
- Base text: `text-xs` on mobile, `text-sm` on desktop
- Consistent scaling across all table elements
- Better readability on small screens without wasting space
## Visual Comparison
### Table Density
- **Before**: ~60px per row with excessive padding
- **After**: ~40px per row with comfortable but efficient spacing
### Card Heights
- **Filter Card**: Reduced from ~80px to ~56px
- **Pagination Card**: Reduced from ~72px to ~48px
- **Table Card**: Now wraps content exactly with no extra space
- **Pagination Layout**: Entry count and pagination controls now stay on the same line on mobile
## Implementation Examples
### Responsive Column Definition
```tsx
const columns: ColumnDef<DataType>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => row.original.name,
// Always visible
},
{
accessorKey: "email",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Email" />
),
cell: ({ row }) => row.original.email,
meta: {
// Hidden on mobile, visible on tablets and up
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
},
{
accessorKey: "createdAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => formatDate(row.getValue("createdAt")),
meta: {
// Only visible on large screens
headerClassName: "hidden lg:table-cell",
cellClassName: "hidden lg:table-cell",
},
},
];
```
### Page Header Actions
Page headers now properly position action buttons to the right on all screen sizes:
```tsx
<PageHeader
title="Invoices"
description="Manage your invoices and track payments"
variant="gradient"
>
<Button asChild variant="brand" size="lg">
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-5 w-5" /> New Invoice
</Link>
</Button>
</PageHeader>
```
### Breakpoint Reference
- `sm`: 640px and up
- `md`: 768px and up
- `lg`: 1024px and up
- `xl`: 1280px and up
## Benefits
1. **More Data Visible**: Tighter spacing allows more rows to be visible without scrolling
2. **Professional Appearance**: Clean, compact design suitable for business applications
3. **Better Mobile UX**: Properly hidden columns prevent layout breaking
4. **Consistent Styling**: All table instances now follow the same spacing rules
5. **Performance**: CSS-only solution with no JavaScript overhead
6. **Improved Mobile Layout**: Pagination controls stay inline with entry count on mobile
7. **Consistent Header Actions**: Action buttons properly positioned to the right
## Migration Checklist
- [x] Update column definitions to use `meta` properties
- [x] Remove inline responsive classes from cell content
- [x] Test on actual mobile devices
- [x] Verify touch targets remain accessible (min 44x44px)
- [x] Check that critical data remains visible on small screens
## Best Practices Going Forward
1. **Column Priority**: Always keep the most important 2-3 columns visible on mobile
2. **Content Density**: Use the tighter spacing for data tables, looser spacing for content lists
3. **Responsive Testing**: Test at 320px, 768px, and 1024px minimum
4. **Accessibility**: Ensure interactive elements maintain proper touch targets despite tighter spacing
@@ -0,0 +1,246 @@
# Data Table Responsive Design Guide
## Overview
The data table component has been updated to provide better responsive behavior, consistent padding, and proper scaling across different screen sizes.
## Key Improvements
### 1. Consistent Padding
- Uniform padding across all table elements
- Responsive padding that scales with screen size
- Cards now have consistent spacing (p-3 on mobile, p-4 on desktop)
### 2. Proper Responsive Column Hiding
- Columns now properly hide both headers and cells on smaller screens
- Uses `meta` properties for clean column visibility control
- No more orphaned headers on mobile devices
### 3. Better Scaling
- Font sizes adapt to screen size (text-xs on mobile, text-sm on desktop)
- Button sizes and spacing adjust appropriately
- Pagination controls are optimized for touch devices
## Using Responsive Columns
### Basic Column Definition
```tsx
const columns: ColumnDef<YourDataType>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => row.original.name,
// Always visible on all screen sizes
},
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => row.original.phone || "—",
meta: {
// Hidden on mobile, visible on md screens and up
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
},
{
accessorKey: "address",
header: "Address",
cell: ({ row }) => formatAddress(row.original),
meta: {
// Hidden on mobile and tablet, visible on lg screens and up
headerClassName: "hidden lg:table-cell",
cellClassName: "hidden lg:table-cell",
},
},
{
accessorKey: "createdAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => formatDate(row.getValue("createdAt")),
meta: {
// Only visible on xl screens and up
headerClassName: "hidden xl:table-cell",
cellClassName: "hidden xl:table-cell",
},
},
];
```
### Responsive Breakpoints
- **Always visible**: No meta properties needed
- **md and up** (768px+): `hidden md:table-cell`
- **lg and up** (1024px+): `hidden lg:table-cell`
- **xl and up** (1280px+): `hidden xl:table-cell`
## Complex Cell Content
For cells with complex content that should partially hide on mobile:
```tsx
{
accessorKey: "client",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Client" />
),
cell: ({ row }) => {
const client = row.original;
return (
<div className="flex items-center gap-3">
{/* Icon hidden on mobile, shown on sm screens */}
<div className="bg-status-info-muted hidden rounded-lg p-2 sm:flex">
<UserIcon className="text-status-info h-4 w-4" />
</div>
<div className="min-w-0">
<p className="truncate font-medium">{client.name}</p>
{/* Secondary info can be hidden on very small screens if needed */}
<p className="text-muted-foreground truncate text-sm">
{client.email || "—"}
</p>
</div>
</div>
);
},
}
```
## Best Practices
### 1. Priority-Based Column Hiding
- Always show the most important columns (e.g., name, status, primary action)
- Hide supplementary information first (e.g., dates, secondary details)
- Consider hiding decorative elements (icons) on mobile while keeping text
### 2. Mobile-First Design
- Ensure at least 2-3 columns are visible on mobile
- Test on actual devices, not just browser dev tools
- Consider the minimum viable information for each row
### 3. Touch-Friendly Actions
- Action buttons should be at least 44x44px on mobile
- Use appropriate spacing between interactive elements
- Consider grouping actions in a dropdown on mobile
### 4. Performance
- The responsive system uses CSS classes, so there's no JavaScript overhead
- Column visibility is handled by Tailwind's responsive utilities
- No re-renders needed when resizing
## Migration Guide
If you have existing data tables, update them as follows:
### Before:
```tsx
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => (
<span className="hidden md:inline">{row.original.phone || "—"}</span>
),
}
```
### After:
```tsx
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => row.original.phone || "—",
meta: {
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
}
```
## Common Patterns
### Status Columns
Always visible, use color and icons to convey information efficiently:
```tsx
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => <StatusBadge status={row.original.status} />,
}
```
### Date Columns
Often hidden on mobile, show relative dates when space is limited:
```tsx
{
accessorKey: "createdAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => {
const date = row.getValue("createdAt") as Date;
return (
<>
{/* Full date on larger screens */}
<span className="hidden sm:inline">{formatDate(date)}</span>
{/* Relative date on mobile */}
<span className="sm:hidden">{formatRelativeDate(date)}</span>
</>
);
},
}
```
### Action Columns
Keep actions accessible but space-efficient:
```tsx
{
id: "actions",
cell: ({ row }) => {
const item = row.original;
return (
<div className="flex items-center justify-end gap-1">
{/* Show individual buttons on larger screens */}
<div className="hidden sm:flex sm:gap-1">
<EditButton item={item} />
<DeleteButton item={item} />
</div>
{/* Dropdown menu on mobile */}
<div className="sm:hidden">
<ActionsDropdown item={item} />
</div>
</div>
);
},
}
```
## Testing Checklist
- [ ] Table is readable on 320px wide screens
- [ ] Headers and cells align properly at all breakpoints
- [ ] Touch targets are at least 44x44px on mobile
- [ ] Horizontal scrolling works smoothly when needed
- [ ] Critical information is always visible
- [ ] Loading states work correctly
- [ ] Empty states are responsive
- [ ] Pagination controls are touch-friendly
## Accessibility Notes
- Hidden columns are properly hidden from screen readers
- Table remains navigable with keyboard at all screen sizes
- Sort controls are accessible on mobile
- Focus indicators are visible on all interactive elements
+281
View File
@@ -0,0 +1,281 @@
# Enhanced Email Sending Features
## Overview
The beenvoice application now includes a comprehensive email sending system with preview, rich text editing, and confirmation features. This enhancement provides a professional email experience for sending invoices to clients.
## Features
### 🎨 Rich Text Email Composer
- **Tiptap Editor Integration**: Professional rich text editing with formatting options
- **Text Formatting**: Bold, italic, strikethrough, and color options
- **Text Alignment**: Left, center, and right alignment
- **Lists**: Bullet points and numbered lists
- **Color Picker**: Choose from a variety of text colors
- **Real-time Preview**: See changes as you type
### 👁️ Email Preview
- **Visual Preview**: See exactly how your email will appear to recipients
- **Invoice Summary**: Displays key invoice details (number, date, amount)
- **Attachment Notice**: Shows PDF attachment information
- **Professional Styling**: Clean, branded email template
- **Responsive Design**: Optimized for all screen sizes with proper text wrapping
- **Mobile-First**: Touch-friendly interface with proper spacing
### ✅ Send Confirmation
- **Two-Step Process**: Compose ↔ Preview with Send Action
- **Action-Based Sending**: Send button available from sidebar and floating action bar
- **Status Updates**: Automatic status change from draft to sent
- **Error Handling**: Clear error messages with specific guidance
- **SSR Compatible**: Proper hydration handling for server-side rendering
### 📄 Smart Templates
- **Auto-Generated Content**: Professional email templates with proper paragraph spacing
- **Time-Based Greetings**: Morning, afternoon, or evening greetings
- **Invoice Details**: Automatically includes invoice number, date, and amount
- **Business Branding**: Uses your business name and contact information
- **Immediate Loading**: Content appears instantly in the editor without requiring tab switching
## Components
### EmailComposer
**Location**: `src/components/forms/email-composer.tsx`
A rich text editor component for composing emails with formatting options.
**Props**:
- `subject`: Email subject line
- `onSubjectChange`: Callback for subject changes
- `content`: Email content (HTML)
- `onContentChange`: Callback for content changes
- `fromEmail`: Sender email address
- `toEmail`: Recipient email address
### EmailPreview
**Location**: `src/components/forms/email-preview.tsx`
Displays a visual preview of how the email will appear to recipients.
**Props**:
- `subject`: Email subject line
- `fromEmail`: Sender email address
- `toEmail`: Recipient email address
- `content`: Email content (HTML)
- `invoice`: Invoice data for summary display
### SendEmailDialog
**Location**: `src/components/forms/send-email-dialog.tsx`
Main dialog component that combines composition, preview, and confirmation.
**Props**:
- `invoiceId`: ID of the invoice to send
- `trigger`: React element that opens the dialog
- `invoice`: Invoice data
- `onEmailSent`: Callback when email is successfully sent
### EnhancedSendInvoiceButton
**Location**: `src/components/forms/enhanced-send-invoice-button.tsx`
Enhanced button component that opens the email dialog.
**Props**:
- `invoiceId`: ID of the invoice to send
- `variant`: Button style variant
- `className`: Additional CSS classes
- `showResend`: Whether to show "Resend" text
- `size`: Button size
## API Enhancements
### Enhanced Email Router
**Location**: `src/server/api/routers/email.ts`
The email API has been enhanced to support custom content and HTML emails.
**New Parameters**:
- `customSubject`: Optional custom email subject
- `customContent`: Optional custom email content (HTML)
- `useHtml`: Boolean flag to send HTML email
**Features**:
- HTML email support with plain text fallback
- Custom subject lines
- Rich HTML content
- Automatic PDF attachment
- BCC to business email
- Comprehensive error handling
## Usage Examples
### Basic Usage
```tsx
import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button";
// Replace existing send buttons
<EnhancedSendInvoiceButton
invoiceId={invoice.id}
className="w-full"
showResend={invoice.status === "sent"}
/>
```
### Custom Dialog
```tsx
import { SendEmailDialog } from "~/components/forms/send-email-dialog";
<SendEmailDialog
invoiceId={invoice.id}
invoice={invoiceData}
trigger={<Button>Send Custom Email</Button>}
onEmailSent={() => console.log("Email sent!")}
/>
```
### Standalone Components
```tsx
import { EmailComposer } from "~/components/forms/email-composer";
import { EmailPreview } from "~/components/forms/email-preview";
// Use individual components for custom implementations
<EmailComposer
subject={subject}
onSubjectChange={setSubject}
content={content}
onContentChange={setContent}
fromEmail="you@business.com"
toEmail="client@company.com"
/>
<EmailPreview
subject={subject}
content={content}
fromEmail="you@business.com"
toEmail="client@company.com"
invoice={invoiceData}
/>
```
## Technical Details
### Dependencies
- **@tiptap/react**: Rich text editor framework
- **@tiptap/starter-kit**: Basic editor functionality
- **@tiptap/extension-text-style**: Text styling support
- **@tiptap/extension-color**: Color picker support
- **@tiptap/extension-text-align**: Text alignment options
### Email Templates
The system generates professional HTML email templates with:
- Responsive design
- Brand colors (green theme)
- Invoice summary cards
- Proper typography
- Attachment indicators
- Footer branding
### Error Handling
Comprehensive error handling for:
- Invalid email addresses
- Missing client information
- Resend API issues
- Network connectivity problems
- Domain verification issues
- Rate limiting
## Usage in Application
The enhanced email functionality is integrated throughout the application:
- Invoice view pages with enhanced send buttons
- Full-page email composition interface
- Professional email templates with invoice integration
- Comprehensive preview and confirmation workflow
## Migration Guide
### From Basic Send Button
Replace existing `SendInvoiceButton` components with `EnhancedSendInvoiceButton`:
```tsx
// Before
import { SendInvoiceButton } from "../_components/send-invoice-button";
<SendInvoiceButton invoiceId={invoice.id} />
// After
import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button";
<EnhancedSendInvoiceButton invoiceId={invoice.id} />
```
### API Compatibility
The enhanced email API is backward compatible with existing implementations. New features are opt-in through additional parameters.
## Security Considerations
- **Input Sanitization**: All user input is validated and sanitized
- **Email Validation**: Comprehensive email format validation
- **Rate Limiting**: Built-in protection against spam
- **Domain Verification**: Resend domain verification required
- **Authentication**: All email operations require valid authentication
## Performance
- **SSR Optimization**: Proper server-side rendering with hydration safeguards
- **Efficient Loading**: Content initializes immediately without requiring user interaction
- **Optimized Rendering**: Efficient React component updates with proper state management
- **Caching**: Proper query caching for invoice data
- **Error Boundaries**: Graceful error handling without crashes
- **Responsive Design**: Optimized layouts for all screen sizes with text overflow prevention
## Navigation
### Send Email Page
Access the email interface by clicking "Send Invoice" on any invoice:
- `/dashboard/invoices/[id]/send` - Full-page email composition
- Two-tab interface: Compose ↔ Preview
- Send action available from sidebar and floating action bar
- Fully responsive design with proper text wrapping and overflow handling
- Professional layout with sidebar containing:
- Invoice summary (number, client, date, status)
- Email details (from, to, subject, attachment info)
- Context-aware action buttons
- Auto-filled message with proper HTML formatting and paragraph spacing
- Immediate content loading without requiring tab navigation
## Fixes and Improvements
Recent fixes and enhancements:
- **SSR Compatibility**: Fixed Tiptap hydration issues for reliable server-side rendering
- **Content Loading**: Improved email content initialization for immediate display
- **Responsive Design**: Enhanced text wrapping and overflow handling for all screen sizes
- **UI/UX**: Removed confirmation tab in favor of action-based sending approach
- **Performance**: Optimized state management for faster content loading
## Future Enhancements
Planned improvements include:
- Email templates library
- Scheduling email delivery
- Email tracking and read receipts
- Bulk email sending
- Custom email signatures
- Integration with email marketing tools
## Support
For issues or questions related to the email system:
1. Check the console for error messages
2. Verify Resend API configuration
3. Ensure client email addresses are valid
4. Review domain verification status
5. Check network connectivity
## Changelog
### Version 1.0.0
- Initial release of enhanced email system
- Rich text editor integration
- Email preview functionality
- Send confirmation workflow
- HTML email support
- Professional templates
- Demo page implementation
+279
View File
@@ -0,0 +1,279 @@
# Forms Improvement Guide
## Overview
The business and client creation/editing forms have been significantly improved with better organization, shared components, enhanced validation, and improved user experience.
## Key Improvements
### 1. Shared Components & Utilities
#### Address Form Component (`src/components/ui/address-form.tsx`)
A reusable address form component that handles:
- Country-aware formatting (US ZIP codes, Canadian postal codes)
- State dropdown for US addresses, text input for other countries
- Popular countries listed first in country dropdown
- Automatic field adjustments based on country selection
```tsx
<AddressForm
addressLine1={formData.addressLine1}
addressLine2={formData.addressLine2}
city={formData.city}
state={formData.state}
postalCode={formData.postalCode}
country={formData.country}
onChange={handleInputChange}
errors={errors}
required={false}
/>
```
#### Form Constants & Utilities (`src/lib/form-constants.ts`)
Centralized location for:
- US states list with proper formatting
- All countries with ISO codes
- Popular countries for quick selection
- Format functions for phone, postal codes, tax IDs, and URLs
- Validation utilities and messages
### 2. Enhanced Form Validation
#### Real-time Validation
- Errors clear as soon as user starts typing
- Field-specific validation messages
- Visual feedback with red borders on invalid fields
#### Smart Validation Rules
- Email: Proper email format checking
- Phone: US phone number format validation
- Address: Required fields only if any address field is filled
- URL: Automatic https:// prefix addition
```typescript
// Example validation
if (formData.email && !isValidEmail(formData.email)) {
newErrors.email = VALIDATION_MESSAGES.email;
}
```
### 3. Better Form Organization
#### Card-based Sections
Forms are now organized into logical sections using cards:
- **Basic Information**: Core fields like name, tax ID
- **Contact Information**: Email, phone, website
- **Address**: Complete address form with smart country handling
- **Settings**: Business-specific settings like default business flag
#### Consistent Layout
- Maximum width container for better readability
- Responsive grid layouts that stack on mobile
- Proper spacing between sections
- Clear visual hierarchy
### 4. Improved User Experience
#### Loading States
- Skeleton loader while fetching data in edit mode
- Disabled form fields during submission
- Loading spinner in submit button
#### Unsaved Changes Warning
```typescript
const handleCancel = () => {
if (isDirty) {
const confirmed = window.confirm(
"You have unsaved changes. Are you sure you want to leave?"
);
if (!confirmed) return;
}
router.push("/dashboard/businesses");
};
```
#### Smart Field Formatting
- Phone numbers: Auto-format as (555) 123-4567
- Tax ID: Auto-format as 12-3456789
- Postal codes: Format based on country (US vs Canadian)
- Website URLs: Auto-add https:// if missing
### 5. Responsive Design
#### Mobile Optimizations
- Form sections stack vertically on small screens
- Touch-friendly input sizes
- Proper button positioning
- Readable font sizes
#### Desktop Enhancements
- Two-column layouts for related fields
- Optimal reading width
- Side-by-side form actions
### 6. Code Reusability
#### Shared Between Business & Client Forms
- Address form component
- Validation logic
- Format functions
- Constants (states, countries)
- Error handling patterns
#### TypeScript Interfaces
```typescript
interface FormData {
name: string;
email: string;
phone: string;
// ... other fields
}
interface FormErrors {
name?: string;
email?: string;
// ... validation errors
}
```
## Usage Examples
### Basic Form Implementation
```tsx
export function BusinessForm({ businessId, mode }: BusinessFormProps) {
const [formData, setFormData] = useState<FormData>(initialFormData);
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDirty, setIsDirty] = useState(false);
// Handle input changes
const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setIsDirty(true);
// Clear error when user types
if (errors[field as keyof FormErrors]) {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
};
// Validate and submit
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
toast.error("Please correct the errors in the form");
return;
}
// Submit logic...
};
}
```
### Field with Icon and Validation
```tsx
<div className="space-y-2">
<Label htmlFor="email">
Email
<span className="text-muted-foreground ml-1 text-xs">(Optional)</span>
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder={PLACEHOLDERS.email}
className={`pl-10 ${errors.email ? "border-destructive" : ""}`}
disabled={isSubmitting}
/>
</div>
{errors.email && (
<p className="text-sm text-destructive">{errors.email}</p>
)}
</div>
```
## Best Practices
### 1. Form State Management
- Use controlled components for all inputs
- Track dirty state for unsaved changes warnings
- Clear errors when user corrects them
- Disable form during submission
### 2. Validation Strategy
- Validate on submit, not on blur (less annoying)
- Clear errors immediately when user starts fixing them
- Show field-level errors below each input
- Use consistent error message format
### 3. Accessibility
- Proper label associations with htmlFor
- Required field indicators
- Error messages linked to fields
- Keyboard navigation support
- Focus management
### 4. Performance
- Memoize expensive computations
- Use debouncing for format functions if needed
- Lazy load country lists
- Optimize re-renders with proper state management
## Migration Guide
### From Old Forms
1. Replace inline state/country arrays with imported constants
2. Use `AddressForm` component instead of individual address fields
3. Apply format functions from `form-constants.ts`
4. Update validation to use shared utilities
5. Wrap sections in Card components
6. Add loading and dirty state tracking
### Example Migration
```tsx
// Before
const US_STATES = [
{ value: "AL", label: "Alabama" },
// ... duplicated in each form
];
// After
import { US_STATES, formatPhoneNumber } from "~/lib/form-constants";
import { AddressForm } from "~/components/ui/address-form";
```
## Future Enhancements
### Planned Improvements
1. **Field-level permissions**: Disable fields based on user role
2. **Auto-save**: Save draft as user types
3. **Multi-step forms**: Break long forms into steps
4. **Conditional fields**: Show/hide fields based on other values
5. **Bulk operations**: Create multiple records at once
6. **Import from templates**: Pre-fill common business types
### Extensibility
The form system is designed to be easily extended:
- Add new format functions to `form-constants.ts`
- Create additional shared form components
- Extend validation rules as needed
- Add new field types with consistent patterns
## Troubleshooting
### Common Issues
1. **Validation not working**: Ensure field names match FormErrors interface
2. **Format function not applying**: Check that onChange uses the format function
3. **Country dropdown not searching**: Verify SearchableSelect has search enabled
4. **Address validation failing**: Check if country field affects validation rules
### Debug Tips
- Use React DevTools to inspect form state
- Check console for validation errors
- Verify API responses match expected format
- Test with different country selections