Add 'apps/web/' from commit '1e7174fa604b11e7c3983cd8ad01c596f6e77e96'
git-subtree-dir: apps/web git-subtree-mainline:068a51b46bgit-subtree-split:1e7174fa60
This commit is contained in:
@@ -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 (0000–0014+)
|
||||
```
|
||||
|
||||
## 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`
|
||||
@@ -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 (~50–100 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.
|
||||
@@ -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 |
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user