Build Cobble project structure from the handoff
- Remove the preset document suite (letters, emails, invoices, quotes, PDF generation, line items) with its routes, API handlers and tables - Rebrand to Cobble: site data, Lato, handoff design tokens (light and dark) in main.css, brand assets in public/brand, ember stone logo and regenerated favicons - Add bots, bot_messages, bot_tasks and bot_task_steps tables, regenerate DB types (BUN_SQLITE_COBBLE_*) and add a sample crew seed (seed:bots) - Add the chat shell at /admin/bots: rail with pinned coordinator and user popover, bottom-anchored conversation with composer, working-on panel, and a send-message API scoped to the session user - Add BotStone and Wordmark components, crew status and relative time utils with tests, and update AGENTS.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
@@ -9,7 +9,7 @@ node_modules
|
||||
.data
|
||||
.bunext
|
||||
.bun-sqlite
|
||||
db/bsqlite
|
||||
db/cobble
|
||||
db/.backups
|
||||
db/.exports
|
||||
coverage
|
||||
|
||||
@@ -40,7 +40,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
/.bunext
|
||||
|
||||
# bun-sqlite local database + artifacts
|
||||
/db/bsqlite
|
||||
/db/cobble
|
||||
/db/.backups
|
||||
/db/.exports
|
||||
.bun-sqlite
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Agent Instructions
|
||||
|
||||
This is **Cobble** (`cobble.moduletrace.org`): an open-source crew of small AI worker bots. Product and design source of truth is the handoff in `assets/handoff/` (gitignored locally): `PRODUCT.md`, `IMPLEMENTATION.md`, `tokens/`. Unit noun is **bot**, never "agent" or "expert".
|
||||
|
||||
## Stack
|
||||
|
||||
- **Runtime:** Bun - not Node. Use `bun` commands, never `npm`/`npx`.
|
||||
@@ -8,7 +10,7 @@
|
||||
- **UI:** Tailwind CSS v4 + the `@moduletrace/twui` component library (installed package) + shared components in `src/components/general/`. There is **no** shadcn/ui, no `tailwind.config`, and no `components.json` - twui is the only component system. CSS chain: `src/pages/__root.tsx` imports `src/styles/main.css`, which imports `@moduletrace/twui/base.css` (`@import "tailwindcss"`). Theme tokens (colors/radius/fonts) are overridden via the `@theme inline` block in `src/styles/main.css`.
|
||||
- **Icons:** `lucide-react`.
|
||||
- **Validation:** `zod` v4 (`[email protected]`).
|
||||
- **Branding:** Site name/slug live only in `src/data/site-data.ts`. Logo source of truth is `public/logos/logo.svg` (rendered inline by `src/components/general/logo-icon.tsx`); browser favicon/assets are generated from it.
|
||||
- **Branding:** Site name/slug live only in `src/data/site-data.ts`. Logo source of truth is `public/logos/logo.svg` (the ember stone; rendered inline via `BotStone` in `src/components/general/logo-icon.tsx`); browser favicon/assets are generated from it. Handoff brand assets (stones, coordinator, wordmarks, app icon) live in `public/brand/`.
|
||||
|
||||
## Entrypoint
|
||||
|
||||
@@ -32,6 +34,7 @@
|
||||
| DB admin UI | `bun run db:admin` |
|
||||
| DB backup | `bun run db:backup` |
|
||||
| Seed super admin user | `bun run seed` (runs `src/scripts/seed-super-admin.tsx`) |
|
||||
| Seed sample bot crew | `bun run seed:bots [email]` (runs `src/scripts/seed-bots.ts`, idempotent) |
|
||||
| Query DB (CLI) | `bun llm-code-assistant/query-db.ts "<sql>"` |
|
||||
| Install | `bun install` |
|
||||
|
||||
@@ -74,19 +77,14 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
|
||||
|
||||
`api/admin/crud/[[...paths]].ts` dispatches `GET/POST/PUT/DELETE` by table name: `userAuth` → `read_only` GET-only gate → per-table switch → generic fallback (super admin only). Tables without custom handlers fall through to generic CRUD with zero code. Customize a table by adding `src/pages/api/admin/crud/(tables)/<table>/` with `index.ts` + per-method files, then add a `case` in the router.
|
||||
|
||||
### Documents (Letters / Emails / Invoices / Quotes)
|
||||
### Bots (the chat shell)
|
||||
|
||||
The preset ships a complete document suite on top of the generic CRUD:
|
||||
|
||||
- **Tables:** `letters`, `emails`, `email_attachments`, `invoices`, `invoice_items`, `quotes`, `quote_items`, `invoice_quotes` (statuses/enums in `src/dict/document-dict.ts`)
|
||||
- **Admin sections:** `/admin/letters`, `/admin/emails`, `/admin/invoices`, `/admin/quotes` - full list/create/view/edit CRUD, line items, quote-to-invoice linking, PDF download, send-email modals
|
||||
- **PDF:** `src/functions/backend/pdf/` - `pdf-helpers.ts`, `letter-pdf.ts`, `invoice-pdf.ts` (drives both invoices and quotes via `kind`), `render-blocks.ts` + `html-blocks.ts` (TinyMCE HTML → PDF blocks), `letter-date.ts`
|
||||
- **Email:** `src/functions/backend/email/` - `email-client.ts` (nodemailer SMTP), `send-email.tsx` (renders ReactNode into the `src/email/root.tsx` shell), `document-emails.tsx` (invoice/quote bodies)
|
||||
- **API routes:** `api/admin/{letters,invoices,quotes}/generate-pdf.ts` (streams PDF binary), `api/admin/{emails,invoices,quotes}/send-email.tsx`, `api/admin/emails/sync-attachments.ts`
|
||||
- **DB grabs:** `db/documents/`, `db/invoices/grab-invoice.ts`, `db/quotes/grab-quote.ts`
|
||||
- **Client helpers:** `src/functions/frontend/document-actions.ts` (PDF download + send-email actions)
|
||||
- **Numbering:** invoice/quote numbers auto-generate as `<SLUG>-YYYYMM-###` / `<SLUG>Q-YYYYMM-###` from `SiteData["SiteSlug"]` in the `invoices`/`quotes` CRUD handlers
|
||||
- **Shared UI:** `src/components/general/` `pdf-preview.tsx`, `sortable-line-items-editor.tsx` (+ `sortable-line-item-row.tsx`), `copy-from-existing-select.tsx`, `main-hero-section.tsx`
|
||||
- **Tables:** `bots` (per-user workspace; `kind` worker|coordinator, `hue`, `role`, `face`, `status_line`, `unread`), `bot_messages`, `bot_tasks`, `bot_task_steps`. Enums in `src/dict/bot-dict.ts`.
|
||||
- **Routes:** `/admin/bots` redirects to the first bot (coordinator pinned first); `/admin/bots/[bot_id]` renders conversation + working-on panel. Both render in the full-bleed `src/layouts/bots/` layout (rail of conversations + user popover), selected in `__root.tsx` before the admin layout.
|
||||
- **Server:** `src/functions/backend/db/bots/` (`grab-bots`, `grab-bot-messages`, `grab-active-bot-task`, `grab-user-bot`, `mark-bot-read`, `insert-user-message`). `api/admin/bots/send-message.ts` posts a user message to a bot owned by the session user (zod: `src/schemas/bot-schema.ts`).
|
||||
- **Avatars:** `BotStone` (`src/components/general/bot-stone.tsx`) renders the stone inline from `src/data/stone-geometry.ts`, hue as a fill token, face (`idle|working|done|asleep|needs-you`) drawn as eyes. The coordinator is hue-less (ink body, paper eyes; inverts in dark). Never put a stone in a circle/badge or add outline/shadow. Wordmark is vector (`Wordmark`, `src/data/wordmark-paths.ts`), never live text.
|
||||
- **Copy helpers:** `src/utils/crew-status.ts` (`3 working · 1 needs you`, "Three bots are working. One needs you."), `src/utils/relative-time.ts` (relative timestamps only).
|
||||
- **Not built yet:** add-bot flow, attachments, routines, help, live updates over WebSocket, the model layer.
|
||||
|
||||
## Admin Route Hierarchy
|
||||
|
||||
@@ -94,10 +92,8 @@ Actual preset routes under `/admin`:
|
||||
|
||||
- `/admin` - dashboard
|
||||
- `/admin/people` - admin users list, plus `create/`, `[user_id]/` (profile), `[user_id]/edit/`
|
||||
- `/admin/letters` - letter templates, plus `create/`, `[id]/`, `[id]/edit/`
|
||||
- `/admin/emails` - email records/drafts, plus `create/`, `[id]/`, `[id]/edit/`, `template/` (live email-shell preview)
|
||||
- `/admin/invoices` - invoices, plus `create/`, `[id]/`, `[id]/edit/`
|
||||
- `/admin/quotes` - quotes, plus `create/`, `[id]/`, `[id]/edit/`
|
||||
- `/admin/bots` - the Cobble chat shell, plus `[bot_id]/`
|
||||
- `/admin/logs` - audit log (super admin; also the "Activity" item in the bots popover)
|
||||
- `/admin/backups` - super admin only: create/list/download database snapshots in `db/.backups`, plus `restore/` (restore a listed backup, or upload a SQLite database file which is saved to `.backups` then restored). Shells out to the `bun-sqlite` CLI (`backup`, and `import <file>` since `restore` is interactive) via `src/functions/backend/backups/`; API routes in `api/admin/backups/`
|
||||
- `/admin/settings` - current user profile, plus `password/` (only when `AUTH_LOGIN_METHOD=password`; redirects to `/admin/settings` otherwise). The aside renders it as a collapsible sub-nav via `children` in `(dict)/admin-aside-groups-dict.tsx`
|
||||
|
||||
@@ -124,8 +120,8 @@ Server functions use `index.server.ts` alongside `index.tsx` (e.g. `single-docum
|
||||
|
||||
- **`src/hooks/`** - Shared React hooks: `use-admin-crud-get`, `use-fetch-get`, `use-form-init`, `use-local-user`, `use-users-get`
|
||||
- **`src/functions/`** - Shared functions split by target:
|
||||
- `src/functions/backend/` - Server-only: auth (`backend/auth/`), DB queries (`backend/db/` incl. `documents/`, `invoices/`, `quotes/`), email client + templates (`email/`), PDF generation (`pdf/`), media upload, `logger.ts` (audit logging)
|
||||
- `src/functions/frontend/` - Client-safe: CRUD handlers, fetch handlers, media upload/delete handlers, `document-actions.ts` (PDF download/send actions)
|
||||
- `src/functions/backend/` - Server-only: auth (`backend/auth/`), DB queries (`backend/db/` incl. `bots/`), email client + templates (`email/`), media upload, `logger.ts` (audit logging)
|
||||
- `src/functions/frontend/` - Client-safe: CRUD handlers, fetch handlers, media upload/delete handlers
|
||||
- `src/functions/server/` - Server utilities (media server handler etc.)
|
||||
- **`src/utils/`** - Utility modules: cookies, crypt, ejson, money, safe-compare, text-to-html, and more. Most have corresponding `.test.ts` files.
|
||||
- **`src/utils/rbac/`** - RBAC engine: `user-type-assignment` (matrix interpreter), `is-pass-through-user`, `can-*` permission helpers, `filter-assignable-user-types` - with `bun:test` suites driven by fixture matrices
|
||||
@@ -140,7 +136,7 @@ Server functions use `index.server.ts` alongside `index.tsx` (e.g. `single-docum
|
||||
| Directory | Description |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `@moduletrace/twui` | **The** design system - external package. Contains layout primitives, elements, editors (TinyMCE), form components, hooks, SVGs, and utilities. Import from `@moduletrace/twui` or a deep path such as `@moduletrace/twui/layout/Button` |
|
||||
| `src/components/general/` | Shared page components built on twui: `logo`, `logo-icon`, `inner-page-hero-section`, `main-hero-section`, `admin-hero`, `login-shell`, `tagline-section`, `pdf-preview`, `sortable-line-items-editor`, `copy-from-existing-select`, media/image helpers |
|
||||
| `src/components/general/` | Shared page components built on twui: `logo`, `logo-icon`, `wordmark`, `bot-stone` (+ `bot-stone-face`, `stone-eye-*`), `inner-page-hero-section`, `main-hero-section`, `admin-hero`, `login-shell`, `tagline-section`, media/image helpers |
|
||||
|
||||
## UI & Styling Rules (MANDATORY)
|
||||
|
||||
@@ -151,8 +147,8 @@ Server functions use `index.server.ts` alongside `index.tsx` (e.g. `single-docum
|
||||
- Shared: `Logo`, `LogoIcon`, `InnerPageHeroSection`, `AdminHero`, `LoginShell` (auth pages/forms), `TaglineSection`
|
||||
- **UNCHANGEABLE RULE - twui components always win over raw JSX elements.** Use twui components (`H1`-`H5`, `P`, `Span`, `List`, `Img`, `Row`, `Stack`, `Container`, `Section`, `Card`, `Button`, `Tag`, `Link`, etc.) before basic JSX elements (`<h1>`-`<h6>`, `<p>`, `<span>`, `<ul>`, `<li>`, `<img>`, `<a>`, `<button>`, `<div>`-as-card, etc.). Never introduce or reintroduce a raw JSX element where a twui equivalent exists. Override twui defaults via `className` rather than reaching for the raw element.
|
||||
- **Never create new CSS classes, CSS files, or inline `style={{...}}` hacks.** Style exclusively by composing existing component props/classNames, existing twui classNames (`twui-*`), semantic theme-token utilities (`bg-dark`, `bg-light`, `text-dark`, `text-primary`, `bg-primary`, `text-accent`, `bg-accent`, `text-secondary`, `border-slate-200`, etc.), and utility classes already used elsewhere in this codebase. Build up from what exists; extend component code only when the existing API cannot express the need.
|
||||
- **Theming:** colors, radii, and fonts change only in two places: the `@theme inline` block in `src/styles/main.css` (overrides `@moduletrace/twui/base.css` defaults). Never hardcode hex colors, radii, or font stacks inside components.
|
||||
- **Branding:** `public/logos/logo.svg` is the single source of truth for the logo mark; `logo-icon.tsx` renders it inline and `logo.tsx` renders the wordmark from `SiteData["SiteName"]`. Whenever the logo changes, run `bun run gen:favicon` to regenerate `public/favicon.ico`, `public/favicon.png`, `public/apple-touch-icon.png`, and `public/logos/logo.png` (the last is embedded in generated PDFs). Font + favicon links live in `__root.tsx` `Head()`.
|
||||
- **Theming:** colors, radii, and fonts change only in `src/styles/main.css`: Cobble tokens as CSS vars on `:root` (light) and `.dark`, exposed in the `@theme inline` block as `paper`, `surface`, `sunken`, `ink`, `ink-secondary`, `ink-disabled`, `hairline`, `hairline-strong`, `on-ink`, `hue-<name>`, plus `shadow-popover` and `--m-fast/base/slow` motion vars (zeroed under reduced motion). Font is Lato (stand-in). Never hardcode hex colors, radii, or font stacks inside components. Hues are bot identity only, never state; use `src/dict/bot-hue-classes-dict.ts` for hue classes.
|
||||
- **Branding:** `public/logos/logo.svg` is the single source of truth for the logo mark; `logo-icon.tsx` renders it inline (via `BotStone`) and `logo.tsx` renders the stone beside the vector `Wordmark`. Whenever the logo changes, run `bun run gen:favicon` to regenerate `public/favicon.ico`, `public/favicon.png`, `public/apple-touch-icon.png`, and `public/logos/logo.png` (the last is used in emails). Font + favicon links live in `__root.tsx` `Head()`.
|
||||
- **Site name:** brand literals live only in `src/data/site-data.ts`. Page meta titles, emails, and UI copy must interpolate `SiteData["SiteName"]` - never hardcode a brand name in page files.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -5,18 +5,14 @@ export const BunSQLiteTables = [
|
||||
"user_passwords",
|
||||
"media",
|
||||
"media_paradigms",
|
||||
"letters",
|
||||
"emails",
|
||||
"email_attachments",
|
||||
"invoices",
|
||||
"invoice_items",
|
||||
"quotes",
|
||||
"quote_items",
|
||||
"invoice_quotes",
|
||||
"bots",
|
||||
"bot_messages",
|
||||
"bot_tasks",
|
||||
"bot_task_steps",
|
||||
"logs",
|
||||
] as const
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_USERS = {
|
||||
export type BUN_SQLITE_COBBLE_USERS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -37,7 +33,7 @@ export type BUN_SQLITE_BSQLITE_USERS = {
|
||||
archived?: 0 | 1 | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_USER_TYPES = {
|
||||
export type BUN_SQLITE_COBBLE_USER_TYPES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -54,7 +50,7 @@ export type BUN_SQLITE_BSQLITE_USER_TYPES = {
|
||||
user_type?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES = {
|
||||
export type BUN_SQLITE_COBBLE_SSO_LOGIN_CODES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -71,7 +67,7 @@ export type BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES = {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_USER_PASSWORDS = {
|
||||
export type BUN_SQLITE_COBBLE_USER_PASSWORDS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -88,7 +84,7 @@ export type BUN_SQLITE_BSQLITE_USER_PASSWORDS = {
|
||||
password_hash?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_MEDIA = {
|
||||
export type BUN_SQLITE_COBBLE_MEDIA = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -116,7 +112,7 @@ export type BUN_SQLITE_BSQLITE_MEDIA = {
|
||||
archived?: 0 | 1 | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS = {
|
||||
export type BUN_SQLITE_COBBLE_MEDIA_PARADIGMS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -133,7 +129,7 @@ export type BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS = {
|
||||
media_paradigm?: "user-profile-image" | "generic" | "event" | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_LETTERS = {
|
||||
export type BUN_SQLITE_COBBLE_BOTS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -146,21 +142,83 @@ export type BUN_SQLITE_BSQLITE_LETTERS = {
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
/**
|
||||
* Workspace owner
|
||||
*/
|
||||
user_id?: number | "";
|
||||
name?: string;
|
||||
/**
|
||||
* One coordinator per workspace; everything else is a worker
|
||||
*/
|
||||
kind?: "worker" | "coordinator" | "";
|
||||
/**
|
||||
* Permanent identity hue. Null for the coordinator
|
||||
*/
|
||||
hue?: "ember" | "amber" | "moss" | "teal" | "cobalt" | "violet" | "rose" | "";
|
||||
/**
|
||||
* The bot's job in one line. Null for the coordinator
|
||||
*/
|
||||
role?: string;
|
||||
/**
|
||||
* The bot's status, shown as its face
|
||||
*/
|
||||
face?: "idle" | "working" | "done" | "asleep" | "needs-you" | "";
|
||||
/**
|
||||
* Short status sentence, e.g. "Waiting on you"
|
||||
*/
|
||||
status_line?: string;
|
||||
unread?: 0 | 1 | "";
|
||||
/**
|
||||
* Unix timestamp of the latest message, for rail ordering
|
||||
*/
|
||||
last_activity_at?: number | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_BOT_MESSAGES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
bot_id?: number | "";
|
||||
author?: "bot" | "user" | "";
|
||||
text?: string;
|
||||
/**
|
||||
* Renders the Needs you chip under the message
|
||||
*/
|
||||
needs_you?: 0 | 1 | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_BOT_TASKS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
bot_id?: number | "";
|
||||
title?: string;
|
||||
to_name?: string;
|
||||
to_address?: string;
|
||||
from_name?: string;
|
||||
from_address?: string;
|
||||
subject?: string;
|
||||
salutation?: string;
|
||||
body_html?: string;
|
||||
closing?: string;
|
||||
signature_name?: string;
|
||||
signature_title?: string;
|
||||
archived?: 0 | 1 | "";
|
||||
status?: "active" | "done" | "";
|
||||
/**
|
||||
* "Open computer" target
|
||||
*/
|
||||
computer_url?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_EMAILS = {
|
||||
export type BUN_SQLITE_COBBLE_BOT_TASK_STEPS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -173,149 +231,13 @@ export type BUN_SQLITE_BSQLITE_EMAILS = {
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
title?: string;
|
||||
subject?: string;
|
||||
body_html?: string;
|
||||
to_emails?: string;
|
||||
status?: "draft" | "sent" | "";
|
||||
sent_at?: number | "";
|
||||
archived?: 0 | 1 | "";
|
||||
task_id?: number | "";
|
||||
label?: string;
|
||||
state?: "done" | "active" | "pending" | "";
|
||||
position?: number | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
email_id?: number | "";
|
||||
type?: "letter" | "invoice" | "quote" | "file" | "";
|
||||
letter_id?: number | "";
|
||||
media_id?: number | "";
|
||||
invoice_id?: number | "";
|
||||
quote_id?: number | "";
|
||||
file_name?: string;
|
||||
mime_type?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_INVOICES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
title?: string;
|
||||
short_description?: string;
|
||||
invoice_number?: string;
|
||||
client_name?: string;
|
||||
client_email?: string;
|
||||
client_address?: string;
|
||||
issue_date?: number | "";
|
||||
due_date?: number | "";
|
||||
status?: "draft" | "issued" | "paid" | "cancelled" | "";
|
||||
notes?: string;
|
||||
archived?: 0 | 1 | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_INVOICE_ITEMS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
invoice_id?: number | "";
|
||||
description?: string;
|
||||
quantity?: number | "";
|
||||
unit_price_kobo?: number | "";
|
||||
sort_order?: number | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_QUOTES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
title?: string;
|
||||
short_description?: string;
|
||||
quote_number?: string;
|
||||
client_name?: string;
|
||||
client_email?: string;
|
||||
client_address?: string;
|
||||
issue_date?: number | "";
|
||||
valid_until?: number | "";
|
||||
status?: "draft" | "sent" | "accepted" | "declined" | "expired" | "";
|
||||
notes?: string;
|
||||
archived?: 0 | 1 | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_QUOTE_ITEMS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
quote_id?: number | "";
|
||||
description?: string;
|
||||
quantity?: number | "";
|
||||
unit_price_kobo?: number | "";
|
||||
sort_order?: number | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_INVOICE_QUOTES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
invoice_id?: number | "";
|
||||
quote_id?: number | "";
|
||||
amount_kobo?: number | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_LOGS = {
|
||||
export type BUN_SQLITE_COBBLE_LOGS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@@ -362,4 +284,4 @@ export type BUN_SQLITE_BSQLITE_LOGS = {
|
||||
ip_address?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_BSQLITE_ALL_TYPEDEFS = BUN_SQLITE_BSQLITE_USERS & BUN_SQLITE_BSQLITE_USER_TYPES & BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES & BUN_SQLITE_BSQLITE_USER_PASSWORDS & BUN_SQLITE_BSQLITE_MEDIA & BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS & BUN_SQLITE_BSQLITE_LETTERS & BUN_SQLITE_BSQLITE_EMAILS & BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS & BUN_SQLITE_BSQLITE_INVOICES & BUN_SQLITE_BSQLITE_INVOICE_ITEMS & BUN_SQLITE_BSQLITE_QUOTES & BUN_SQLITE_BSQLITE_QUOTE_ITEMS & BUN_SQLITE_BSQLITE_INVOICE_QUOTES & BUN_SQLITE_BSQLITE_LOGS
|
||||
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_LOGS
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bunext-sqlite-preset",
|
||||
"name": "cobble",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -11,6 +11,7 @@
|
||||
"db:admin": "bunx bun-sqlite admin",
|
||||
"db:backup": "bunx bun-sqlite backup",
|
||||
"seed": "bun src/scripts/seed-super-admin.tsx",
|
||||
"seed:bots": "bun src/scripts/seed-bots.ts",
|
||||
"deploy:prod": "./deploy/deploy-prod.sh",
|
||||
"start": "NODE_ENV=production bun src/server.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 872 B |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1600" height="400" viewBox="0 0 1600 400">
|
||||
<title>cobble-display</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M598.5 194.95 C597.567 196.083 596.667 196.983 595.8 197.65 C594.933 198.317 593.7 198.65 592.1 198.65 C590.567 198.65 589.183 198.283 587.95 197.55 C586.717 196.817 585.333 196 583.8 195.1 C582.267 194.2 580.467 193.383 578.4 192.65 C576.333 191.917 573.767 191.55 570.7 191.55 C566.9 191.55 563.617 192.25 560.85 193.65 C558.083 195.05 555.8 197.05 554 199.65 C552.2 202.25 550.867 205.433 550 209.2 C549.133 212.967 548.7 217.217 548.7 221.95 C548.7 231.883 550.617 239.517 554.45 244.85 C558.283 250.183 563.567 252.85 570.3 252.85 C573.9 252.85 576.75 252.4 578.85 251.5 C580.95 250.6 582.733 249.6 584.2 248.5 C585.667 247.4 587.017 246.383 588.25 245.45 C589.483 244.517 591.033 244.05 592.9 244.05 C595.367 244.05 597.233 244.95 598.5 246.75 L607.5 257.85 C604.3 261.517 600.933 264.5 597.4 266.8 C593.867 269.1 590.25 270.9 586.55 272.2 C582.85 273.5 579.133 274.4 575.4 274.9 C571.667 275.4 568.033 275.65 564.5 275.65 C558.167 275.65 552.117 274.45 546.35 272.05 C540.583 269.65 535.517 266.167 531.15 261.6 C526.783 257.033 523.317 251.417 520.75 244.75 C518.183 238.083 516.9 230.483 516.9 221.95 C516.9 214.483 518.017 207.5 520.25 201 C522.483 194.5 525.783 188.867 530.15 184.1 C534.517 179.333 539.917 175.583 546.35 172.85 C552.783 170.117 560.233 168.75 568.7 168.75 C576.833 168.75 583.967 170.05 590.1 172.65 C596.233 175.25 601.767 179.05 606.7 184.05 Z" fill="#0e1116"/>
|
||||
<path d="M724 274.05 L724 124.35 L755 124.35 L755 181.45 C759 177.583 763.367 174.5 768.1 172.2 C772.833 169.9 778.167 168.75 784.1 168.75 C789.967 168.75 795.3 169.95 800.1 172.35 C804.9 174.75 809 178.167 812.4 182.6 C815.8 187.033 818.417 192.383 820.25 198.65 C822.083 204.917 823 211.917 823 219.65 C823 228.05 821.933 235.683 819.8 242.55 C817.667 249.417 814.65 255.3 810.75 260.2 C806.85 265.1 802.2 268.9 796.8 271.6 C791.4 274.3 785.433 275.65 778.9 275.65 C775.767 275.65 772.967 275.35 770.5 274.75 C768.033 274.15 765.767 273.3 763.7 272.2 C761.633 271.1 759.75 269.783 758.05 268.25 C756.35 266.717 754.7 265.05 753.1 263.25 L752 268.45 C751.467 270.583 750.6 272.05 749.4 272.85 C748.2 273.65 746.6 274.05 744.6 274.05 Z M773.2 192.05 C769.4 192.05 766.1 192.883 763.3 194.55 C760.5 196.217 757.733 198.65 755 201.85 L755 245.85 C757.333 248.45 759.867 250.267 762.6 251.3 C765.333 252.333 768.1 252.85 770.9 252.85 C773.833 252.85 776.533 252.3 779 251.2 C781.467 250.1 783.583 248.3 785.35 245.8 C787.117 243.3 788.5 240 789.5 235.9 C790.5 231.8 791 226.75 791 220.75 C791 215.55 790.617 211.133 789.85 207.5 C789.083 203.867 787.95 200.9 786.45 198.6 C784.95 196.3 783.083 194.633 780.85 193.6 C778.617 192.567 776.067 192.05 773.2 192.05 Z" fill="#0e1116"/>
|
||||
<path d="M833.9 274.05 L833.9 124.35 L864.9 124.35 L864.9 181.45 C868.9 177.583 873.267 174.5 878 172.2 C882.733 169.9 888.067 168.75 894 168.75 C899.867 168.75 905.2 169.95 910 172.35 C914.8 174.75 918.9 178.167 922.3 182.6 C925.7 187.033 928.317 192.383 930.15 198.65 C931.983 204.917 932.9 211.917 932.9 219.65 C932.9 228.05 931.833 235.683 929.7 242.55 C927.567 249.417 924.55 255.3 920.65 260.2 C916.75 265.1 912.1 268.9 906.7 271.6 C901.3 274.3 895.333 275.65 888.8 275.65 C885.667 275.65 882.867 275.35 880.4 274.75 C877.933 274.15 875.667 273.3 873.6 272.2 C871.533 271.1 869.65 269.783 867.95 268.25 C866.25 266.717 864.6 265.05 863 263.25 L861.9 268.45 C861.367 270.583 860.5 272.05 859.3 272.85 C858.1 273.65 856.5 274.05 854.5 274.05 Z M883.1 192.05 C879.3 192.05 876 192.883 873.2 194.55 C870.4 196.217 867.633 198.65 864.9 201.85 L864.9 245.85 C867.233 248.45 869.767 250.267 872.5 251.3 C875.233 252.333 878 252.85 880.8 252.85 C883.733 252.85 886.433 252.3 888.9 251.2 C891.367 250.1 893.483 248.3 895.25 245.8 C897.017 243.3 898.4 240 899.4 235.9 C900.4 231.8 900.9 226.75 900.9 220.75 C900.9 215.55 900.517 211.133 899.75 207.5 C898.983 203.867 897.85 200.9 896.35 198.6 C894.85 196.3 892.983 194.633 890.75 193.6 C888.517 192.567 885.967 192.05 883.1 192.05 Z" fill="#0e1116"/>
|
||||
<path d="M974.8 124.35 L974.8 274.05 L943.8 274.05 L943.8 124.35 Z" fill="#0e1116"/>
|
||||
<path d="M1036.5 168.75 C1043.367 168.75 1049.65 169.817 1055.35 171.95 C1061.05 174.083 1065.95 177.183 1070.05 181.25 C1074.15 185.317 1077.35 190.3 1079.65 196.2 C1081.95 202.1 1083.1 208.783 1083.1 216.25 C1083.1 218.583 1083 220.483 1082.8 221.95 C1082.6 223.417 1082.233 224.583 1081.7 225.45 C1081.167 226.317 1080.45 226.917 1079.55 227.25 C1078.65 227.583 1077.5 227.75 1076.1 227.75 L1016.3 227.75 C1017.3 236.417 1019.933 242.7 1024.2 246.6 C1028.467 250.5 1033.967 252.45 1040.7 252.45 C1044.3 252.45 1047.4 252.017 1050 251.15 C1052.6 250.283 1054.917 249.317 1056.95 248.25 C1058.983 247.183 1060.85 246.217 1062.55 245.35 C1064.25 244.483 1066.033 244.05 1067.9 244.05 C1070.367 244.05 1072.233 244.95 1073.5 246.75 L1082.5 257.85 C1079.3 261.517 1075.85 264.5 1072.15 266.8 C1068.45 269.1 1064.65 270.9 1060.75 272.2 C1056.85 273.5 1052.95 274.4 1049.05 274.9 C1045.15 275.4 1041.433 275.65 1037.9 275.65 C1030.633 275.65 1023.817 274.467 1017.45 272.1 C1011.083 269.733 1005.517 266.217 1000.75 261.55 C995.983 256.883 992.217 251.083 989.45 244.15 C986.683 237.217 985.3 229.15 985.3 219.95 C985.3 213.017 986.483 206.45 988.85 200.25 C991.217 194.05 994.617 188.617 999.05 183.95 C1003.483 179.283 1008.85 175.583 1015.15 172.85 C1021.45 170.117 1028.567 168.75 1036.5 168.75 Z M1037.1 190.15 C1031.167 190.15 1026.533 191.833 1023.2 195.2 C1019.867 198.567 1017.667 203.417 1016.6 209.75 L1055.3 209.75 C1055.3 207.283 1054.983 204.883 1054.35 202.55 C1053.717 200.217 1052.683 198.133 1051.25 196.3 C1049.817 194.467 1047.933 192.983 1045.6 191.85 C1043.267 190.717 1040.433 190.15 1037.1 190.15 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
<path id="o-pebble" d="M660.6 168.75 C668.467 168.75 675.633 169.983 682.1 172.45 C688.567 174.917 694.117 178.45 698.75 183.05 C703.383 187.65 705.283 190.576 707.899 196.006 C708.681 197.627 707.9 198.206 707.9 200.006 L707.9 244.132 C707.9 245.932 708.675 246.508 707.899 248.132 C705.288 253.603 703.383 256.567 698.75 261.2 C694.117 265.833 688.567 269.4 682.1 271.9 C675.633 274.4 668.467 275.65 660.6 275.65 C652.667 275.65 645.45 274.4 638.95 271.9 C632.45 269.4 626.85 265.833 622.15 261.2 C617.45 256.567 613.817 250.933 611.25 244.3 C608.683 237.667 607.4 230.217 607.4 221.95 C607.4 213.75 608.683 206.367 611.25 199.8 C613.817 193.233 617.45 187.65 622.15 183.05 C626.85 178.45 632.45 174.917 638.95 172.45 C645.45 169.983 652.667 168.75 660.6 168.75 Z M660.6 252.65 C667.733 252.65 672.983 250.133 676.35 245.1 C679.717 240.067 681.4 232.417 681.4 222.15 C681.4 211.883 679.717 204.25 676.35 199.25 C672.983 194.25 667.733 191.75 660.6 191.75 C653.267 191.75 647.9 194.25 644.5 199.25 C641.1 204.25 639.4 211.883 639.4 222.15 C639.4 232.417 641.1 240.067 644.5 245.1 C647.9 250.133 653.267 252.65 660.6 252.65 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="566.2" height="229.578" viewBox="0 0 566.2 229.578">
|
||||
<title>cobble-display.svg</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M81.6 70.6 C80.667 71.733 79.767 72.633 78.9 73.3 C78.033 73.967 76.8 74.3 75.2 74.3 C73.667 74.3 72.283 73.933 71.05 73.2 C69.817 72.467 68.433 71.65 66.9 70.75 C65.367 69.85 63.567 69.033 61.5 68.3 C59.433 67.567 56.867 67.2 53.8 67.2 C50 67.2 46.717 67.9 43.95 69.3 C41.183 70.7 38.9 72.7 37.1 75.3 C35.3 77.9 33.967 81.083 33.1 84.85 C32.233 88.617 31.8 92.867 31.8 97.6 C31.8 107.533 33.717 115.167 37.55 120.5 C41.383 125.833 46.667 128.5 53.4 128.5 C57 128.5 59.85 128.05 61.95 127.15 C64.05 126.25 65.833 125.25 67.3 124.15 C68.767 123.05 70.117 122.033 71.35 121.1 C72.583 120.167 74.133 119.7 76 119.7 C78.467 119.7 80.333 120.6 81.6 122.4 L90.6 133.5 C87.4 137.167 84.033 140.15 80.5 142.45 C76.967 144.75 73.35 146.55 69.65 147.85 C65.95 149.15 62.233 150.05 58.5 150.55 C54.767 151.05 51.133 151.3 47.6 151.3 C41.267 151.3 35.217 150.1 29.45 147.7 C23.683 145.3 18.617 141.817 14.25 137.25 C9.883 132.683 6.417 127.067 3.85 120.4 C1.283 113.733 0 106.133 0 97.6 C0 90.133 1.117 83.15 3.35 76.65 C5.583 70.15 8.883 64.517 13.25 59.75 C17.617 54.983 23.017 51.233 29.45 48.5 C35.883 45.767 43.333 44.4 51.8 44.4 C59.933 44.4 67.067 45.7 73.2 48.3 C79.333 50.9 84.867 54.7 89.8 59.7 Z" fill="#0e1116"/>
|
||||
<path d="M207.1 149.7 L207.1 0 L238.1 0 L238.1 57.1 C242.1 53.233 246.467 50.15 251.2 47.85 C255.933 45.55 261.267 44.4 267.2 44.4 C273.067 44.4 278.4 45.6 283.2 48 C288 50.4 292.1 53.817 295.5 58.25 C298.9 62.683 301.517 68.033 303.35 74.3 C305.183 80.567 306.1 87.567 306.1 95.3 C306.1 103.7 305.033 111.333 302.9 118.2 C300.767 125.067 297.75 130.95 293.85 135.85 C289.95 140.75 285.3 144.55 279.9 147.25 C274.5 149.95 268.533 151.3 262 151.3 C258.867 151.3 256.067 151 253.6 150.4 C251.133 149.8 248.867 148.95 246.8 147.85 C244.733 146.75 242.85 145.433 241.15 143.9 C239.45 142.367 237.8 140.7 236.2 138.9 L235.1 144.1 C234.567 146.233 233.7 147.7 232.5 148.5 C231.3 149.3 229.7 149.7 227.7 149.7 Z M256.3 67.7 C252.5 67.7 249.2 68.533 246.4 70.2 C243.6 71.867 240.833 74.3 238.1 77.5 L238.1 121.5 C240.433 124.1 242.967 125.917 245.7 126.95 C248.433 127.983 251.2 128.5 254 128.5 C256.933 128.5 259.633 127.95 262.1 126.85 C264.567 125.75 266.683 123.95 268.45 121.45 C270.217 118.95 271.6 115.65 272.6 111.55 C273.6 107.45 274.1 102.4 274.1 96.4 C274.1 91.2 273.717 86.783 272.95 83.15 C272.183 79.517 271.05 76.55 269.55 74.25 C268.05 71.95 266.183 70.283 263.95 69.25 C261.717 68.217 259.167 67.7 256.3 67.7 Z" fill="#0e1116"/>
|
||||
<path d="M317 149.7 L317 0 L348 0 L348 57.1 C352 53.233 356.367 50.15 361.1 47.85 C365.833 45.55 371.167 44.4 377.1 44.4 C382.967 44.4 388.3 45.6 393.1 48 C397.9 50.4 402 53.817 405.4 58.25 C408.8 62.683 411.417 68.033 413.25 74.3 C415.083 80.567 416 87.567 416 95.3 C416 103.7 414.933 111.333 412.8 118.2 C410.667 125.067 407.65 130.95 403.75 135.85 C399.85 140.75 395.2 144.55 389.8 147.25 C384.4 149.95 378.433 151.3 371.9 151.3 C368.767 151.3 365.967 151 363.5 150.4 C361.033 149.8 358.767 148.95 356.7 147.85 C354.633 146.75 352.75 145.433 351.05 143.9 C349.35 142.367 347.7 140.7 346.1 138.9 L345 144.1 C344.467 146.233 343.6 147.7 342.4 148.5 C341.2 149.3 339.6 149.7 337.6 149.7 Z M366.2 67.7 C362.4 67.7 359.1 68.533 356.3 70.2 C353.5 71.867 350.733 74.3 348 77.5 L348 121.5 C350.333 124.1 352.867 125.917 355.6 126.95 C358.333 127.983 361.1 128.5 363.9 128.5 C366.833 128.5 369.533 127.95 372 126.85 C374.467 125.75 376.583 123.95 378.35 121.45 C380.117 118.95 381.5 115.65 382.5 111.55 C383.5 107.45 384 102.4 384 96.4 C384 91.2 383.617 86.783 382.85 83.15 C382.083 79.517 380.95 76.55 379.45 74.25 C377.95 71.95 376.083 70.283 373.85 69.25 C371.617 68.217 369.067 67.7 366.2 67.7 Z" fill="#0e1116"/>
|
||||
<path d="M457.9 0 L457.9 149.7 L426.9 149.7 L426.9 0 Z" fill="#0e1116"/>
|
||||
<path d="M519.6 44.4 C526.467 44.4 532.75 45.467 538.45 47.6 C544.15 49.733 549.05 52.833 553.15 56.9 C557.25 60.967 560.45 65.95 562.75 71.85 C565.05 77.75 566.2 84.433 566.2 91.9 C566.2 94.233 566.1 96.133 565.9 97.6 C565.7 99.067 565.333 100.233 564.8 101.1 C564.267 101.967 563.55 102.567 562.65 102.9 C561.75 103.233 560.6 103.4 559.2 103.4 L499.4 103.4 C500.4 112.067 503.033 118.35 507.3 122.25 C511.567 126.15 517.067 128.1 523.8 128.1 C527.4 128.1 530.5 127.667 533.1 126.8 C535.7 125.933 538.017 124.967 540.05 123.9 C542.083 122.833 543.95 121.867 545.65 121 C547.35 120.133 549.133 119.7 551 119.7 C553.467 119.7 555.333 120.6 556.6 122.4 L565.6 133.5 C562.4 137.167 558.95 140.15 555.25 142.45 C551.55 144.75 547.75 146.55 543.85 147.85 C539.95 149.15 536.05 150.05 532.15 150.55 C528.25 151.05 524.533 151.3 521 151.3 C513.733 151.3 506.917 150.117 500.55 147.75 C494.183 145.383 488.617 141.867 483.85 137.2 C479.083 132.533 475.317 126.733 472.55 119.8 C469.783 112.867 468.4 104.8 468.4 95.6 C468.4 88.667 469.583 82.1 471.95 75.9 C474.317 69.7 477.717 64.267 482.15 59.6 C486.583 54.933 491.95 51.233 498.25 48.5 C504.55 45.767 511.667 44.4 519.6 44.4 Z M520.2 65.8 C514.267 65.8 509.633 67.483 506.3 70.85 C502.967 74.217 500.767 79.067 499.7 85.4 L538.4 85.4 C538.4 82.933 538.083 80.533 537.45 78.2 C536.817 75.867 535.783 73.783 534.35 71.95 C532.917 70.117 531.033 68.633 528.7 67.5 C526.367 66.367 523.533 65.8 520.2 65.8 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
<path id="o-pebble" d="M143.7 44.4 C151.567 44.4 158.733 45.633 165.2 48.1 C171.667 50.567 177.217 54.1 181.85 58.7 C186.483 63.3 188.383 66.226 190.999 71.656 C191.781 73.277 191 73.856 191 75.656 L191 119.782 C191 121.582 191.775 122.158 190.999 123.782 C188.388 129.253 186.483 132.217 181.85 136.85 C177.217 141.483 171.667 145.05 165.2 147.55 C158.733 150.05 151.567 151.3 143.7 151.3 C135.767 151.3 128.55 150.05 122.05 147.55 C115.55 145.05 109.95 141.483 105.25 136.85 C100.55 132.217 96.917 126.583 94.35 119.95 C91.783 113.317 90.5 105.867 90.5 97.6 C90.5 89.4 91.783 82.017 94.35 75.45 C96.917 68.883 100.55 63.3 105.25 58.7 C109.95 54.1 115.55 50.567 122.05 48.1 C128.55 45.633 135.767 44.4 143.7 44.4 Z M143.7 128.3 C150.833 128.3 156.083 125.783 159.45 120.75 C162.817 115.717 164.5 108.067 164.5 97.8 C164.5 87.533 162.817 79.9 159.45 74.9 C156.083 69.9 150.833 67.4 143.7 67.4 C136.367 67.4 131 69.9 127.6 74.9 C124.2 79.9 122.5 87.533 122.5 97.8 C122.5 108.067 124.2 115.717 127.6 120.75 C131 125.783 136.367 128.3 143.7 128.3 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M43.667 219.85 C43.204 219.85 42.847 219.774 42.598 219.623 C42.348 219.471 42.152 219.173 42.01 218.728 L41.475 216.189 C40.763 216.83 40.063 217.405 39.377 217.912 C38.691 218.42 37.979 218.848 37.239 219.195 C36.5 219.543 35.707 219.81 34.861 219.997 C34.015 220.184 33.075 220.278 32.041 220.278 C30.99 220.278 30.001 220.131 29.075 219.837 C28.148 219.543 27.342 219.102 26.656 218.514 C25.97 217.926 25.427 217.182 25.026 216.282 C24.625 215.382 24.425 214.318 24.425 213.089 C24.425 212.02 24.719 210.991 25.307 210.002 C25.895 209.013 26.848 208.136 28.166 207.369 C29.485 206.603 31.204 205.975 33.324 205.485 C35.444 204.995 38.046 204.715 41.128 204.643 L41.128 202.532 C41.128 200.43 40.674 198.84 39.765 197.762 C38.856 196.684 37.529 196.145 35.783 196.145 C34.607 196.145 33.623 196.292 32.83 196.586 C32.037 196.88 31.351 197.209 30.772 197.575 C30.193 197.94 29.694 198.27 29.275 198.564 C28.857 198.858 28.442 199.005 28.033 199.005 C27.712 199.005 27.436 198.92 27.204 198.751 C26.973 198.581 26.777 198.372 26.616 198.123 L25.761 196.599 C27.258 195.156 28.87 194.078 30.598 193.366 C32.326 192.653 34.242 192.297 36.344 192.297 C37.859 192.297 39.204 192.546 40.38 193.045 C41.556 193.544 42.544 194.239 43.346 195.129 C44.148 196.02 44.754 197.098 45.163 198.363 C45.573 199.628 45.778 201.018 45.778 202.532 L45.778 219.85 Z M33.458 216.937 C34.295 216.937 35.061 216.852 35.756 216.683 C36.451 216.514 37.106 216.273 37.72 215.962 C38.335 215.65 38.923 215.267 39.484 214.812 C40.046 214.358 40.593 213.846 41.128 213.276 L41.128 207.663 C38.936 207.735 37.07 207.908 35.529 208.185 C33.988 208.461 32.732 208.821 31.761 209.267 C30.79 209.712 30.086 210.238 29.65 210.844 C29.213 211.449 28.995 212.126 28.995 212.875 C28.995 213.587 29.111 214.202 29.342 214.719 C29.574 215.235 29.89 215.659 30.291 215.988 C30.692 216.318 31.164 216.558 31.707 216.71 C32.251 216.861 32.834 216.937 33.458 216.937 Z" fill="#5b6470"/>
|
||||
<path d="M86.667 206.006 C86.881 206.434 87.086 206.884 87.282 207.356 C87.478 207.828 87.674 208.296 87.87 208.759 C88.048 208.278 88.235 207.801 88.431 207.329 C88.627 206.857 88.841 206.407 89.073 205.98 L102.061 182.435 C102.275 182.025 102.511 181.776 102.769 181.687 C103.027 181.598 103.388 181.553 103.851 181.553 L107.673 181.553 L107.673 219.85 L103.13 219.85 L103.13 191.709 C103.13 191.334 103.139 190.934 103.157 190.506 C103.174 190.078 103.201 189.642 103.237 189.196 L90.115 213.142 C89.705 213.944 89.081 214.345 88.244 214.345 L87.496 214.345 C86.658 214.345 86.035 213.944 85.625 213.142 L72.209 189.116 C72.263 189.579 72.303 190.034 72.329 190.479 C72.356 190.925 72.369 191.334 72.369 191.709 L72.369 219.85 L67.826 219.85 L67.826 181.553 L71.648 181.553 C72.111 181.553 72.467 181.598 72.717 181.687 C72.966 181.776 73.207 182.025 73.438 182.435 Z" fill="#5b6470"/>
|
||||
<path d="M126.915 192.35 C128.893 192.35 130.674 192.68 132.26 193.339 C133.846 193.998 135.2 194.933 136.322 196.145 C137.445 197.356 138.304 198.822 138.901 200.541 C139.498 202.26 139.797 204.18 139.797 206.3 C139.797 208.438 139.498 210.363 138.901 212.073 C138.304 213.783 137.445 215.244 136.322 216.456 C135.2 217.667 133.846 218.598 132.26 219.249 C130.674 219.899 128.893 220.224 126.915 220.224 C124.92 220.224 123.125 219.899 121.53 219.249 C119.935 218.598 118.577 217.667 117.455 216.456 C116.332 215.244 115.472 213.783 114.876 212.073 C114.279 210.363 113.98 208.438 113.98 206.3 C113.98 204.18 114.279 202.26 114.876 200.541 C115.472 198.822 116.332 197.356 117.455 196.145 C118.577 194.933 119.935 193.998 121.53 193.339 C123.125 192.68 124.92 192.35 126.915 192.35 Z M126.915 216.509 C129.588 216.509 131.583 215.614 132.902 213.824 C134.22 212.033 134.879 209.534 134.879 206.327 C134.879 203.102 134.22 200.59 132.902 198.791 C131.583 196.991 129.588 196.091 126.915 196.091 C125.561 196.091 124.381 196.323 123.374 196.786 C122.367 197.25 121.53 197.918 120.862 198.791 C120.194 199.664 119.695 200.737 119.365 202.011 C119.036 203.285 118.871 204.724 118.871 206.327 C118.871 209.534 119.535 212.033 120.862 213.824 C122.189 215.614 124.207 216.509 126.915 216.509 Z" fill="#5b6470"/>
|
||||
<path d="M165.372 219.85 C164.695 219.85 164.268 219.52 164.09 218.861 L163.662 215.574 C162.504 216.982 161.181 218.108 159.693 218.955 C158.206 219.801 156.5 220.224 154.576 220.224 C153.025 220.224 151.618 219.926 150.353 219.329 C149.088 218.732 148.01 217.855 147.119 216.696 C146.228 215.538 145.542 214.095 145.061 212.367 C144.58 210.639 144.34 208.652 144.34 206.407 C144.34 204.412 144.607 202.554 145.142 200.835 C145.676 199.116 146.442 197.624 147.44 196.359 C148.438 195.094 149.663 194.1 151.115 193.379 C152.567 192.657 154.201 192.297 156.019 192.297 C157.676 192.297 159.097 192.577 160.281 193.138 C161.466 193.7 162.522 194.488 163.448 195.504 L163.448 180.484 L168.205 180.484 L168.205 219.85 Z M156.126 216.376 C157.693 216.376 159.061 216.019 160.228 215.307 C161.395 214.594 162.468 213.587 163.448 212.287 L163.448 199.218 C162.557 198.042 161.595 197.214 160.562 196.733 C159.529 196.252 158.37 196.011 157.088 196.011 C154.576 196.011 152.642 196.911 151.288 198.711 C149.934 200.51 149.257 203.076 149.257 206.407 C149.257 208.171 149.409 209.681 149.712 210.937 C150.014 212.193 150.46 213.227 151.048 214.037 C151.636 214.848 152.357 215.44 153.213 215.815 C154.068 216.189 155.039 216.376 156.126 216.376 Z" fill="#5b6470"/>
|
||||
<path d="M180.232 192.778 L180.232 210.042 C180.232 212.091 180.699 213.677 181.635 214.799 C182.57 215.921 184 216.483 185.924 216.483 C187.314 216.483 188.623 216.153 189.853 215.494 C191.082 214.835 192.213 213.917 193.247 212.741 L193.247 192.778 L198.004 192.778 L198.004 219.85 L195.171 219.85 C194.494 219.85 194.066 219.52 193.888 218.861 L193.514 215.948 C192.338 217.249 191.02 218.295 189.559 219.088 C188.098 219.881 186.423 220.278 184.534 220.278 C183.055 220.278 181.75 220.033 180.619 219.543 C179.488 219.053 178.539 218.362 177.773 217.471 C177.007 216.581 176.432 215.503 176.049 214.238 C175.666 212.973 175.474 211.574 175.474 210.042 L175.474 192.778 Z" fill="#5b6470"/>
|
||||
<path d="M210.458 180.484 L210.458 219.85 L205.7 219.85 L205.7 180.484 Z" fill="#5b6470"/>
|
||||
<path d="M229.245 192.35 C230.866 192.35 232.368 192.622 233.748 193.165 C235.129 193.708 236.323 194.492 237.329 195.517 C238.336 196.541 239.125 197.806 239.695 199.312 C240.265 200.817 240.55 202.532 240.55 204.456 C240.55 205.205 240.47 205.704 240.309 205.953 C240.149 206.202 239.846 206.327 239.401 206.327 L221.308 206.327 C221.343 208.038 221.575 209.525 222.003 210.79 C222.43 212.055 223.018 213.111 223.767 213.957 C224.515 214.803 225.406 215.436 226.439 215.855 C227.472 216.273 228.631 216.483 229.913 216.483 C231.107 216.483 232.136 216.345 233 216.068 C233.864 215.792 234.608 215.494 235.232 215.173 C235.855 214.852 236.376 214.554 236.795 214.278 C237.214 214.002 237.574 213.864 237.877 213.864 C238.269 213.864 238.572 214.015 238.786 214.318 L240.122 216.055 C239.534 216.768 238.831 217.387 238.011 217.912 C237.191 218.438 236.314 218.87 235.379 219.209 C234.443 219.547 233.477 219.801 232.479 219.97 C231.481 220.14 230.492 220.224 229.512 220.224 C227.642 220.224 225.918 219.908 224.341 219.275 C222.764 218.643 221.401 217.716 220.252 216.496 C219.103 215.276 218.208 213.766 217.566 211.966 C216.925 210.167 216.604 208.1 216.604 205.766 C216.604 203.877 216.894 202.114 217.473 200.474 C218.052 198.835 218.885 197.414 219.972 196.212 C221.058 195.009 222.386 194.065 223.954 193.379 C225.521 192.693 227.285 192.35 229.245 192.35 Z M229.352 195.851 C227.054 195.851 225.245 196.515 223.927 197.842 C222.608 199.169 221.789 201.009 221.468 203.361 L236.274 203.361 C236.274 202.256 236.118 201.245 235.806 200.327 C235.494 199.41 235.04 198.617 234.443 197.949 C233.846 197.281 233.12 196.764 232.265 196.399 C231.41 196.034 230.439 195.851 229.352 195.851 Z" fill="#5b6470"/>
|
||||
<path d="M254.42 220.278 C252.282 220.278 250.639 219.681 249.489 218.487 C248.34 217.293 247.766 215.574 247.766 213.329 L247.766 196.76 L244.505 196.76 C244.22 196.76 243.98 196.675 243.784 196.506 C243.588 196.336 243.49 196.074 243.49 195.717 L243.49 193.82 L247.926 193.259 L249.022 184.894 C249.075 184.626 249.195 184.408 249.382 184.239 C249.57 184.07 249.815 183.985 250.117 183.985 L252.523 183.985 L252.523 193.312 L260.353 193.312 L260.353 196.76 L252.523 196.76 L252.523 213.008 C252.523 214.149 252.799 214.995 253.351 215.547 C253.903 216.1 254.616 216.376 255.489 216.376 C255.988 216.376 256.42 216.309 256.785 216.175 C257.151 216.042 257.467 215.895 257.734 215.734 C258.001 215.574 258.228 215.427 258.416 215.293 C258.603 215.16 258.767 215.093 258.91 215.093 C259.159 215.093 259.382 215.244 259.578 215.547 L260.968 217.819 C260.148 218.585 259.159 219.186 258.001 219.623 C256.843 220.059 255.649 220.278 254.42 220.278 Z" fill="#5b6470"/>
|
||||
<path d="M265.27 219.85 L265.27 192.778 L267.996 192.778 C268.513 192.778 268.869 192.876 269.065 193.072 C269.261 193.268 269.395 193.606 269.466 194.087 L269.76 198.203 C270.615 196.35 271.671 194.902 272.927 193.86 C274.183 192.818 275.711 192.297 277.511 192.297 C278.081 192.297 278.629 192.359 279.154 192.484 C279.68 192.608 280.147 192.804 280.557 193.072 L280.21 196.626 C280.103 197.071 279.836 197.294 279.408 197.294 C279.159 197.294 278.793 197.241 278.312 197.134 C277.831 197.027 277.288 196.973 276.682 196.973 C275.827 196.973 275.061 197.103 274.384 197.361 C273.707 197.619 273.105 197.993 272.58 198.483 C272.054 198.973 271.586 199.575 271.177 200.287 C270.767 201 270.393 201.82 270.054 202.746 L270.054 219.85 Z" fill="#5b6470"/>
|
||||
<path d="M301.51 219.85 C301.046 219.85 300.69 219.774 300.441 219.623 C300.191 219.471 299.995 219.173 299.853 218.728 L299.318 216.189 C298.605 216.83 297.906 217.405 297.22 217.912 C296.534 218.42 295.822 218.848 295.082 219.195 C294.343 219.543 293.55 219.81 292.704 219.997 C291.857 220.184 290.918 220.278 289.884 220.278 C288.833 220.278 287.844 220.131 286.918 219.837 C285.991 219.543 285.185 219.102 284.499 218.514 C283.813 217.926 283.27 217.182 282.869 216.282 C282.468 215.382 282.268 214.318 282.268 213.089 C282.268 212.02 282.562 210.991 283.15 210.002 C283.737 209.013 284.691 208.136 286.009 207.369 C287.328 206.603 289.047 205.975 291.167 205.485 C293.287 204.995 295.888 204.715 298.971 204.643 L298.971 202.532 C298.971 200.43 298.516 198.84 297.608 197.762 C296.699 196.684 295.372 196.145 293.626 196.145 C292.45 196.145 291.465 196.292 290.673 196.586 C289.88 196.88 289.194 197.209 288.615 197.575 C288.036 197.94 287.537 198.27 287.118 198.564 C286.699 198.858 286.285 199.005 285.875 199.005 C285.555 199.005 285.279 198.92 285.047 198.751 C284.815 198.581 284.619 198.372 284.459 198.123 L283.604 196.599 C285.1 195.156 286.713 194.078 288.441 193.366 C290.169 192.653 292.085 192.297 294.187 192.297 C295.701 192.297 297.047 192.546 298.222 193.045 C299.398 193.544 300.387 194.239 301.189 195.129 C301.991 196.02 302.596 197.098 303.006 198.363 C303.416 199.628 303.621 201.018 303.621 202.532 L303.621 219.85 Z M291.301 216.937 C292.138 216.937 292.904 216.852 293.599 216.683 C294.294 216.514 294.949 216.273 295.563 215.962 C296.178 215.65 296.766 215.267 297.327 214.812 C297.888 214.358 298.436 213.846 298.971 213.276 L298.971 207.663 C296.779 207.735 294.913 207.908 293.372 208.185 C291.831 208.461 290.575 208.821 289.604 209.267 C288.633 209.712 287.929 210.238 287.492 210.844 C287.056 211.449 286.838 212.126 286.838 212.875 C286.838 213.587 286.953 214.202 287.185 214.719 C287.417 215.235 287.733 215.659 288.134 215.988 C288.535 216.318 289.007 216.558 289.55 216.71 C290.094 216.861 290.677 216.937 291.301 216.937 Z" fill="#5b6470"/>
|
||||
<path d="M329.704 197.588 C329.562 197.784 329.419 197.936 329.277 198.042 C329.134 198.149 328.938 198.203 328.689 198.203 C328.422 198.203 328.128 198.091 327.807 197.869 C327.486 197.646 327.09 197.401 326.618 197.134 C326.146 196.867 325.567 196.622 324.881 196.399 C324.195 196.176 323.353 196.065 322.355 196.065 C321.019 196.065 319.843 196.301 318.827 196.773 C317.812 197.245 316.961 197.927 316.275 198.817 C315.589 199.708 315.073 200.786 314.725 202.051 C314.378 203.316 314.204 204.733 314.204 206.3 C314.204 207.94 314.391 209.396 314.765 210.67 C315.139 211.944 315.669 213.013 316.355 213.877 C317.041 214.741 317.87 215.4 318.841 215.855 C319.812 216.309 320.903 216.536 322.115 216.536 C323.273 216.536 324.226 216.398 324.974 216.122 C325.722 215.846 326.342 215.538 326.832 215.2 C327.321 214.861 327.731 214.554 328.061 214.278 C328.39 214.002 328.716 213.864 329.036 213.864 C329.428 213.864 329.731 214.015 329.945 214.318 L331.281 216.055 C330.693 216.786 330.025 217.409 329.277 217.926 C328.529 218.442 327.722 218.875 326.858 219.222 C325.994 219.569 325.09 219.823 324.146 219.984 C323.201 220.144 322.239 220.224 321.259 220.224 C319.567 220.224 317.994 219.912 316.542 219.289 C315.09 218.665 313.83 217.761 312.761 216.576 C311.692 215.391 310.854 213.935 310.249 212.207 C309.643 210.478 309.34 208.51 309.34 206.3 C309.34 204.287 309.621 202.425 310.182 200.715 C310.743 199.005 311.563 197.53 312.641 196.292 C313.718 195.054 315.046 194.087 316.623 193.392 C318.199 192.697 320.012 192.35 322.061 192.35 C323.967 192.35 325.647 192.657 327.099 193.272 C328.551 193.887 329.838 194.755 330.961 195.878 Z" fill="#5b6470"/>
|
||||
<path d="M346.996 192.35 C348.617 192.35 350.118 192.622 351.499 193.165 C352.879 193.708 354.073 194.492 355.08 195.517 C356.086 196.541 356.875 197.806 357.445 199.312 C358.015 200.817 358.3 202.532 358.3 204.456 C358.3 205.205 358.22 205.704 358.06 205.953 C357.899 206.202 357.596 206.327 357.151 206.327 L339.058 206.327 C339.094 208.038 339.325 209.525 339.753 210.79 C340.181 212.055 340.769 213.111 341.517 213.957 C342.265 214.803 343.156 215.436 344.189 215.855 C345.223 216.273 346.381 216.483 347.664 216.483 C348.857 216.483 349.886 216.345 350.75 216.068 C351.614 215.792 352.358 215.494 352.982 215.173 C353.606 214.852 354.127 214.554 354.545 214.278 C354.964 214.002 355.325 213.864 355.628 213.864 C356.02 213.864 356.323 214.015 356.536 214.318 L357.873 216.055 C357.285 216.768 356.581 217.387 355.761 217.912 C354.942 218.438 354.064 218.87 353.129 219.209 C352.194 219.547 351.227 219.801 350.229 219.97 C349.232 220.14 348.243 220.224 347.263 220.224 C345.392 220.224 343.668 219.908 342.091 219.275 C340.515 218.643 339.152 217.716 338.003 216.496 C336.853 215.276 335.958 213.766 335.317 211.966 C334.675 210.167 334.355 208.1 334.355 205.766 C334.355 203.877 334.644 202.114 335.223 200.474 C335.802 198.835 336.635 197.414 337.722 196.212 C338.809 195.009 340.136 194.065 341.704 193.379 C343.272 192.693 345.036 192.35 346.996 192.35 Z M347.102 195.851 C344.804 195.851 342.996 196.515 341.677 197.842 C340.359 199.169 339.539 201.009 339.219 203.361 L354.024 203.361 C354.024 202.256 353.868 201.245 353.557 200.327 C353.245 199.41 352.79 198.617 352.194 197.949 C351.597 197.281 350.871 196.764 350.015 196.399 C349.16 196.034 348.189 195.851 347.102 195.851 Z" fill="#5b6470"/>
|
||||
<path d="M377.809 229.017 L377.809 192.778 L380.669 192.778 C381.346 192.778 381.765 193.107 381.925 193.766 L382.326 196.973 C383.484 195.566 384.811 194.435 386.308 193.579 C387.805 192.724 389.515 192.297 391.439 192.297 C393.007 192.297 394.423 192.595 395.688 193.192 C396.953 193.789 398.031 194.671 398.922 195.838 C399.813 197.005 400.499 198.452 400.98 200.18 C401.461 201.909 401.702 203.895 401.702 206.14 C401.702 208.136 401.434 209.993 400.9 211.712 C400.365 213.432 399.595 214.919 398.588 216.175 C397.581 217.431 396.352 218.42 394.9 219.142 C393.448 219.863 391.813 220.224 389.996 220.224 C388.339 220.224 386.918 219.948 385.733 219.396 C384.549 218.843 383.502 218.059 382.593 217.044 L382.593 229.017 Z M389.889 196.145 C388.339 196.145 386.981 196.501 385.814 197.214 C384.647 197.927 383.573 198.933 382.593 200.234 L382.593 213.329 C383.448 214.505 384.397 215.333 385.439 215.815 C386.482 216.296 387.644 216.536 388.927 216.536 C391.457 216.536 393.399 215.636 394.753 213.837 C396.107 212.037 396.784 209.472 396.784 206.14 C396.784 204.376 396.628 202.862 396.317 201.597 C396.005 200.332 395.555 199.294 394.967 198.483 C394.379 197.673 393.657 197.08 392.802 196.706 C391.947 196.332 390.976 196.145 389.889 196.145 Z" fill="#5b6470"/>
|
||||
<path d="M407.768 219.85 L407.768 192.778 L410.494 192.778 C411.011 192.778 411.367 192.876 411.563 193.072 C411.759 193.268 411.893 193.606 411.964 194.087 L412.258 198.203 C413.113 196.35 414.169 194.902 415.425 193.86 C416.681 192.818 418.209 192.297 420.008 192.297 C420.578 192.297 421.126 192.359 421.652 192.484 C422.177 192.608 422.645 192.804 423.055 193.072 L422.707 196.626 C422.601 197.071 422.333 197.294 421.906 197.294 C421.656 197.294 421.291 197.241 420.81 197.134 C420.329 197.027 419.786 196.973 419.18 196.973 C418.325 196.973 417.558 197.103 416.881 197.361 C416.204 197.619 415.603 197.993 415.077 198.483 C414.552 198.973 414.084 199.575 413.674 200.287 C413.265 201 412.89 201.82 412.552 202.746 L412.552 219.85 Z" fill="#5b6470"/>
|
||||
<path d="M437.54 192.35 C439.517 192.35 441.299 192.68 442.885 193.339 C444.471 193.998 445.825 194.933 446.947 196.145 C448.069 197.356 448.929 198.822 449.526 200.541 C450.123 202.26 450.421 204.18 450.421 206.3 C450.421 208.438 450.123 210.363 449.526 212.073 C448.929 213.783 448.069 215.244 446.947 216.456 C445.825 217.667 444.471 218.598 442.885 219.249 C441.299 219.899 439.517 220.224 437.54 220.224 C435.544 220.224 433.749 219.899 432.155 219.249 C430.56 218.598 429.202 217.667 428.079 216.456 C426.957 215.244 426.097 213.783 425.5 212.073 C424.903 210.363 424.605 208.438 424.605 206.3 C424.605 204.18 424.903 202.26 425.5 200.541 C426.097 198.822 426.957 197.356 428.079 196.145 C429.202 194.933 430.56 193.998 432.155 193.339 C433.749 192.68 435.544 192.35 437.54 192.35 Z M437.54 216.509 C440.212 216.509 442.208 215.614 443.526 213.824 C444.845 212.033 445.504 209.534 445.504 206.327 C445.504 203.102 444.845 200.59 443.526 198.791 C442.208 196.991 440.212 196.091 437.54 196.091 C436.186 196.091 435.005 196.323 433.999 196.786 C432.992 197.25 432.155 197.918 431.487 198.791 C430.818 199.664 430.32 200.737 429.99 202.011 C429.66 203.285 429.496 204.724 429.496 206.327 C429.496 209.534 430.159 212.033 431.487 213.824 C432.814 215.614 434.832 216.509 437.54 216.509 Z" fill="#5b6470"/>
|
||||
<path d="M461.459 192.778 L461.459 221.854 C461.459 222.941 461.316 223.952 461.031 224.888 C460.746 225.823 460.292 226.638 459.668 227.333 C459.045 228.028 458.247 228.576 457.276 228.977 C456.305 229.377 455.143 229.578 453.789 229.578 C453.218 229.578 452.693 229.533 452.212 229.444 C451.731 229.355 451.25 229.222 450.769 229.043 L450.956 226.478 C450.991 226.317 451.049 226.197 451.129 226.117 C451.21 226.037 451.321 225.988 451.464 225.97 C451.606 225.952 451.789 225.948 452.011 225.957 C452.234 225.966 452.515 225.97 452.853 225.97 C454.243 225.97 455.232 225.645 455.82 224.995 C456.408 224.344 456.702 223.298 456.702 221.854 L456.702 192.778 Z M462.448 184.279 C462.448 184.742 462.354 185.174 462.167 185.575 C461.98 185.976 461.73 186.332 461.419 186.644 C461.107 186.956 460.746 187.201 460.336 187.379 C459.926 187.557 459.49 187.646 459.027 187.646 C458.563 187.646 458.131 187.557 457.731 187.379 C457.33 187.201 456.978 186.956 456.675 186.644 C456.372 186.332 456.132 185.976 455.953 185.575 C455.775 185.174 455.686 184.742 455.686 184.279 C455.686 183.816 455.775 183.375 455.953 182.956 C456.132 182.537 456.372 182.172 456.675 181.86 C456.978 181.549 457.33 181.304 457.731 181.125 C458.131 180.947 458.563 180.858 459.027 180.858 C459.49 180.858 459.926 180.947 460.336 181.125 C460.746 181.304 461.107 181.549 461.419 181.86 C461.73 182.172 461.98 182.537 462.167 182.956 C462.354 183.375 462.448 183.816 462.448 184.279 Z" fill="#5b6470"/>
|
||||
<path d="M480.353 192.35 C481.975 192.35 483.476 192.622 484.856 193.165 C486.237 193.708 487.431 194.492 488.438 195.517 C489.444 196.541 490.233 197.806 490.803 199.312 C491.373 200.817 491.658 202.532 491.658 204.456 C491.658 205.205 491.578 205.704 491.417 205.953 C491.257 206.202 490.954 206.327 490.509 206.327 L472.416 206.327 C472.452 208.038 472.683 209.525 473.111 210.79 C473.538 212.055 474.126 213.111 474.875 213.957 C475.623 214.803 476.514 215.436 477.547 215.855 C478.581 216.273 479.739 216.483 481.021 216.483 C482.215 216.483 483.244 216.345 484.108 216.068 C484.972 215.792 485.716 215.494 486.34 215.173 C486.963 214.852 487.484 214.554 487.903 214.278 C488.322 214.002 488.683 213.864 488.985 213.864 C489.377 213.864 489.68 214.015 489.894 214.318 L491.23 216.055 C490.642 216.768 489.939 217.387 489.119 217.912 C488.3 218.438 487.422 218.87 486.487 219.209 C485.551 219.547 484.585 219.801 483.587 219.97 C482.589 220.14 481.6 220.224 480.621 220.224 C478.75 220.224 477.026 219.908 475.449 219.275 C473.872 218.643 472.509 217.716 471.36 216.496 C470.211 215.276 469.316 213.766 468.674 211.966 C468.033 210.167 467.712 208.1 467.712 205.766 C467.712 203.877 468.002 202.114 468.581 200.474 C469.16 198.835 469.993 197.414 471.08 196.212 C472.167 195.009 473.494 194.065 475.062 193.379 C476.63 192.693 478.393 192.35 480.353 192.35 Z M480.46 195.851 C478.162 195.851 476.353 196.515 475.035 197.842 C473.717 199.169 472.897 201.009 472.576 203.361 L487.382 203.361 C487.382 202.256 487.226 201.245 486.914 200.327 C486.602 199.41 486.148 198.617 485.551 197.949 C484.954 197.281 484.228 196.764 483.373 196.399 C482.518 196.034 481.547 195.851 480.46 195.851 Z" fill="#5b6470"/>
|
||||
<path d="M516.325 197.588 C516.183 197.784 516.04 197.936 515.898 198.042 C515.755 198.149 515.559 198.203 515.31 198.203 C515.042 198.203 514.748 198.091 514.428 197.869 C514.107 197.646 513.711 197.401 513.238 197.134 C512.766 196.867 512.187 196.622 511.501 196.399 C510.815 196.176 509.973 196.065 508.976 196.065 C507.64 196.065 506.464 196.301 505.448 196.773 C504.433 197.245 503.582 197.927 502.896 198.817 C502.21 199.708 501.693 200.786 501.346 202.051 C500.998 203.316 500.825 204.733 500.825 206.3 C500.825 207.94 501.012 209.396 501.386 210.67 C501.76 211.944 502.29 213.013 502.976 213.877 C503.662 214.741 504.49 215.4 505.461 215.855 C506.432 216.309 507.524 216.536 508.735 216.536 C509.893 216.536 510.847 216.398 511.595 216.122 C512.343 215.846 512.962 215.538 513.452 215.2 C513.942 214.861 514.352 214.554 514.682 214.278 C515.011 214.002 515.336 213.864 515.657 213.864 C516.049 213.864 516.352 214.015 516.566 214.318 L517.902 216.055 C517.314 216.786 516.646 217.409 515.898 217.926 C515.149 218.442 514.343 218.875 513.479 219.222 C512.615 219.569 511.711 219.823 510.766 219.984 C509.822 220.144 508.86 220.224 507.88 220.224 C506.187 220.224 504.615 219.912 503.163 219.289 C501.711 218.665 500.45 217.761 499.381 216.576 C498.312 215.391 497.475 213.935 496.869 212.207 C496.264 210.478 495.961 208.51 495.961 206.3 C495.961 204.287 496.241 202.425 496.803 200.715 C497.364 199.005 498.183 197.53 499.261 196.292 C500.339 195.054 501.666 194.087 503.243 193.392 C504.82 192.697 506.633 192.35 508.682 192.35 C510.588 192.35 512.267 192.657 513.719 193.272 C515.171 193.887 516.459 194.755 517.581 195.878 Z" fill="#5b6470"/>
|
||||
<path d="M531.318 220.278 C529.18 220.278 527.536 219.681 526.387 218.487 C525.238 217.293 524.663 215.574 524.663 213.329 L524.663 196.76 L521.403 196.76 C521.118 196.76 520.877 196.675 520.681 196.506 C520.485 196.336 520.387 196.074 520.387 195.717 L520.387 193.82 L524.824 193.259 L525.919 184.894 C525.973 184.626 526.093 184.408 526.28 184.239 C526.467 184.07 526.712 183.985 527.015 183.985 L529.42 183.985 L529.42 193.312 L537.251 193.312 L537.251 196.76 L529.42 196.76 L529.42 213.008 C529.42 214.149 529.697 214.995 530.249 215.547 C530.801 216.1 531.514 216.376 532.387 216.376 C532.886 216.376 533.318 216.309 533.683 216.175 C534.048 216.042 534.365 215.895 534.632 215.734 C534.899 215.574 535.126 215.427 535.313 215.293 C535.5 215.16 535.665 215.093 535.808 215.093 C536.057 215.093 536.28 215.244 536.476 215.547 L537.865 217.819 C537.046 218.585 536.057 219.186 534.899 219.623 C533.741 220.059 532.547 220.278 531.318 220.278 Z" fill="#5b6470"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1600" height="400" viewBox="0 0 1600 400">
|
||||
<title>cobble-horizontal</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<g>
|
||||
<g>
|
||||
<path id="body" d="M567.406 233.03 C566.7 234.716 565.938 236.373 565.122 238.001 C564.304 239.63 563.435 241.23 562.51 242.797 C561.587 244.364 560.611 245.898 559.583 247.398 C558.555 248.898 557.478 250.362 556.35 251.79 C555.224 253.217 554.048 254.606 552.827 255.955 C551.604 257.304 550.335 258.614 549.022 259.879 C547.708 261.146 546.351 262.369 544.952 263.549 C543.554 264.726 542.113 265.861 540.634 266.947 C539.154 268.032 537.636 269.071 536.081 270.061 C534.526 271.052 532.936 271.993 531.313 272.883 C529.689 273.773 528.03 274.611 526.343 275.396 C524.656 276.184 522.937 276.916 521.192 277.596 C519.446 278.277 517.672 278.902 515.874 279.471 C514.075 280.043 512.252 280.557 510.405 281.016 C508.56 281.474 506.691 281.877 504.8 282.223 C502.912 282.568 501 282.856 499.067 283.087 C497.135 283.317 495.181 283.491 493.196 283.607 C491.21 283.721 489.345 283.778 487.046 283.778 C484.746 283.778 482.898 283.717 480.912 283.602 C478.928 283.484 476.975 283.31 475.043 283.077 C473.111 282.844 471.201 282.554 469.311 282.208 C467.421 281.861 465.554 281.457 463.708 280.996 C461.863 280.537 460.04 280.019 458.242 279.448 C456.444 278.877 454.671 278.25 452.927 277.568 C451.181 276.888 449.464 276.152 447.776 275.365 C446.089 274.577 444.434 273.738 442.81 272.846 C441.188 271.955 439.598 271.013 438.045 270.021 C436.49 269.029 434.974 267.989 433.496 266.901 C432.016 265.814 430.577 264.68 429.18 263.5 C427.782 262.32 426.427 261.096 425.115 259.827 C423.803 258.56 422.534 257.25 421.313 255.9 C420.093 254.549 418.918 253.158 417.793 251.731 C416.667 250.303 415.589 248.837 414.564 247.336 C413.538 245.836 412.563 244.3 411.642 242.732 C410.718 241.165 409.85 239.565 409.034 237.934 C408.22 236.306 407.459 234.647 406.756 232.961 C406.05 231.276 405.402 229.563 404.81 227.828 C404.219 226.09 403.686 224.331 403.21 222.548 C402.734 220.765 402.317 218.961 401.959 217.136 C401.6 215.311 401.3 213.466 401.06 211.6 C400.821 209.733 400.64 207.847 400.521 205.931 C400.4 204.015 400.34 202.733 400.34 200 C400.34 197.267 400.4 195.985 400.521 194.069 C400.64 192.153 400.821 190.267 401.06 188.4 C401.3 186.534 401.6 184.689 401.959 182.864 C402.317 181.039 402.734 179.235 403.21 177.452 C403.686 175.669 404.219 173.91 404.81 172.172 C405.402 170.437 406.05 168.724 406.756 167.039 C407.459 165.353 408.22 163.694 409.034 162.066 C409.85 160.435 410.718 158.835 411.642 157.268 C412.563 155.7 413.538 154.164 414.564 152.664 C415.589 151.163 416.667 149.697 417.793 148.269 C418.918 146.842 420.093 145.451 421.313 144.1 C422.534 142.75 423.803 141.44 425.115 140.173 C426.427 138.904 427.782 137.68 429.18 136.5 C430.577 135.32 432.016 134.186 433.496 133.099 C434.974 132.011 436.49 130.971 438.045 129.979 C439.598 128.987 441.188 128.045 442.81 127.154 C444.434 126.262 446.089 125.423 447.776 124.635 C449.464 123.848 451.181 123.112 452.927 122.432 C454.671 121.75 456.444 121.123 458.242 120.552 C460.04 119.981 461.863 119.463 463.708 119.004 C465.554 118.543 467.421 118.139 469.311 117.792 C471.201 117.446 473.111 117.156 475.043 116.923 C476.975 116.69 478.928 116.516 480.912 116.398 C482.898 116.283 484.746 116.223 487.046 116.223 C489.345 116.223 491.21 116.279 493.196 116.393 C495.181 116.509 497.135 116.683 499.067 116.913 C501 117.144 502.912 117.432 504.8 117.777 C506.691 118.123 508.56 118.526 510.405 118.984 C512.252 119.443 514.075 119.957 515.874 120.529 C517.672 121.098 519.446 121.723 521.192 122.404 C522.937 123.084 524.656 123.816 526.343 124.604 C528.03 125.389 529.689 126.227 531.313 127.117 C532.936 128.007 534.526 128.948 536.081 129.939 C537.636 130.929 539.154 131.968 540.634 133.053 C542.113 134.139 543.554 135.274 544.952 136.451 C546.351 137.631 547.708 138.854 549.022 140.121 C550.335 141.386 551.604 142.696 552.827 144.045 C554.048 145.394 555.224 146.783 556.35 148.21 C557.478 149.638 558.555 151.102 559.583 152.602 C560.611 154.102 561.587 155.636 562.51 157.203 C563.435 158.77 564.304 160.37 565.122 161.999 C565.938 163.627 566.7 165.284 567.406 166.97 C567.722 167.762 567.895 168.604 567.895 169.456 L567.895 230.544 C567.895 231.396 567.722 232.238 567.406 233.03 Z" fill="#e4572e"/>
|
||||
<path id="eye-l" d="M445.999 186.596 C445.999 181.274 450.312 176.961 455.633 176.961 C460.955 176.961 465.268 181.274 465.268 186.596 C465.268 191.917 460.955 196.23 455.633 196.23 C450.312 196.23 445.999 191.917 445.999 186.596 Z" fill="#0e1116"/>
|
||||
<path id="eye-r" d="M502.967 186.596 C502.967 181.274 507.28 176.961 512.602 176.961 C517.923 176.961 522.236 181.274 522.236 186.596 C522.236 191.917 517.923 196.23 512.602 196.23 C507.28 196.23 502.967 191.917 502.967 186.596 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M715.06 193.75 C714.127 194.883 713.227 195.783 712.36 196.45 C711.493 197.117 710.26 197.45 708.66 197.45 C707.127 197.45 705.743 197.083 704.51 196.35 C703.277 195.617 701.893 194.8 700.36 193.9 C698.827 193 697.027 192.183 694.96 191.45 C692.893 190.717 690.327 190.35 687.26 190.35 C683.46 190.35 680.177 191.05 677.41 192.45 C674.643 193.85 672.36 195.85 670.56 198.45 C668.76 201.05 667.427 204.233 666.56 208 C665.693 211.767 665.26 216.017 665.26 220.75 C665.26 230.683 667.177 238.317 671.01 243.65 C674.843 248.983 680.127 251.65 686.86 251.65 C690.46 251.65 693.31 251.2 695.41 250.3 C697.51 249.4 699.293 248.4 700.76 247.3 C702.227 246.2 703.577 245.183 704.81 244.25 C706.043 243.317 707.593 242.85 709.46 242.85 C711.927 242.85 713.793 243.75 715.06 245.55 L724.06 256.65 C720.86 260.317 717.493 263.3 713.96 265.6 C710.427 267.9 706.81 269.7 703.11 271 C699.41 272.3 695.693 273.2 691.96 273.7 C688.227 274.2 684.593 274.45 681.06 274.45 C674.727 274.45 668.677 273.25 662.91 270.85 C657.143 268.45 652.077 264.967 647.71 260.4 C643.343 255.833 639.877 250.217 637.31 243.55 C634.743 236.883 633.46 229.283 633.46 220.75 C633.46 213.283 634.577 206.3 636.81 199.8 C639.043 193.3 642.343 187.667 646.71 182.9 C651.077 178.133 656.477 174.383 662.91 171.65 C669.343 168.917 676.793 167.55 685.26 167.55 C693.393 167.55 700.527 168.85 706.66 171.45 C712.793 174.05 718.327 177.85 723.26 182.85 Z" fill="#0e1116"/>
|
||||
<path d="M840.56 272.85 L840.56 123.15 L871.56 123.15 L871.56 180.25 C875.56 176.383 879.927 173.3 884.66 171 C889.393 168.7 894.727 167.55 900.66 167.55 C906.527 167.55 911.86 168.75 916.66 171.15 C921.46 173.55 925.56 176.967 928.96 181.4 C932.36 185.833 934.977 191.183 936.81 197.45 C938.643 203.717 939.56 210.717 939.56 218.45 C939.56 226.85 938.493 234.483 936.36 241.35 C934.227 248.217 931.21 254.1 927.31 259 C923.41 263.9 918.76 267.7 913.36 270.4 C907.96 273.1 901.993 274.45 895.46 274.45 C892.327 274.45 889.527 274.15 887.06 273.55 C884.593 272.95 882.327 272.1 880.26 271 C878.193 269.9 876.31 268.583 874.61 267.05 C872.91 265.517 871.26 263.85 869.66 262.05 L868.56 267.25 C868.027 269.383 867.16 270.85 865.96 271.65 C864.76 272.45 863.16 272.85 861.16 272.85 Z M889.76 190.85 C885.96 190.85 882.66 191.683 879.86 193.35 C877.06 195.017 874.293 197.45 871.56 200.65 L871.56 244.65 C873.893 247.25 876.427 249.067 879.16 250.1 C881.893 251.133 884.66 251.65 887.46 251.65 C890.393 251.65 893.093 251.1 895.56 250 C898.027 248.9 900.143 247.1 901.91 244.6 C903.677 242.1 905.06 238.8 906.06 234.7 C907.06 230.6 907.56 225.55 907.56 219.55 C907.56 214.35 907.177 209.933 906.41 206.3 C905.643 202.667 904.51 199.7 903.01 197.4 C901.51 195.1 899.643 193.433 897.41 192.4 C895.177 191.367 892.627 190.85 889.76 190.85 Z" fill="#0e1116"/>
|
||||
<path d="M950.46 272.85 L950.46 123.15 L981.46 123.15 L981.46 180.25 C985.46 176.383 989.827 173.3 994.56 171 C999.293 168.7 1004.627 167.55 1010.56 167.55 C1016.427 167.55 1021.76 168.75 1026.56 171.15 C1031.36 173.55 1035.46 176.967 1038.86 181.4 C1042.26 185.833 1044.877 191.183 1046.71 197.45 C1048.543 203.717 1049.46 210.717 1049.46 218.45 C1049.46 226.85 1048.393 234.483 1046.26 241.35 C1044.127 248.217 1041.11 254.1 1037.21 259 C1033.31 263.9 1028.66 267.7 1023.26 270.4 C1017.86 273.1 1011.893 274.45 1005.36 274.45 C1002.227 274.45 999.427 274.15 996.96 273.55 C994.493 272.95 992.227 272.1 990.16 271 C988.093 269.9 986.21 268.583 984.51 267.05 C982.81 265.517 981.16 263.85 979.56 262.05 L978.46 267.25 C977.927 269.383 977.06 270.85 975.86 271.65 C974.66 272.45 973.06 272.85 971.06 272.85 Z M999.66 190.85 C995.86 190.85 992.56 191.683 989.76 193.35 C986.96 195.017 984.193 197.45 981.46 200.65 L981.46 244.65 C983.793 247.25 986.327 249.067 989.06 250.1 C991.793 251.133 994.56 251.65 997.36 251.65 C1000.293 251.65 1002.993 251.1 1005.46 250 C1007.927 248.9 1010.043 247.1 1011.81 244.6 C1013.577 242.1 1014.96 238.8 1015.96 234.7 C1016.96 230.6 1017.46 225.55 1017.46 219.55 C1017.46 214.35 1017.077 209.933 1016.31 206.3 C1015.543 202.667 1014.41 199.7 1012.91 197.4 C1011.41 195.1 1009.543 193.433 1007.31 192.4 C1005.077 191.367 1002.527 190.85 999.66 190.85 Z" fill="#0e1116"/>
|
||||
<path d="M1091.36 123.15 L1091.36 272.85 L1060.36 272.85 L1060.36 123.15 Z" fill="#0e1116"/>
|
||||
<path d="M1153.06 167.55 C1159.927 167.55 1166.21 168.617 1171.91 170.75 C1177.61 172.883 1182.51 175.983 1186.61 180.05 C1190.71 184.117 1193.91 189.1 1196.21 195 C1198.51 200.9 1199.66 207.583 1199.66 215.05 C1199.66 217.383 1199.56 219.283 1199.36 220.75 C1199.16 222.217 1198.793 223.383 1198.26 224.25 C1197.727 225.117 1197.01 225.717 1196.11 226.05 C1195.21 226.383 1194.06 226.55 1192.66 226.55 L1132.86 226.55 C1133.86 235.217 1136.493 241.5 1140.76 245.4 C1145.027 249.3 1150.527 251.25 1157.26 251.25 C1160.86 251.25 1163.96 250.817 1166.56 249.95 C1169.16 249.083 1171.477 248.117 1173.51 247.05 C1175.543 245.983 1177.41 245.017 1179.11 244.15 C1180.81 243.283 1182.593 242.85 1184.46 242.85 C1186.927 242.85 1188.793 243.75 1190.06 245.55 L1199.06 256.65 C1195.86 260.317 1192.41 263.3 1188.71 265.6 C1185.01 267.9 1181.21 269.7 1177.31 271 C1173.41 272.3 1169.51 273.2 1165.61 273.7 C1161.71 274.2 1157.993 274.45 1154.46 274.45 C1147.193 274.45 1140.377 273.267 1134.01 270.9 C1127.643 268.533 1122.077 265.017 1117.31 260.35 C1112.543 255.683 1108.777 249.883 1106.01 242.95 C1103.243 236.017 1101.86 227.95 1101.86 218.75 C1101.86 211.817 1103.043 205.25 1105.41 199.05 C1107.777 192.85 1111.177 187.417 1115.61 182.75 C1120.043 178.083 1125.41 174.383 1131.71 171.65 C1138.01 168.917 1145.127 167.55 1153.06 167.55 Z M1153.66 188.95 C1147.727 188.95 1143.093 190.633 1139.76 194 C1136.427 197.367 1134.227 202.217 1133.16 208.55 L1171.86 208.55 C1171.86 206.083 1171.543 203.683 1170.91 201.35 C1170.277 199.017 1169.243 196.933 1167.81 195.1 C1166.377 193.267 1164.493 191.783 1162.16 190.65 C1159.827 189.517 1156.993 188.95 1153.66 188.95 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
<path id="o-pebble" d="M777.16 167.55 C785.027 167.55 792.193 168.783 798.66 171.25 C805.127 173.717 810.677 177.25 815.31 181.85 C819.943 186.45 821.843 189.376 824.459 194.806 C825.241 196.427 824.46 197.006 824.46 198.806 L824.46 242.932 C824.46 244.732 825.235 245.308 824.459 246.932 C821.848 252.403 819.943 255.367 815.31 260 C810.677 264.633 805.127 268.2 798.66 270.7 C792.193 273.2 785.027 274.45 777.16 274.45 C769.227 274.45 762.01 273.2 755.51 270.7 C749.01 268.2 743.41 264.633 738.71 260 C734.01 255.367 730.377 249.733 727.81 243.1 C725.243 236.467 723.96 229.017 723.96 220.75 C723.96 212.55 725.243 205.167 727.81 198.6 C730.377 192.033 734.01 186.45 738.71 181.85 C743.41 177.25 749.01 173.717 755.51 171.25 C762.01 168.783 769.227 167.55 777.16 167.55 Z M777.16 251.45 C784.293 251.45 789.543 248.933 792.91 243.9 C796.277 238.867 797.96 231.217 797.96 220.95 C797.96 210.683 796.277 203.05 792.91 198.05 C789.543 193.05 784.293 190.55 777.16 190.55 C769.827 190.55 764.46 193.05 761.06 198.05 C757.66 203.05 755.96 210.683 755.96 220.95 C755.96 231.217 757.66 238.867 761.06 243.9 C764.46 248.933 769.827 251.45 777.16 251.45 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1600" height="400" viewBox="0 0 1600 400">
|
||||
<title>cobble-plain</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<g>
|
||||
<path d="M598.5 194.95 C597.567 196.083 596.667 196.983 595.8 197.65 C594.933 198.317 593.7 198.65 592.1 198.65 C590.567 198.65 589.183 198.283 587.95 197.55 C586.717 196.817 585.333 196 583.8 195.1 C582.267 194.2 580.467 193.383 578.4 192.65 C576.333 191.917 573.767 191.55 570.7 191.55 C566.9 191.55 563.617 192.25 560.85 193.65 C558.083 195.05 555.8 197.05 554 199.65 C552.2 202.25 550.867 205.433 550 209.2 C549.133 212.967 548.7 217.217 548.7 221.95 C548.7 231.883 550.617 239.517 554.45 244.85 C558.283 250.183 563.567 252.85 570.3 252.85 C573.9 252.85 576.75 252.4 578.85 251.5 C580.95 250.6 582.733 249.6 584.2 248.5 C585.667 247.4 587.017 246.383 588.25 245.45 C589.483 244.517 591.033 244.05 592.9 244.05 C595.367 244.05 597.233 244.95 598.5 246.75 L607.5 257.85 C604.3 261.517 600.933 264.5 597.4 266.8 C593.867 269.1 590.25 270.9 586.55 272.2 C582.85 273.5 579.133 274.4 575.4 274.9 C571.667 275.4 568.033 275.65 564.5 275.65 C558.167 275.65 552.117 274.45 546.35 272.05 C540.583 269.65 535.517 266.167 531.15 261.6 C526.783 257.033 523.317 251.417 520.75 244.75 C518.183 238.083 516.9 230.483 516.9 221.95 C516.9 214.483 518.017 207.5 520.25 201 C522.483 194.5 525.783 188.867 530.15 184.1 C534.517 179.333 539.917 175.583 546.35 172.85 C552.783 170.117 560.233 168.75 568.7 168.75 C576.833 168.75 583.967 170.05 590.1 172.65 C596.233 175.25 601.767 179.05 606.7 184.05 Z" fill="#0e1116"/>
|
||||
<path d="M660.6 168.75 C668.467 168.75 675.633 169.983 682.1 172.45 C688.567 174.917 694.117 178.45 698.75 183.05 C703.383 187.65 706.983 193.233 709.55 199.8 C712.117 206.367 713.4 213.75 713.4 221.95 C713.4 230.217 712.117 237.667 709.55 244.3 C706.983 250.933 703.383 256.567 698.75 261.2 C694.117 265.833 688.567 269.4 682.1 271.9 C675.633 274.4 668.467 275.65 660.6 275.65 C652.667 275.65 645.45 274.4 638.95 271.9 C632.45 269.4 626.85 265.833 622.15 261.2 C617.45 256.567 613.817 250.933 611.25 244.3 C608.683 237.667 607.4 230.217 607.4 221.95 C607.4 213.75 608.683 206.367 611.25 199.8 C613.817 193.233 617.45 187.65 622.15 183.05 C626.85 178.45 632.45 174.917 638.95 172.45 C645.45 169.983 652.667 168.75 660.6 168.75 Z M660.6 252.65 C667.733 252.65 672.983 250.133 676.35 245.1 C679.717 240.067 681.4 232.417 681.4 222.15 C681.4 211.883 679.717 204.25 676.35 199.25 C672.983 194.25 667.733 191.75 660.6 191.75 C653.267 191.75 647.9 194.25 644.5 199.25 C641.1 204.25 639.4 211.883 639.4 222.15 C639.4 232.417 641.1 240.067 644.5 245.1 C647.9 250.133 653.267 252.65 660.6 252.65 Z" fill="#0e1116"/>
|
||||
<path d="M724 274.05 L724 124.35 L755 124.35 L755 181.45 C759 177.583 763.367 174.5 768.1 172.2 C772.833 169.9 778.167 168.75 784.1 168.75 C789.967 168.75 795.3 169.95 800.1 172.35 C804.9 174.75 809 178.167 812.4 182.6 C815.8 187.033 818.417 192.383 820.25 198.65 C822.083 204.917 823 211.917 823 219.65 C823 228.05 821.933 235.683 819.8 242.55 C817.667 249.417 814.65 255.3 810.75 260.2 C806.85 265.1 802.2 268.9 796.8 271.6 C791.4 274.3 785.433 275.65 778.9 275.65 C775.767 275.65 772.967 275.35 770.5 274.75 C768.033 274.15 765.767 273.3 763.7 272.2 C761.633 271.1 759.75 269.783 758.05 268.25 C756.35 266.717 754.7 265.05 753.1 263.25 L752 268.45 C751.467 270.583 750.6 272.05 749.4 272.85 C748.2 273.65 746.6 274.05 744.6 274.05 Z M773.2 192.05 C769.4 192.05 766.1 192.883 763.3 194.55 C760.5 196.217 757.733 198.65 755 201.85 L755 245.85 C757.333 248.45 759.867 250.267 762.6 251.3 C765.333 252.333 768.1 252.85 770.9 252.85 C773.833 252.85 776.533 252.3 779 251.2 C781.467 250.1 783.583 248.3 785.35 245.8 C787.117 243.3 788.5 240 789.5 235.9 C790.5 231.8 791 226.75 791 220.75 C791 215.55 790.617 211.133 789.85 207.5 C789.083 203.867 787.95 200.9 786.45 198.6 C784.95 196.3 783.083 194.633 780.85 193.6 C778.617 192.567 776.067 192.05 773.2 192.05 Z" fill="#0e1116"/>
|
||||
<path d="M833.9 274.05 L833.9 124.35 L864.9 124.35 L864.9 181.45 C868.9 177.583 873.267 174.5 878 172.2 C882.733 169.9 888.067 168.75 894 168.75 C899.867 168.75 905.2 169.95 910 172.35 C914.8 174.75 918.9 178.167 922.3 182.6 C925.7 187.033 928.317 192.383 930.15 198.65 C931.983 204.917 932.9 211.917 932.9 219.65 C932.9 228.05 931.833 235.683 929.7 242.55 C927.567 249.417 924.55 255.3 920.65 260.2 C916.75 265.1 912.1 268.9 906.7 271.6 C901.3 274.3 895.333 275.65 888.8 275.65 C885.667 275.65 882.867 275.35 880.4 274.75 C877.933 274.15 875.667 273.3 873.6 272.2 C871.533 271.1 869.65 269.783 867.95 268.25 C866.25 266.717 864.6 265.05 863 263.25 L861.9 268.45 C861.367 270.583 860.5 272.05 859.3 272.85 C858.1 273.65 856.5 274.05 854.5 274.05 Z M883.1 192.05 C879.3 192.05 876 192.883 873.2 194.55 C870.4 196.217 867.633 198.65 864.9 201.85 L864.9 245.85 C867.233 248.45 869.767 250.267 872.5 251.3 C875.233 252.333 878 252.85 880.8 252.85 C883.733 252.85 886.433 252.3 888.9 251.2 C891.367 250.1 893.483 248.3 895.25 245.8 C897.017 243.3 898.4 240 899.4 235.9 C900.4 231.8 900.9 226.75 900.9 220.75 C900.9 215.55 900.517 211.133 899.75 207.5 C898.983 203.867 897.85 200.9 896.35 198.6 C894.85 196.3 892.983 194.633 890.75 193.6 C888.517 192.567 885.967 192.05 883.1 192.05 Z" fill="#0e1116"/>
|
||||
<path d="M974.8 124.35 L974.8 274.05 L943.8 274.05 L943.8 124.35 Z" fill="#0e1116"/>
|
||||
<path d="M1036.5 168.75 C1043.367 168.75 1049.65 169.817 1055.35 171.95 C1061.05 174.083 1065.95 177.183 1070.05 181.25 C1074.15 185.317 1077.35 190.3 1079.65 196.2 C1081.95 202.1 1083.1 208.783 1083.1 216.25 C1083.1 218.583 1083 220.483 1082.8 221.95 C1082.6 223.417 1082.233 224.583 1081.7 225.45 C1081.167 226.317 1080.45 226.917 1079.55 227.25 C1078.65 227.583 1077.5 227.75 1076.1 227.75 L1016.3 227.75 C1017.3 236.417 1019.933 242.7 1024.2 246.6 C1028.467 250.5 1033.967 252.45 1040.7 252.45 C1044.3 252.45 1047.4 252.017 1050 251.15 C1052.6 250.283 1054.917 249.317 1056.95 248.25 C1058.983 247.183 1060.85 246.217 1062.55 245.35 C1064.25 244.483 1066.033 244.05 1067.9 244.05 C1070.367 244.05 1072.233 244.95 1073.5 246.75 L1082.5 257.85 C1079.3 261.517 1075.85 264.5 1072.15 266.8 C1068.45 269.1 1064.65 270.9 1060.75 272.2 C1056.85 273.5 1052.95 274.4 1049.05 274.9 C1045.15 275.4 1041.433 275.65 1037.9 275.65 C1030.633 275.65 1023.817 274.467 1017.45 272.1 C1011.083 269.733 1005.517 266.217 1000.75 261.55 C995.983 256.883 992.217 251.083 989.45 244.15 C986.683 237.217 985.3 229.15 985.3 219.95 C985.3 213.017 986.483 206.45 988.85 200.25 C991.217 194.05 994.617 188.617 999.05 183.95 C1003.483 179.283 1008.85 175.583 1015.15 172.85 C1021.45 170.117 1028.567 168.75 1036.5 168.75 Z M1037.1 190.15 C1031.167 190.15 1026.533 191.833 1023.2 195.2 C1019.867 198.567 1017.667 203.417 1016.6 209.75 L1055.3 209.75 C1055.3 207.283 1054.983 204.883 1054.35 202.55 C1053.717 200.217 1052.683 198.133 1051.25 196.3 C1049.817 194.467 1047.933 192.983 1045.6 191.85 C1043.267 190.717 1040.433 190.15 1037.1 190.15 Z" fill="#0e1116"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,60 @@
|
||||
import type { BotFace } from "@/src/dict/bot-dict";
|
||||
import { StoneEyes } from "@/src/data/stone-geometry";
|
||||
import StoneEyeDot from "./stone-eye-dot";
|
||||
import StoneEyeLidded from "./stone-eye-lidded";
|
||||
import StoneEyeArc from "./stone-eye-arc";
|
||||
import StoneEyeFlat from "./stone-eye-flat";
|
||||
|
||||
type Props = {
|
||||
face: BotFace;
|
||||
/**
|
||||
* Fill class for solid eyes.
|
||||
*/
|
||||
fill_class: string;
|
||||
/**
|
||||
* Stroke class for arc eyes.
|
||||
*/
|
||||
stroke_class: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The face is the status. Geometry follows the handoff face states:
|
||||
* idle dots, working half-lidded, done upward arcs, asleep one flattened
|
||||
* dot, needs-you dot plus arc.
|
||||
*/
|
||||
export default function BotStoneFace({ face, fill_class, stroke_class }: Props) {
|
||||
const { left, right, centre } = StoneEyes;
|
||||
|
||||
switch (face) {
|
||||
case "working":
|
||||
return (
|
||||
<>
|
||||
<StoneEyeLidded {...left} className={fill_class} />
|
||||
<StoneEyeLidded {...right} className={fill_class} />
|
||||
</>
|
||||
);
|
||||
case "done":
|
||||
return (
|
||||
<>
|
||||
<StoneEyeArc {...left} className={stroke_class} />
|
||||
<StoneEyeArc {...right} className={stroke_class} />
|
||||
</>
|
||||
);
|
||||
case "asleep":
|
||||
return <StoneEyeFlat {...centre} className={fill_class} />;
|
||||
case "needs-you":
|
||||
return (
|
||||
<>
|
||||
<StoneEyeDot {...left} className={fill_class} />
|
||||
<StoneEyeArc {...right} className={stroke_class} />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<StoneEyeDot {...left} className={fill_class} />
|
||||
<StoneEyeDot {...right} className={fill_class} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { BotFace, BotHue } from "@/src/dict/bot-dict";
|
||||
import { BotHueFillClasses } from "@/src/dict/bot-hue-classes-dict";
|
||||
import {
|
||||
CoordinatorBodyPath,
|
||||
StoneBodyPaths,
|
||||
StoneViewBox,
|
||||
} from "@/src/data/stone-geometry";
|
||||
import BotStoneFace from "./bot-stone-face";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The bot's identity hue. Omit (or `null`) for the coordinator, which
|
||||
* renders hue-less: ink body, paper eyes, inverted in dark mode.
|
||||
*/
|
||||
hue?: BotHue | null;
|
||||
face?: BotFace;
|
||||
size?: number;
|
||||
/**
|
||||
* Accessible name. Leave empty when a visible bot name sits beside the
|
||||
* stone (the default), so the stone is decorative.
|
||||
*/
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Bot Stone
|
||||
* @description A Cobble bot avatar: a stone with exactly one flat cut and two
|
||||
* eyes. Never put it in a circle, ring or badge, or add an outline or shadow.
|
||||
*/
|
||||
export default function BotStone({
|
||||
hue,
|
||||
face = "idle",
|
||||
size = 32,
|
||||
label,
|
||||
className,
|
||||
}: Props) {
|
||||
const is_coordinator = !hue;
|
||||
const body_path = hue ? StoneBodyPaths[hue] : CoordinatorBodyPath;
|
||||
const body_class = hue ? BotHueFillClasses[hue] : "fill-ink";
|
||||
const eye_fill = is_coordinator ? "fill-paper" : "fill-dark";
|
||||
const eye_stroke = is_coordinator ? "stroke-paper" : "stroke-dark";
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={StoneViewBox}
|
||||
role={label ? "img" : undefined}
|
||||
aria-label={label || undefined}
|
||||
aria-hidden={label ? undefined : true}
|
||||
className={twMerge("shrink-0", className)}
|
||||
>
|
||||
<path d={body_path} className={body_class} />
|
||||
<BotStoneFace
|
||||
face={face}
|
||||
fill_class={eye_fill}
|
||||
stroke_class={eye_stroke}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import Loading from "@moduletrace/twui/elements/Loading";
|
||||
import Select from "@moduletrace/twui/form/Select";
|
||||
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
|
||||
import type { TableType } from "@/src/types";
|
||||
import Modal from "@moduletrace/twui/elements/Modal";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Card from "@moduletrace/twui/elements/Card";
|
||||
import H3 from "@moduletrace/twui/layout/H3";
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import useStatus from "@moduletrace/twui/hooks/useStatus";
|
||||
import H4 from "@moduletrace/twui/layout/H4";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import dayjs from "dayjs";
|
||||
import twuiNumberfy from "@moduletrace/twui/utils/numberfy";
|
||||
import LucideIcon from "@moduletrace/twui/elements/lucide-icon";
|
||||
import EmptyContent from "@moduletrace/twui/elements/EmptyContent";
|
||||
|
||||
export type CopyableRecord = {
|
||||
id?: number | "";
|
||||
title?: string;
|
||||
subject?: string;
|
||||
};
|
||||
|
||||
type Props<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS> = {
|
||||
table: TableType;
|
||||
title: string;
|
||||
onSelect?: (record: T) => void;
|
||||
onClear?: () => void;
|
||||
};
|
||||
|
||||
export default function CopyFromExistingSelect<
|
||||
T extends BUN_SQLITE_BSQLITE_ALL_TYPEDEFS =
|
||||
BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
>({ table, title, onSelect, onClear }: Props<T>) {
|
||||
const { res: records, ready } = useAdminCrudGet<T>({
|
||||
table,
|
||||
});
|
||||
|
||||
const { open, setOpen } = useStatus();
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Button
|
||||
title="Clear Form"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClear?.();
|
||||
}}
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
target={
|
||||
<Button
|
||||
title="Duplicate Existing Record"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
Create from an Existing Record
|
||||
</Button>
|
||||
}
|
||||
className="xl:p-6"
|
||||
>
|
||||
<Stack className="gap-6 w-full">
|
||||
<Stack className="gap-1">
|
||||
<H3 className="text-2xl! font-semibold">
|
||||
{title || "Select Record"}
|
||||
</H3>
|
||||
<Span>Select a record to duplicate</Span>
|
||||
</Stack>
|
||||
|
||||
<Stack className="w-full items-stretch">
|
||||
{records?.[0] ? (
|
||||
records?.map((rec, index) => {
|
||||
const date = dayjs(
|
||||
twuiNumberfy(rec.created_at),
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
onClick={() => {
|
||||
onSelect?.(rec);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<Row className="w-full justify-between">
|
||||
<Row>
|
||||
<LucideIcon
|
||||
name="Database"
|
||||
size={17}
|
||||
opacity={0.5}
|
||||
/>
|
||||
<H4 className="text-base! font-semibold">
|
||||
{rec.title}
|
||||
</H4>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Span
|
||||
size="smaller"
|
||||
variant="faded"
|
||||
className="text-nowrap"
|
||||
>
|
||||
{date
|
||||
.toString()
|
||||
.substring(0, 16)}
|
||||
</Span>
|
||||
</Row>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<EmptyContent title="No Records Found!" />
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +1,22 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import BotStone from "./bot-stone";
|
||||
|
||||
type Props = {
|
||||
icon_size?: number;
|
||||
img_props?: Pick<ComponentProps<"svg">, "className">;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Cobble mark: the ember stone. Source of truth is
|
||||
* `public/logos/logo.svg` (copied from `public/brand/icons/ember.svg`).
|
||||
*/
|
||||
export default function LogoIcon({ icon_size = 40, img_props }: Props) {
|
||||
return (
|
||||
<svg
|
||||
width={icon_size}
|
||||
height={icon_size}
|
||||
viewBox="0 0 512 512"
|
||||
role="img"
|
||||
aria-label="Logo Icon"
|
||||
className={twMerge("shrink-0", img_props?.className)}
|
||||
>
|
||||
<rect
|
||||
x="16"
|
||||
y="16"
|
||||
width="480"
|
||||
height="480"
|
||||
rx="120"
|
||||
className="fill-slate-900 dark:fill-slate-800"
|
||||
<BotStone
|
||||
hue="ember"
|
||||
size={icon_size}
|
||||
label="Logo Icon"
|
||||
className={img_props?.className}
|
||||
/>
|
||||
<path
|
||||
d="M290 100 L144 286 H244 L222 412 L368 226 H268 Z"
|
||||
strokeWidth="28"
|
||||
strokeLinejoin="round"
|
||||
className="fill-white stroke-white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import type { ComponentProps, PropsWithChildren } from "react";
|
||||
import type Span from "@moduletrace/twui/layout/Span";
|
||||
import type { ComponentProps } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import LogoIcon from "./logo-icon";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import Wordmark from "./wordmark";
|
||||
|
||||
type Props = {
|
||||
icon_size?: number;
|
||||
text_wrapper_props?: ComponentProps<typeof Stack>;
|
||||
/**
|
||||
* Only `className` applies: it sets the wordmark colour.
|
||||
*/
|
||||
text_props?: ComponentProps<typeof Span>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Horizontal lockup: the ember stone beside the `cobble` wordmark.
|
||||
*/
|
||||
export default function Logo({
|
||||
icon_size = 36,
|
||||
text_props,
|
||||
@@ -19,38 +25,18 @@ export default function Logo({
|
||||
}: Props) {
|
||||
return (
|
||||
<a href="/" className="logo-link">
|
||||
<Row className="gap-3! flex-nowrap items-center">
|
||||
<Row className="gap-2.5! flex-nowrap items-center">
|
||||
<LogoIcon icon_size={icon_size} />
|
||||
<Stack
|
||||
{...text_wrapper_props}
|
||||
className={twMerge("gap-1!", text_wrapper_props?.className)}
|
||||
className={twMerge("gap-0!", text_wrapper_props?.className)}
|
||||
>
|
||||
<Stack className="gap-0!">
|
||||
<HeroText text_props={text_props}>
|
||||
{SiteData["SiteName"]}
|
||||
</HeroText>
|
||||
</Stack>
|
||||
<Wordmark
|
||||
height={Math.round(icon_size * 0.5)}
|
||||
className={text_props?.className}
|
||||
/>
|
||||
</Stack>
|
||||
</Row>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroText({
|
||||
text_props,
|
||||
children,
|
||||
}: PropsWithChildren & {
|
||||
text_props?: ComponentProps<typeof Span>;
|
||||
}) {
|
||||
return (
|
||||
<Span
|
||||
{...text_props}
|
||||
className={twMerge(
|
||||
"text-[17px] font-semibold tracking-tight text-dark dark:text-white heading leading-[24px] whitespace-nowrap",
|
||||
text_props?.className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import Loading from "@moduletrace/twui/elements/Loading";
|
||||
import Center from "@moduletrace/twui/layout/Center";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
body: any;
|
||||
download_name?: string;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export default function PdfPreview({
|
||||
url,
|
||||
body,
|
||||
download_name,
|
||||
height = 1200,
|
||||
}: Props) {
|
||||
const [blob_url, setBlobUrl] = useState<string>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
let object_url: string | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
setBlobUrl(undefined);
|
||||
setError(undefined);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
|
||||
const content_type = res.headers.get("Content-Type") || "";
|
||||
|
||||
if (content_type.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
throw new Error(json.msg || "Couldn't generate PDF");
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
object_url = URL.createObjectURL(blob);
|
||||
setBlobUrl(object_url);
|
||||
} catch (error: any) {
|
||||
if (!cancelled) {
|
||||
setError(error.message || "Couldn't generate PDF preview");
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
if (object_url) {
|
||||
URL.revokeObjectURL(object_url);
|
||||
}
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Center className="py-10">
|
||||
<Span className="text-warning">{error}</Span>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (!blob_url) {
|
||||
return (
|
||||
<Center className="py-10">
|
||||
<Loading />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2 items-end">
|
||||
{download_name ? (
|
||||
<a
|
||||
href={blob_url}
|
||||
download={download_name}
|
||||
className="text-sm text-link-500 hover:opacity-70 underline"
|
||||
>
|
||||
Download PDF
|
||||
</a>
|
||||
) : null}
|
||||
<iframe
|
||||
src={blob_url}
|
||||
title="PDF Preview"
|
||||
className="w-full rounded-default border border-slate-300 dark:border-white/20 bg-white"
|
||||
style={{ height }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import LucideIcon from "@moduletrace/twui/elements/lucide-icon";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Paper from "@moduletrace/twui/elements/Paper";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Border from "@moduletrace/twui/elements/Border";
|
||||
import { formatNaira } from "@/src/utils/money";
|
||||
import twuiNumberfy from "@moduletrace/twui/utils/numberfy";
|
||||
|
||||
export type SortableLineItemFormObject = {
|
||||
description?: string;
|
||||
quantity?: string;
|
||||
/** Naira amount as a plain number string (converted to kobo on submit). */
|
||||
unit_price?: string;
|
||||
/** Client-side row identity for stable React keys during reorder. */
|
||||
_key?: string;
|
||||
};
|
||||
|
||||
type Props<T extends SortableLineItemFormObject> = {
|
||||
item: T;
|
||||
index: number;
|
||||
placeholder: string;
|
||||
is_dragging: boolean;
|
||||
is_drop_target: boolean;
|
||||
/** Whether a drag is currently in progress anywhere in the list. */
|
||||
drag_active: boolean;
|
||||
can_move_up: boolean;
|
||||
can_move_down: boolean;
|
||||
onChange: (item: T) => void;
|
||||
onRemove: () => void;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
onDragStart: () => void;
|
||||
onDragOver: () => void;
|
||||
onDragLeave: () => void;
|
||||
/** `drop_after` is true when dropped on the lower half of this row. */
|
||||
onDrop: (drop_after: boolean) => void;
|
||||
onDragEnd: () => void;
|
||||
};
|
||||
|
||||
export default function SortableLineItemRow<
|
||||
T extends SortableLineItemFormObject,
|
||||
>({
|
||||
item,
|
||||
index,
|
||||
placeholder,
|
||||
is_dragging,
|
||||
is_drop_target,
|
||||
drag_active,
|
||||
can_move_up,
|
||||
can_move_down,
|
||||
onChange,
|
||||
onRemove,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
}: Props<T>) {
|
||||
const line_total_kobo =
|
||||
(Number(item.quantity) || 0) * (Number(item.unit_price) || 0) * 100;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className={[
|
||||
"w-full gap-3!",
|
||||
is_dragging && "opacity-60",
|
||||
is_drop_target && "ring-2 ring-primary/70",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
onDragOver={(e) => {
|
||||
if (!drag_active) return;
|
||||
e.preventDefault();
|
||||
onDragOver();
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
if (drag_active) onDragLeave();
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
if (!drag_active) return;
|
||||
e.preventDefault();
|
||||
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
|
||||
onDrop(e.clientY - rect.top > rect.height / 2);
|
||||
}}
|
||||
>
|
||||
<Row className="w-full justify-between items-center gap-2!">
|
||||
<Row className="gap-2! items-center">
|
||||
<Span
|
||||
title="Drag to reorder"
|
||||
className="cursor-grab select-none p-1 -m-1!"
|
||||
draggable
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<LucideIcon name="GripVertical" size={16} />
|
||||
</Span>
|
||||
<Span className="text-xs font-semibold opacity-60">
|
||||
Item #{index + 1}
|
||||
</Span>
|
||||
</Row>
|
||||
<Row className="gap-1! items-center">
|
||||
<Button
|
||||
title="Move Up"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="smaller"
|
||||
disabled={!can_move_up}
|
||||
onClick={onMoveUp}
|
||||
>
|
||||
<LucideIcon name="ArrowUp" size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
title="Move Down"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="smaller"
|
||||
disabled={!can_move_down}
|
||||
onClick={onMoveDown}
|
||||
>
|
||||
<LucideIcon name="ArrowDown" size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
title="Remove Line Item"
|
||||
variant="ghost"
|
||||
color="error"
|
||||
size="smaller"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<LucideIcon name="Trash2" size={14} />
|
||||
</Button>
|
||||
</Row>
|
||||
</Row>
|
||||
|
||||
<Input
|
||||
title="Description"
|
||||
placeholder={placeholder}
|
||||
defaultValue={item.description}
|
||||
onChange={(e) => {
|
||||
onChange({ ...item, description: e.target.value });
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
|
||||
<Row className="w-full grid xl:grid-cols-3 items-stretch">
|
||||
<Input
|
||||
title="Quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
defaultValue={item.quantity}
|
||||
onChange={(e) => {
|
||||
onChange({ ...item, quantity: e.target.value });
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
|
||||
<Input
|
||||
title="Unit Price (₦)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min={0}
|
||||
defaultValue={item.unit_price}
|
||||
onChange={(e) => {
|
||||
onChange({
|
||||
...item,
|
||||
unit_price: Math.round(
|
||||
twuiNumberfy(e.target.value),
|
||||
).toString(),
|
||||
});
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
|
||||
<Border className="py-1 px-4">
|
||||
<Span className="text-sm font-semibold">
|
||||
{formatNaira(line_total_kobo)}
|
||||
</Span>
|
||||
</Border>
|
||||
</Row>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import H3 from "@moduletrace/twui/layout/H3";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { useEffect, useState } from "react";
|
||||
import { formatNaira, sumInvoiceItemsKobo } from "@/src/utils/money";
|
||||
import SortableLineItemRow, {
|
||||
type SortableLineItemFormObject,
|
||||
} from "./sortable-line-item-row";
|
||||
|
||||
let row_key_counter = 0;
|
||||
|
||||
export const makeRowKey = (): string =>
|
||||
typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `row-${Date.now()}-${row_key_counter++}`;
|
||||
|
||||
type Props<T extends SortableLineItemFormObject> = {
|
||||
items: T[];
|
||||
empty_item: T;
|
||||
item_placeholder: string;
|
||||
onChange: (items: T[]) => void;
|
||||
};
|
||||
|
||||
export default function SortableLineItemsEditor<
|
||||
T extends SortableLineItemFormObject,
|
||||
>({ items, empty_item, item_placeholder, onChange }: Props<T>) {
|
||||
const [drag_source, set_drag_source] = useState<number | undefined>();
|
||||
const [drag_target, set_drag_target] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (items.some((item) => !item._key)) {
|
||||
onChange(
|
||||
items.map((item) => ({ ...item, _key: makeRowKey() })),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const display_items = items.length ? items : [empty_item];
|
||||
const can_remove = items.length > 1;
|
||||
|
||||
const setItems = (next_items: T[]) => onChange(next_items);
|
||||
|
||||
const endDrag = () => {
|
||||
set_drag_source(undefined);
|
||||
set_drag_target(undefined);
|
||||
};
|
||||
|
||||
const moveTo = (from: number | undefined, target: number, drop_after: boolean) => {
|
||||
if (from === undefined || from === target) return;
|
||||
|
||||
let to = drop_after ? target + 1 : target;
|
||||
if (from < to) to -= 1;
|
||||
if (to === from) return;
|
||||
|
||||
const next_items = items.length ? [...items] : [{ ...empty_item }];
|
||||
const moved_item: T = next_items[from] as T;
|
||||
next_items.splice(from, 1);
|
||||
next_items.splice(to, 0, moved_item);
|
||||
setItems(next_items);
|
||||
};
|
||||
|
||||
const moveBy = (index: number, delta: -1 | 1) => {
|
||||
const target = index + delta;
|
||||
if (target < 0 || target >= (items.length ? items.length : 1)) return;
|
||||
moveTo(index, target, delta === 1);
|
||||
};
|
||||
|
||||
const total_kobo = sumInvoiceItemsKobo(
|
||||
items.map((item) => ({
|
||||
quantity: item.quantity,
|
||||
unit_price_kobo: (Number(item.unit_price) || 0) * 100,
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-4">
|
||||
<H3 className="!mb-0">Line Items</H3>
|
||||
|
||||
{display_items.map((item, index) => (
|
||||
<SortableLineItemRow
|
||||
key={item._key || index}
|
||||
item={item}
|
||||
index={index}
|
||||
placeholder={item_placeholder}
|
||||
is_dragging={drag_source === index}
|
||||
is_drop_target={
|
||||
drag_source !== undefined &&
|
||||
drag_source !== index &&
|
||||
drag_target === index
|
||||
}
|
||||
drag_active={drag_source !== undefined}
|
||||
can_move_up={index > 0}
|
||||
can_move_down={index < items.length - 1}
|
||||
onChange={(next_item) => {
|
||||
const next_items = items.length
|
||||
? [...items]
|
||||
: [{ ...empty_item }];
|
||||
next_items[index] = next_item;
|
||||
setItems(next_items);
|
||||
}}
|
||||
onRemove={() => {
|
||||
if (!can_remove) return;
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
}}
|
||||
onMoveUp={() => moveBy(index, -1)}
|
||||
onMoveDown={() => moveBy(index, 1)}
|
||||
onDragStart={() => {
|
||||
if (items.length < 2 || drag_source !== undefined) return;
|
||||
set_drag_source(index);
|
||||
}}
|
||||
onDragOver={() => {
|
||||
if (
|
||||
drag_source === undefined ||
|
||||
drag_source === index ||
|
||||
drag_target === index
|
||||
) return;
|
||||
set_drag_target(index);
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
if (drag_target === index) {
|
||||
set_drag_target(undefined);
|
||||
}
|
||||
}}
|
||||
onDrop={(drop_after) => {
|
||||
moveTo(drag_source, index, drop_after);
|
||||
endDrag();
|
||||
}}
|
||||
onDragEnd={endDrag}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Row className="w-full justify-between items-center">
|
||||
<Button
|
||||
title="Add Line Item"
|
||||
color="gray"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setItems([
|
||||
...items,
|
||||
{ ...empty_item, _key: makeRowKey() },
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Add Line Item
|
||||
</Button>
|
||||
|
||||
<Span className="text-base font-semibold">
|
||||
Total: {formatNaira(total_kobo)}
|
||||
</Span>
|
||||
</Row>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { StoneEyes } from "@/src/data/stone-geometry";
|
||||
|
||||
type Props = {
|
||||
cx: number;
|
||||
cy: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upward arc eye, drawn as a stroke in the eye colour.
|
||||
*/
|
||||
export default function StoneEyeArc({ cx, cy, className }: Props) {
|
||||
const r = StoneEyes.radius;
|
||||
const base = cy + r * 0.4;
|
||||
|
||||
return (
|
||||
<path
|
||||
d={`M ${cx - r} ${base} A ${r} ${r} 0 0 1 ${cx + r} ${base}`}
|
||||
fill="none"
|
||||
strokeWidth={2.6}
|
||||
strokeLinecap="round"
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { StoneEyes } from "@/src/data/stone-geometry";
|
||||
|
||||
type Props = {
|
||||
cx: number;
|
||||
cy: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function StoneEyeDot({ cx, cy, className }: Props) {
|
||||
return <circle cx={cx} cy={cy} r={StoneEyes.radius} className={className} />;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { StoneEyes } from "@/src/data/stone-geometry";
|
||||
|
||||
type Props = {
|
||||
cx: number;
|
||||
cy: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Flattened dot, used once and centred for the asleep face.
|
||||
*/
|
||||
export default function StoneEyeFlat({ cx, cy, className }: Props) {
|
||||
return (
|
||||
<ellipse
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
rx={StoneEyes.radius + 1}
|
||||
ry={1.8}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { StoneEyes } from "@/src/data/stone-geometry";
|
||||
|
||||
type Props = {
|
||||
cx: number;
|
||||
cy: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Half-lidded eye: the lower half of the dot, flat edge on top.
|
||||
*/
|
||||
export default function StoneEyeLidded({ cx, cy, className }: Props) {
|
||||
const r = StoneEyes.radius;
|
||||
|
||||
return (
|
||||
<path
|
||||
d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 0 ${cx + r} ${cy} Z`}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import { WordmarkPaths, WordmarkViewBox } from "@/src/data/wordmark-paths";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Rendered height in px. The plain wordmark is for 16 to 48 px.
|
||||
*/
|
||||
height?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const [, , VIEWBOX_WIDTH, VIEWBOX_HEIGHT] = WordmarkViewBox.split(" ").map(
|
||||
Number,
|
||||
) as [number, number, number, number];
|
||||
|
||||
/**
|
||||
* # Wordmark
|
||||
* @description The `cobble` wordmark as vector paths, never live text. Takes
|
||||
* the current text colour.
|
||||
*/
|
||||
export default function Wordmark({ height = 15, className }: Props) {
|
||||
const width = (height * VIEWBOX_WIDTH) / VIEWBOX_HEIGHT;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={WordmarkViewBox}
|
||||
role="img"
|
||||
aria-label={SiteData["SiteName"]}
|
||||
className={twMerge("shrink-0 fill-current text-ink", className)}
|
||||
>
|
||||
{WordmarkPaths.map((d) => (
|
||||
<WordmarkPath key={d.slice(0, 32)} d={d} />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WordmarkPath({ d }: { d: string }) {
|
||||
return <path d={d} />;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export const SiteData = {
|
||||
SiteName: `Bun SQLite Preset`,
|
||||
SiteURL: `http://localhost:7247`,
|
||||
SiteSlug: `bsqlite`,
|
||||
SiteName: `cobble`,
|
||||
SiteURL: `https://cobble.moduletrace.org`,
|
||||
SiteSlug: `cobble`,
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* `cobble-plain` wordmark paths from the handoff
|
||||
* (`public/brand/wordmark/cobble-plain.svg`), cropped to the glyph bounds.
|
||||
* Use from 16 to 48 px height; never stretch, re-space or recolour it.
|
||||
*/
|
||||
export const WordmarkViewBox = "516.9 124.3 566.2 151.3";
|
||||
|
||||
export const WordmarkPaths = [
|
||||
"M598.5 194.95 C597.567 196.083 596.667 196.983 595.8 197.65 C594.933 198.317 593.7 198.65 592.1 198.65 C590.567 198.65 589.183 198.283 587.95 197.55 C586.717 196.817 585.333 196 583.8 195.1 C582.267 194.2 580.467 193.383 578.4 192.65 C576.333 191.917 573.767 191.55 570.7 191.55 C566.9 191.55 563.617 192.25 560.85 193.65 C558.083 195.05 555.8 197.05 554 199.65 C552.2 202.25 550.867 205.433 550 209.2 C549.133 212.967 548.7 217.217 548.7 221.95 C548.7 231.883 550.617 239.517 554.45 244.85 C558.283 250.183 563.567 252.85 570.3 252.85 C573.9 252.85 576.75 252.4 578.85 251.5 C580.95 250.6 582.733 249.6 584.2 248.5 C585.667 247.4 587.017 246.383 588.25 245.45 C589.483 244.517 591.033 244.05 592.9 244.05 C595.367 244.05 597.233 244.95 598.5 246.75 L607.5 257.85 C604.3 261.517 600.933 264.5 597.4 266.8 C593.867 269.1 590.25 270.9 586.55 272.2 C582.85 273.5 579.133 274.4 575.4 274.9 C571.667 275.4 568.033 275.65 564.5 275.65 C558.167 275.65 552.117 274.45 546.35 272.05 C540.583 269.65 535.517 266.167 531.15 261.6 C526.783 257.033 523.317 251.417 520.75 244.75 C518.183 238.083 516.9 230.483 516.9 221.95 C516.9 214.483 518.017 207.5 520.25 201 C522.483 194.5 525.783 188.867 530.15 184.1 C534.517 179.333 539.917 175.583 546.35 172.85 C552.783 170.117 560.233 168.75 568.7 168.75 C576.833 168.75 583.967 170.05 590.1 172.65 C596.233 175.25 601.767 179.05 606.7 184.05 Z",
|
||||
"M660.6 168.75 C668.467 168.75 675.633 169.983 682.1 172.45 C688.567 174.917 694.117 178.45 698.75 183.05 C703.383 187.65 706.983 193.233 709.55 199.8 C712.117 206.367 713.4 213.75 713.4 221.95 C713.4 230.217 712.117 237.667 709.55 244.3 C706.983 250.933 703.383 256.567 698.75 261.2 C694.117 265.833 688.567 269.4 682.1 271.9 C675.633 274.4 668.467 275.65 660.6 275.65 C652.667 275.65 645.45 274.4 638.95 271.9 C632.45 269.4 626.85 265.833 622.15 261.2 C617.45 256.567 613.817 250.933 611.25 244.3 C608.683 237.667 607.4 230.217 607.4 221.95 C607.4 213.75 608.683 206.367 611.25 199.8 C613.817 193.233 617.45 187.65 622.15 183.05 C626.85 178.45 632.45 174.917 638.95 172.45 C645.45 169.983 652.667 168.75 660.6 168.75 Z M660.6 252.65 C667.733 252.65 672.983 250.133 676.35 245.1 C679.717 240.067 681.4 232.417 681.4 222.15 C681.4 211.883 679.717 204.25 676.35 199.25 C672.983 194.25 667.733 191.75 660.6 191.75 C653.267 191.75 647.9 194.25 644.5 199.25 C641.1 204.25 639.4 211.883 639.4 222.15 C639.4 232.417 641.1 240.067 644.5 245.1 C647.9 250.133 653.267 252.65 660.6 252.65 Z",
|
||||
"M724 274.05 L724 124.35 L755 124.35 L755 181.45 C759 177.583 763.367 174.5 768.1 172.2 C772.833 169.9 778.167 168.75 784.1 168.75 C789.967 168.75 795.3 169.95 800.1 172.35 C804.9 174.75 809 178.167 812.4 182.6 C815.8 187.033 818.417 192.383 820.25 198.65 C822.083 204.917 823 211.917 823 219.65 C823 228.05 821.933 235.683 819.8 242.55 C817.667 249.417 814.65 255.3 810.75 260.2 C806.85 265.1 802.2 268.9 796.8 271.6 C791.4 274.3 785.433 275.65 778.9 275.65 C775.767 275.65 772.967 275.35 770.5 274.75 C768.033 274.15 765.767 273.3 763.7 272.2 C761.633 271.1 759.75 269.783 758.05 268.25 C756.35 266.717 754.7 265.05 753.1 263.25 L752 268.45 C751.467 270.583 750.6 272.05 749.4 272.85 C748.2 273.65 746.6 274.05 744.6 274.05 Z M773.2 192.05 C769.4 192.05 766.1 192.883 763.3 194.55 C760.5 196.217 757.733 198.65 755 201.85 L755 245.85 C757.333 248.45 759.867 250.267 762.6 251.3 C765.333 252.333 768.1 252.85 770.9 252.85 C773.833 252.85 776.533 252.3 779 251.2 C781.467 250.1 783.583 248.3 785.35 245.8 C787.117 243.3 788.5 240 789.5 235.9 C790.5 231.8 791 226.75 791 220.75 C791 215.55 790.617 211.133 789.85 207.5 C789.083 203.867 787.95 200.9 786.45 198.6 C784.95 196.3 783.083 194.633 780.85 193.6 C778.617 192.567 776.067 192.05 773.2 192.05 Z",
|
||||
"M833.9 274.05 L833.9 124.35 L864.9 124.35 L864.9 181.45 C868.9 177.583 873.267 174.5 878 172.2 C882.733 169.9 888.067 168.75 894 168.75 C899.867 168.75 905.2 169.95 910 172.35 C914.8 174.75 918.9 178.167 922.3 182.6 C925.7 187.033 928.317 192.383 930.15 198.65 C931.983 204.917 932.9 211.917 932.9 219.65 C932.9 228.05 931.833 235.683 929.7 242.55 C927.567 249.417 924.55 255.3 920.65 260.2 C916.75 265.1 912.1 268.9 906.7 271.6 C901.3 274.3 895.333 275.65 888.8 275.65 C885.667 275.65 882.867 275.35 880.4 274.75 C877.933 274.15 875.667 273.3 873.6 272.2 C871.533 271.1 869.65 269.783 867.95 268.25 C866.25 266.717 864.6 265.05 863 263.25 L861.9 268.45 C861.367 270.583 860.5 272.05 859.3 272.85 C858.1 273.65 856.5 274.05 854.5 274.05 Z M883.1 192.05 C879.3 192.05 876 192.883 873.2 194.55 C870.4 196.217 867.633 198.65 864.9 201.85 L864.9 245.85 C867.233 248.45 869.767 250.267 872.5 251.3 C875.233 252.333 878 252.85 880.8 252.85 C883.733 252.85 886.433 252.3 888.9 251.2 C891.367 250.1 893.483 248.3 895.25 245.8 C897.017 243.3 898.4 240 899.4 235.9 C900.4 231.8 900.9 226.75 900.9 220.75 C900.9 215.55 900.517 211.133 899.75 207.5 C898.983 203.867 897.85 200.9 896.35 198.6 C894.85 196.3 892.983 194.633 890.75 193.6 C888.517 192.567 885.967 192.05 883.1 192.05 Z",
|
||||
"M974.8 124.35 L974.8 274.05 L943.8 274.05 L943.8 124.35 Z",
|
||||
"M1036.5 168.75 C1043.367 168.75 1049.65 169.817 1055.35 171.95 C1061.05 174.083 1065.95 177.183 1070.05 181.25 C1074.15 185.317 1077.35 190.3 1079.65 196.2 C1081.95 202.1 1083.1 208.783 1083.1 216.25 C1083.1 218.583 1083 220.483 1082.8 221.95 C1082.6 223.417 1082.233 224.583 1081.7 225.45 C1081.167 226.317 1080.45 226.917 1079.55 227.25 C1078.65 227.583 1077.5 227.75 1076.1 227.75 L1016.3 227.75 C1017.3 236.417 1019.933 242.7 1024.2 246.6 C1028.467 250.5 1033.967 252.45 1040.7 252.45 C1044.3 252.45 1047.4 252.017 1050 251.15 C1052.6 250.283 1054.917 249.317 1056.95 248.25 C1058.983 247.183 1060.85 246.217 1062.55 245.35 C1064.25 244.483 1066.033 244.05 1067.9 244.05 C1070.367 244.05 1072.233 244.95 1073.5 246.75 L1082.5 257.85 C1079.3 261.517 1075.85 264.5 1072.15 266.8 C1068.45 269.1 1064.65 270.9 1060.75 272.2 C1056.85 273.5 1052.95 274.4 1049.05 274.9 C1045.15 275.4 1041.433 275.65 1037.9 275.65 C1030.633 275.65 1023.817 274.467 1017.45 272.1 C1011.083 269.733 1005.517 266.217 1000.75 261.55 C995.983 256.883 992.217 251.083 989.45 244.15 C986.683 237.217 985.3 229.15 985.3 219.95 C985.3 213.017 986.483 206.45 988.85 200.25 C991.217 194.05 994.617 188.617 999.05 183.95 C1003.483 179.283 1008.85 175.583 1015.15 172.85 C1021.45 170.117 1028.567 168.75 1036.5 168.75 Z M1037.1 190.15 C1031.167 190.15 1026.533 191.833 1023.2 195.2 C1019.867 198.567 1017.667 203.417 1016.6 209.75 L1055.3 209.75 C1055.3 207.283 1054.983 204.883 1054.35 202.55 C1053.717 200.217 1052.683 198.133 1051.25 196.3 C1049.817 194.467 1047.933 192.983 1045.6 191.85 C1043.267 190.717 1040.433 190.15 1037.1 190.15 Z",
|
||||
] as const;
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Bot identity hues. A bot keeps its hue for life; a hue never means state.
|
||||
* The coordinator has no hue.
|
||||
*/
|
||||
export const BotHues = [
|
||||
{
|
||||
title: "Ember",
|
||||
value: "ember",
|
||||
},
|
||||
{
|
||||
title: "Amber",
|
||||
value: "amber",
|
||||
},
|
||||
{
|
||||
title: "Moss",
|
||||
value: "moss",
|
||||
},
|
||||
{
|
||||
title: "Teal",
|
||||
value: "teal",
|
||||
},
|
||||
{
|
||||
title: "Cobalt",
|
||||
value: "cobalt",
|
||||
},
|
||||
{
|
||||
title: "Violet",
|
||||
value: "violet",
|
||||
},
|
||||
{
|
||||
title: "Rose",
|
||||
value: "rose",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type BotHue = (typeof BotHues)[number]["value"];
|
||||
|
||||
export const BotKinds = [
|
||||
{
|
||||
title: "Worker",
|
||||
value: "worker",
|
||||
},
|
||||
{
|
||||
title: "Coordinator",
|
||||
value: "coordinator",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type BotKind = (typeof BotKinds)[number]["value"];
|
||||
|
||||
/**
|
||||
* A bot's face is its status. There are no badges or spinners.
|
||||
*/
|
||||
export const BotFaces = [
|
||||
{
|
||||
title: "Idle",
|
||||
value: "idle",
|
||||
},
|
||||
{
|
||||
title: "Working",
|
||||
value: "working",
|
||||
},
|
||||
{
|
||||
title: "Done",
|
||||
value: "done",
|
||||
},
|
||||
{
|
||||
title: "Asleep",
|
||||
value: "asleep",
|
||||
},
|
||||
{
|
||||
title: "Needs you",
|
||||
value: "needs-you",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type BotFace = (typeof BotFaces)[number]["value"];
|
||||
|
||||
export const BotMessageAuthors = [
|
||||
{
|
||||
title: "Bot",
|
||||
value: "bot",
|
||||
},
|
||||
{
|
||||
title: "User",
|
||||
value: "user",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type BotMessageAuthor = (typeof BotMessageAuthors)[number]["value"];
|
||||
|
||||
export const BotTaskStatuses = [
|
||||
{
|
||||
title: "Active",
|
||||
value: "active",
|
||||
},
|
||||
{
|
||||
title: "Done",
|
||||
value: "done",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type BotTaskStatus = (typeof BotTaskStatuses)[number]["value"];
|
||||
|
||||
export const BotTaskStepStates = [
|
||||
{
|
||||
title: "Done",
|
||||
value: "done",
|
||||
},
|
||||
{
|
||||
title: "Active",
|
||||
value: "active",
|
||||
},
|
||||
{
|
||||
title: "Pending",
|
||||
value: "pending",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type BotTaskStepState = (typeof BotTaskStepStates)[number]["value"];
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { BotHue } from "./bot-dict";
|
||||
|
||||
/**
|
||||
* Static Tailwind class names per hue, so the classes survive the build.
|
||||
*/
|
||||
export const BotHueFillClasses: Record<BotHue, string> = {
|
||||
ember: "fill-hue-ember",
|
||||
amber: "fill-hue-amber",
|
||||
moss: "fill-hue-moss",
|
||||
teal: "fill-hue-teal",
|
||||
cobalt: "fill-hue-cobalt",
|
||||
violet: "fill-hue-violet",
|
||||
rose: "fill-hue-rose",
|
||||
};
|
||||
|
||||
export const BotHueBgClasses: Record<BotHue, string> = {
|
||||
ember: "bg-hue-ember",
|
||||
amber: "bg-hue-amber",
|
||||
moss: "bg-hue-moss",
|
||||
teal: "bg-hue-teal",
|
||||
cobalt: "bg-hue-cobalt",
|
||||
violet: "bg-hue-violet",
|
||||
rose: "bg-hue-rose",
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
export const EmailStatuses = [
|
||||
{
|
||||
title: "Draft",
|
||||
value: "draft",
|
||||
},
|
||||
{
|
||||
title: "Sent",
|
||||
value: "sent",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type EmailStatus = (typeof EmailStatuses)[number]["value"];
|
||||
|
||||
export const EmailAttachmentTypes = [
|
||||
{
|
||||
title: "Letter",
|
||||
value: "letter",
|
||||
},
|
||||
{
|
||||
title: "Invoice",
|
||||
value: "invoice",
|
||||
},
|
||||
{
|
||||
title: "Quote",
|
||||
value: "quote",
|
||||
},
|
||||
{
|
||||
title: "File",
|
||||
value: "file",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type EmailAttachmentType = (typeof EmailAttachmentTypes)[number]["value"];
|
||||
|
||||
export const InvoiceStatuses = [
|
||||
{
|
||||
title: "Draft",
|
||||
value: "draft",
|
||||
},
|
||||
{
|
||||
title: "Issued",
|
||||
value: "issued",
|
||||
},
|
||||
{
|
||||
title: "Paid",
|
||||
value: "paid",
|
||||
},
|
||||
{
|
||||
title: "Cancelled",
|
||||
value: "cancelled",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type InvoiceStatus = (typeof InvoiceStatuses)[number]["value"];
|
||||
|
||||
export const QuoteStatuses = [
|
||||
{
|
||||
title: "Draft",
|
||||
value: "draft",
|
||||
},
|
||||
{
|
||||
title: "Sent",
|
||||
value: "sent",
|
||||
},
|
||||
{
|
||||
title: "Accepted",
|
||||
value: "accepted",
|
||||
},
|
||||
{
|
||||
title: "Declined",
|
||||
value: "declined",
|
||||
},
|
||||
{
|
||||
title: "Expired",
|
||||
value: "expired",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type QuoteStatus = (typeof QuoteStatuses)[number]["value"];
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import checkUserAccess from "./check-user-access";
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
|
||||
function makeUserTypes(...types: string[]): BUN_SQLITE_BSQLITE_USER_TYPES[] {
|
||||
function makeUserTypes(...types: string[]): BUN_SQLITE_COBBLE_USER_TYPES[] {
|
||||
return types.map((user_type) => ({
|
||||
user_id: 1,
|
||||
user_type,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
import type { UserType } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
type Params = {
|
||||
includes?: UserType[];
|
||||
excludes?: UserType[];
|
||||
user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
user_types?: BUN_SQLITE_COBBLE_USER_TYPES[] | null;
|
||||
};
|
||||
|
||||
export default function checkUserAccess({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_USERS,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
@@ -24,7 +24,7 @@ export default async function createSignupUser({
|
||||
username,
|
||||
}: Params) {
|
||||
const new_user_res = await BunSQLite.insert<
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
BUN_SQLITE_COBBLE_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
@@ -50,7 +50,7 @@ export default async function createSignupUser({
|
||||
}
|
||||
|
||||
const type_res = await BunSQLite.insert<
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "user_types",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_COBBLE_SSO_LOGIN_CODES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
|
||||
@@ -10,7 +10,7 @@ type Params = {
|
||||
|
||||
export default async function deleteUserSSOCodes({ user_id }: Params) {
|
||||
return await BunSQLite.delete<
|
||||
BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_COBBLE_SSO_LOGIN_CODES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "sso_login_codes",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_BSQLITE_USERS, BunSQLiteTables } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USERS, BunSQLiteTables } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
identifier: string;
|
||||
@@ -11,7 +11,7 @@ type Params = {
|
||||
export default async function findUserByIdentifier({ identifier }: Params) {
|
||||
return (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
BUN_SQLITE_COBBLE_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomInt } from "node:crypto";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_BSQLITE_USERS, BunSQLiteTables } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USERS, BunSQLiteTables } from "@/db/types/db";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
|
||||
type Params = {
|
||||
@@ -24,7 +24,7 @@ export default async function generateUniqueUsername({ email }: Params) {
|
||||
|
||||
const existing = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
BUN_SQLITE_COBBLE_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import type { UserAuthReturn } from "@/src/types";
|
||||
@@ -16,7 +16,7 @@ export default async function grabUserAuthTypes({
|
||||
const user_types =
|
||||
(
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "user_types",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_USER_PASSWORDS,
|
||||
BUN_SQLITE_COBBLE_USER_PASSWORDS,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
|
||||
@@ -15,7 +15,7 @@ type Params = {
|
||||
export default async function grabUserPasswordHash({ user_id }: Params) {
|
||||
const record = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USER_PASSWORDS,
|
||||
BUN_SQLITE_COBBLE_USER_PASSWORDS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "user_passwords",
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
BunextAPIRouteHandlerObjectReturn,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_COBBLE_SSO_LOGIN_CODES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
|
||||
@@ -37,7 +37,7 @@ export default async function issueSSOLoginCode({
|
||||
const sso_code = randomInt(0, 1000000).toString().padStart(6, "0");
|
||||
|
||||
const record_sso = await BunSQLite.insert<
|
||||
BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_COBBLE_SSO_LOGIN_CODES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
data: [
|
||||
|
||||
@@ -4,8 +4,8 @@ import { encrypt } from "@/src/utils/crypt";
|
||||
import grabUsers from "../db/users/grab-users";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import generateCSRF from "./gen-csrf";
|
||||
@@ -44,7 +44,7 @@ export default async function loginUser({
|
||||
|
||||
const target_user_types = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "user_types",
|
||||
@@ -65,7 +65,7 @@ export default async function loginUser({
|
||||
const profile_media = target_user.profile_media_id
|
||||
? (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "media",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_USER_PASSWORDS,
|
||||
BUN_SQLITE_COBBLE_USER_PASSWORDS,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import hashPassword from "./hash-password";
|
||||
@@ -21,7 +21,7 @@ export default async function setUserPassword({ user_id, password }: Params) {
|
||||
const password_hash = await hashPassword({ password });
|
||||
|
||||
const res = await BunSQLite.insert<
|
||||
BUN_SQLITE_BSQLITE_USER_PASSWORDS,
|
||||
BUN_SQLITE_COBBLE_USER_PASSWORDS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "user_passwords",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_COBBLE_SSO_LOGIN_CODES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
@@ -70,7 +70,7 @@ export default async function verifySSOCode({
|
||||
|
||||
const target_sso = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_COBBLE_SSO_LOGIN_CODES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "sso_login_codes",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type {
|
||||
BUN_SQLITE_COBBLE_BOT_TASK_STEPS,
|
||||
BUN_SQLITE_COBBLE_BOT_TASKS,
|
||||
} from "@/db/types/db";
|
||||
import type { BotTaskWithSteps } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The bot's most recent active task and its steps, for the working-on panel.
|
||||
*/
|
||||
export default async function grabActiveBotTask({
|
||||
bot_id,
|
||||
}: Params): Promise<BotTaskWithSteps | null> {
|
||||
const task_res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_TASKS>({
|
||||
sql: `SELECT * FROM bot_tasks WHERE bot_id = ? AND status = 'active' ORDER BY created_at DESC, id DESC LIMIT 1`,
|
||||
values: [bot_id],
|
||||
});
|
||||
|
||||
const task = task_res.singleRes || task_res.payload?.[0];
|
||||
|
||||
if (!task?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const steps_res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_TASK_STEPS>({
|
||||
sql: `SELECT * FROM bot_task_steps WHERE task_id = ? ORDER BY position ASC, id ASC`,
|
||||
values: [task.id],
|
||||
});
|
||||
|
||||
return { ...task, steps: steps_res.payload || [] };
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOT_MESSAGES } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The latest messages of a bot's conversation, oldest first.
|
||||
*/
|
||||
export default async function grabBotMessages({
|
||||
bot_id,
|
||||
limit = 100,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_MESSAGES[]> {
|
||||
let sql = ``;
|
||||
|
||||
sql += `SELECT * FROM (\n`;
|
||||
sql += ` SELECT * FROM bot_messages\n`;
|
||||
sql += ` WHERE bot_id = ?\n`;
|
||||
sql += ` ORDER BY created_at DESC, id DESC\n`;
|
||||
sql += ` LIMIT ?\n`;
|
||||
sql += `) ORDER BY created_at ASC, id ASC\n`;
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_MESSAGES>({
|
||||
sql,
|
||||
values: [bot_id, limit],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A workspace's crew for the rail: the coordinator pinned first, then
|
||||
* workers by latest activity. Each row carries its latest message.
|
||||
*/
|
||||
export default async function grabBots({
|
||||
user_id,
|
||||
}: Params): Promise<BotWithLastMessage[]> {
|
||||
let sql = ``;
|
||||
|
||||
sql += `SELECT\n`;
|
||||
sql += ` b.*,\n`;
|
||||
sql += ` lm.text AS last_message_text,\n`;
|
||||
sql += ` lm.created_at AS last_message_at\n`;
|
||||
sql += `FROM bots b\n`;
|
||||
sql += `LEFT JOIN bot_messages lm ON lm.id = (\n`;
|
||||
sql += ` SELECT id FROM bot_messages\n`;
|
||||
sql += ` WHERE bot_id = b.id\n`;
|
||||
sql += ` ORDER BY created_at DESC, id DESC\n`;
|
||||
sql += ` LIMIT 1\n`;
|
||||
sql += `)\n`;
|
||||
sql += `WHERE b.user_id = ?\n`;
|
||||
sql += `ORDER BY\n`;
|
||||
sql += ` CASE WHEN b.kind = 'coordinator' THEN 0 ELSE 1 END,\n`;
|
||||
sql += ` COALESCE(lm.created_at, b.last_activity_at, b.created_at) DESC\n`;
|
||||
|
||||
const res = await BunSQLite.sql<BotWithLastMessage>({
|
||||
sql,
|
||||
values: [user_id],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot only if it belongs to the given workspace owner.
|
||||
*/
|
||||
export default async function grabUserBot({
|
||||
bot_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOTS | null> {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT * FROM bots WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [bot_id, user_id],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type {
|
||||
BUN_SQLITE_COBBLE_BOT_MESSAGES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a user message to a bot's conversation and bumps its activity.
|
||||
* Callers must check the bot belongs to the session user first.
|
||||
*/
|
||||
export default async function insertUserMessage({
|
||||
bot_id,
|
||||
text,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_MESSAGES> {
|
||||
const now = Date.now();
|
||||
|
||||
const res = await BunSQLite.insert<
|
||||
BUN_SQLITE_COBBLE_BOT_MESSAGES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "bot_messages",
|
||||
data: [{ bot_id, author: "user", text, needs_you: 0 }],
|
||||
});
|
||||
|
||||
const id = res.postInsertReturn?.insertId;
|
||||
|
||||
if (!res.success || !id) {
|
||||
throw new Error(`Failed to save message: ${res.msg}`);
|
||||
}
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET last_activity_at = ? WHERE id = ?`,
|
||||
values: [now, bot_id],
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
bot_id,
|
||||
author: "user",
|
||||
text,
|
||||
needs_you: 0,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears a bot's unread flag when its owner opens the conversation.
|
||||
*/
|
||||
export default async function markBotRead({ bot_id, user_id }: Params) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET unread = 0 WHERE id = ? AND user_id = ? AND unread = 1`,
|
||||
values: [bot_id, user_id],
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { TableType } from "@/src/types";
|
||||
import uniqueStringArr from "@/src/utils/unique-string-arr";
|
||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite";
|
||||
@@ -15,11 +15,11 @@ type Params = {
|
||||
/**
|
||||
* Allowed fields from joins
|
||||
*/
|
||||
include_fields?: (keyof BUN_SQLITE_BSQLITE_ALL_TYPEDEFS)[];
|
||||
include_fields?: (keyof BUN_SQLITE_COBBLE_ALL_TYPEDEFS)[];
|
||||
/**
|
||||
* Fields to exclude from joins
|
||||
*/
|
||||
exclude_fields?: (keyof BUN_SQLITE_BSQLITE_ALL_TYPEDEFS)[];
|
||||
exclude_fields?: (keyof BUN_SQLITE_COBBLE_ALL_TYPEDEFS)[];
|
||||
};
|
||||
|
||||
export default function checkAllowedJoins({
|
||||
@@ -53,7 +53,7 @@ export default function checkAllowedJoins({
|
||||
for (let i = 0; i < all_joins.length; i++) {
|
||||
const join = all_joins[i];
|
||||
if (!join) continue;
|
||||
const join_field = join.field as keyof BUN_SQLITE_BSQLITE_ALL_TYPEDEFS;
|
||||
const join_field = join.field as keyof BUN_SQLITE_COBBLE_ALL_TYPEDEFS;
|
||||
if (!include_fields.includes(join_field)) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS } from "@/db/types/db";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export type EmailAttachmentRecord = BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS & {
|
||||
letter_title?: string;
|
||||
invoice_number?: string;
|
||||
invoice_title?: string;
|
||||
quote_number?: string;
|
||||
quote_title?: string;
|
||||
media_write_path?: string;
|
||||
media_mime_type?: string;
|
||||
};
|
||||
|
||||
export default async function grabEmailAttachments({
|
||||
email_id,
|
||||
}: {
|
||||
email_id: number | string;
|
||||
}): Promise<APIResponseObject<EmailAttachmentRecord>> {
|
||||
return await BunSQLite.sql<EmailAttachmentRecord>({
|
||||
sql: `
|
||||
SELECT
|
||||
ea.*,
|
||||
l.title AS letter_title,
|
||||
i.invoice_number,
|
||||
i.title AS invoice_title,
|
||||
q.quote_number,
|
||||
q.title AS quote_title,
|
||||
m.media_write_path,
|
||||
m.mime_type AS media_mime_type
|
||||
FROM email_attachments ea
|
||||
LEFT JOIN letters l ON l.id = ea.letter_id
|
||||
LEFT JOIN invoices i ON i.id = ea.invoice_id
|
||||
LEFT JOIN quotes q ON q.id = ea.quote_id
|
||||
LEFT JOIN media m ON m.id = ea.media_id
|
||||
WHERE ea.email_id = ?
|
||||
ORDER BY ea.id ASC
|
||||
`,
|
||||
values: [email_id],
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { BUN_SQLITE_BSQLITE_EMAILS } from "@/db/types/db";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function grabEmail({
|
||||
id,
|
||||
}: {
|
||||
id: number | string;
|
||||
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_EMAILS>> {
|
||||
return await BunSQLite.select<BUN_SQLITE_BSQLITE_EMAILS>({
|
||||
table: "emails",
|
||||
targetId: id,
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { BUN_SQLITE_BSQLITE_LETTERS } from "@/db/types/db";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function grabLetter({
|
||||
id,
|
||||
}: {
|
||||
id: number | string;
|
||||
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_LETTERS>> {
|
||||
return await BunSQLite.select<BUN_SQLITE_BSQLITE_LETTERS>({
|
||||
table: "letters",
|
||||
targetId: id,
|
||||
});
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import grabNextDocumentSequence, {
|
||||
type DocumentNumberSqlRunner,
|
||||
} from "./grab-next-document-sequence";
|
||||
|
||||
let db: Database;
|
||||
let run_sql: DocumentNumberSqlRunner;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(":memory:");
|
||||
db.run("CREATE TABLE invoices (invoice_number TEXT UNIQUE)");
|
||||
|
||||
run_sql = async ({ sql, values }) => {
|
||||
const row = db.query(sql).get(...values) as
|
||||
| { max_seq: number | null }
|
||||
| null;
|
||||
|
||||
return { singleRes: row };
|
||||
};
|
||||
});
|
||||
|
||||
function insertNumbers(numbers: string[]) {
|
||||
const insert = db.prepare("INSERT INTO invoices (invoice_number) VALUES (?)");
|
||||
|
||||
for (const number of numbers) {
|
||||
insert.run(number);
|
||||
}
|
||||
}
|
||||
|
||||
describe("grabNextDocumentSequence", () => {
|
||||
test("starts at 1 for an empty table", async () => {
|
||||
const next = await grabNextDocumentSequence({
|
||||
table: "invoices",
|
||||
column: "invoice_number",
|
||||
prefix: "BSQLITE-202610",
|
||||
run_sql,
|
||||
});
|
||||
|
||||
expect(next).toBe(1);
|
||||
});
|
||||
|
||||
test("returns max + 1 for existing numbers", async () => {
|
||||
insertNumbers([
|
||||
"BSQLITE-202610-001",
|
||||
"BSQLITE-202610-002",
|
||||
"BSQLITE-202610-003",
|
||||
]);
|
||||
|
||||
const next = await grabNextDocumentSequence({
|
||||
table: "invoices",
|
||||
column: "invoice_number",
|
||||
prefix: "BSQLITE-202610",
|
||||
run_sql,
|
||||
});
|
||||
|
||||
expect(next).toBe(4);
|
||||
});
|
||||
|
||||
test("does not reuse a number after a deletion", async () => {
|
||||
insertNumbers(["BSQLITE-202610-001", "BSQLITE-202610-003"]);
|
||||
|
||||
const next = await grabNextDocumentSequence({
|
||||
table: "invoices",
|
||||
column: "invoice_number",
|
||||
prefix: "BSQLITE-202610",
|
||||
run_sql,
|
||||
});
|
||||
|
||||
expect(next).toBe(4);
|
||||
});
|
||||
|
||||
test("ignores numbers from other prefixes", async () => {
|
||||
insertNumbers([
|
||||
"BSQLITE-202609-099",
|
||||
"BSQLITEQ-202610-050",
|
||||
"BSQLITE-202610-002",
|
||||
]);
|
||||
|
||||
const next = await grabNextDocumentSequence({
|
||||
table: "invoices",
|
||||
column: "invoice_number",
|
||||
prefix: "BSQLITE-202610",
|
||||
run_sql,
|
||||
});
|
||||
|
||||
expect(next).toBe(3);
|
||||
});
|
||||
|
||||
test("handles sequences beyond three digits", async () => {
|
||||
insertNumbers(["BSQLITE-202610-009", "BSQLITE-202610-010"]);
|
||||
|
||||
const next = await grabNextDocumentSequence({
|
||||
table: "invoices",
|
||||
column: "invoice_number",
|
||||
prefix: "BSQLITE-202610",
|
||||
run_sql,
|
||||
});
|
||||
|
||||
expect(next).toBe(11);
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type DocumentNumberTarget = {
|
||||
table: "invoices" | "quotes";
|
||||
column: "invoice_number" | "quote_number";
|
||||
};
|
||||
|
||||
export type DocumentNumberSqlRunner = (args: {
|
||||
sql: string;
|
||||
values: (string | number)[];
|
||||
}) => Promise<{ singleRes?: { max_seq?: number | string | null } | null }>;
|
||||
|
||||
type Params = DocumentNumberTarget & {
|
||||
prefix: string;
|
||||
/**
|
||||
* Injectable query runner, used by tests to target an in-memory DB.
|
||||
*/
|
||||
run_sql?: DocumentNumberSqlRunner;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Grab the next document sequence.
|
||||
* Reads the highest existing numeric suffix for the prefix instead of
|
||||
* counting rows, so deleting a document can never hand out a number that
|
||||
* already exists.
|
||||
*/
|
||||
export default async function grabNextDocumentSequence({
|
||||
table,
|
||||
column,
|
||||
prefix,
|
||||
run_sql,
|
||||
}: Params): Promise<number> {
|
||||
const sql = `SELECT MAX(CAST(SUBSTR(${column}, ?) AS INTEGER)) AS max_seq FROM ${table} WHERE ${column} LIKE ?`;
|
||||
const values = [prefix.length + 2, `${prefix}-%`];
|
||||
|
||||
const res = run_sql
|
||||
? await run_sql({ sql, values })
|
||||
: await BunSQLite.sql({ sql, values });
|
||||
|
||||
return Number(res.singleRes?.max_seq || 0) + 1;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_INVOICE_ITEMS,
|
||||
BUN_SQLITE_BSQLITE_INVOICE_QUOTES,
|
||||
BUN_SQLITE_BSQLITE_INVOICES,
|
||||
} from "@/db/types/db";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function grabInvoice({
|
||||
id,
|
||||
}: {
|
||||
id: number | string;
|
||||
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_INVOICES>> {
|
||||
return await BunSQLite.select<BUN_SQLITE_BSQLITE_INVOICES>({
|
||||
table: "invoices",
|
||||
targetId: id,
|
||||
});
|
||||
}
|
||||
|
||||
export async function grabInvoiceItems({
|
||||
invoice_id,
|
||||
}: {
|
||||
invoice_id: number | string;
|
||||
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_INVOICE_ITEMS>> {
|
||||
return await BunSQLite.select<BUN_SQLITE_BSQLITE_INVOICE_ITEMS>({
|
||||
table: "invoice_items",
|
||||
query: {
|
||||
query: {
|
||||
invoice_id: {
|
||||
value: Number(invoice_id),
|
||||
},
|
||||
},
|
||||
order: [
|
||||
{ field: "sort_order", strategy: "ASC" },
|
||||
{ field: "id", strategy: "ASC" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type InvoiceQuoteLinkRecord = BUN_SQLITE_BSQLITE_INVOICE_QUOTES & {
|
||||
quote_number?: string;
|
||||
quote_title?: string;
|
||||
};
|
||||
|
||||
export async function grabInvoiceQuoteLinks({
|
||||
invoice_id,
|
||||
}: {
|
||||
invoice_id: number | string;
|
||||
}): Promise<APIResponseObject<InvoiceQuoteLinkRecord>> {
|
||||
return await BunSQLite.sql<InvoiceQuoteLinkRecord>({
|
||||
sql: `
|
||||
SELECT
|
||||
iq.*,
|
||||
q.quote_number,
|
||||
q.title AS quote_title
|
||||
FROM invoice_quotes iq
|
||||
LEFT JOIN quotes q ON q.id = iq.quote_id
|
||||
WHERE iq.invoice_id = ?
|
||||
ORDER BY iq.id ASC
|
||||
`,
|
||||
values: [invoice_id],
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA_JOIN } from "@/src/types/sql-joins";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA_JOIN } from "@/src/types/sql-joins";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
import _ from "lodash";
|
||||
import grabUsers from "../users/grab-users";
|
||||
|
||||
type Params = {
|
||||
media_res: APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_JOIN>;
|
||||
media_res: APIResponseObject<BUN_SQLITE_COBBLE_MEDIA_JOIN>;
|
||||
};
|
||||
|
||||
export default async function appendUsersToMedia({
|
||||
media_res,
|
||||
}: Params): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_JOIN>> {
|
||||
}: Params): Promise<APIResponseObject<BUN_SQLITE_COBBLE_MEDIA_JOIN>> {
|
||||
try {
|
||||
let new_media_res = _.cloneDeep(media_res);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from "lodash";
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
|
||||
import grabMediaFromParadigm from "@/src/functions/backend/db/media/grab-media-from-paraidigm";
|
||||
import type { MediaParadigmType, TableType, User } from "@/src/types";
|
||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
@@ -55,7 +55,7 @@ export default async function deleteMedia(params: Params) {
|
||||
const media_id_to_delete = media_ids_to_del[i];
|
||||
|
||||
const target_media = (
|
||||
await BunSQLite.select<BUN_SQLITE_BSQLITE_MEDIA>({
|
||||
await BunSQLite.select<BUN_SQLITE_COBBLE_MEDIA>({
|
||||
table: "media",
|
||||
targetId: media_id_to_delete,
|
||||
})
|
||||
@@ -77,7 +77,7 @@ export default async function deleteMedia(params: Params) {
|
||||
const media_id = media_to_delete_array[i];
|
||||
|
||||
const target_media = (
|
||||
await BunSQLite.select<BUN_SQLITE_BSQLITE_MEDIA>({
|
||||
await BunSQLite.select<BUN_SQLITE_COBBLE_MEDIA>({
|
||||
table: "media",
|
||||
targetId: media_id,
|
||||
})
|
||||
@@ -101,7 +101,7 @@ export default async function deleteMedia(params: Params) {
|
||||
);
|
||||
}
|
||||
|
||||
await BunSQLite.delete<BUN_SQLITE_BSQLITE_MEDIA, TableType>({
|
||||
await BunSQLite.delete<BUN_SQLITE_COBBLE_MEDIA, TableType>({
|
||||
table: "media",
|
||||
targetId: target_media.id,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA_PARADIGMS } from "@/db/types/db";
|
||||
import type { MediaParadigmType, TableType } from "@/src/types";
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS_JOIN } from "@/src/types/sql-joins";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA_PARADIGMS_JOIN } from "@/src/types/sql-joins";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
@@ -13,10 +13,10 @@ type Params = {
|
||||
export default async function grabMediaFromParadigm({
|
||||
media_paradigm,
|
||||
user_id,
|
||||
}: Params): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS_JOIN>> {
|
||||
}: Params): Promise<APIResponseObject<BUN_SQLITE_COBBLE_MEDIA_PARADIGMS_JOIN>> {
|
||||
try {
|
||||
const media_paradigms_res = await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
|
||||
BUN_SQLITE_COBBLE_MEDIA_PARADIGMS,
|
||||
TableType
|
||||
>({
|
||||
table: "media_paradigms",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { ApiReqParams, User } from "@/src/types";
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA_JOIN } from "@/src/types/sql-joins";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA_JOIN } from "@/src/types/sql-joins";
|
||||
import BunSQLite, { type APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
import type { SQLInsertGenValueType } from "@moduletrace/bun-sqlite";
|
||||
import _ from "lodash";
|
||||
@@ -14,7 +14,7 @@ type Params = {
|
||||
|
||||
export default async function grabMedia(
|
||||
params?: Params,
|
||||
): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_JOIN>> {
|
||||
): Promise<APIResponseObject<BUN_SQLITE_COBBLE_MEDIA_JOIN>> {
|
||||
const { query, user } = params || {};
|
||||
|
||||
try {
|
||||
@@ -72,7 +72,7 @@ export default async function grabMedia(
|
||||
|
||||
// 4. Parameterized Search Terms
|
||||
if (query?.search_terms?.length) {
|
||||
const fields_to_search: (keyof BUN_SQLITE_BSQLITE_MEDIA)[] = [
|
||||
const fields_to_search: (keyof BUN_SQLITE_COBBLE_MEDIA)[] = [
|
||||
"title",
|
||||
"description",
|
||||
"summary",
|
||||
@@ -109,7 +109,7 @@ export default async function grabMedia(
|
||||
const final_sql = `${sql}\n${sql_limit}`;
|
||||
const final_values = [...values, ...limitValues];
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_BSQLITE_MEDIA_JOIN>({
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_MEDIA_JOIN>({
|
||||
sql: final_sql,
|
||||
values: final_values,
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_QUOTE_ITEMS,
|
||||
BUN_SQLITE_BSQLITE_QUOTES,
|
||||
} from "@/db/types/db";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function grabQuote({
|
||||
id,
|
||||
}: {
|
||||
id: number | string;
|
||||
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_QUOTES>> {
|
||||
return await BunSQLite.select<BUN_SQLITE_BSQLITE_QUOTES>({
|
||||
table: "quotes",
|
||||
targetId: id,
|
||||
});
|
||||
}
|
||||
|
||||
export async function grabQuoteItems({
|
||||
quote_id,
|
||||
}: {
|
||||
quote_id: number | string;
|
||||
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_QUOTE_ITEMS>> {
|
||||
return await BunSQLite.select<BUN_SQLITE_BSQLITE_QUOTE_ITEMS>({
|
||||
table: "quote_items",
|
||||
query: {
|
||||
query: {
|
||||
quote_id: {
|
||||
value: Number(quote_id),
|
||||
},
|
||||
},
|
||||
order: [
|
||||
{ field: "sort_order", strategy: "ASC" },
|
||||
{ field: "id", strategy: "ASC" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
@@ -13,9 +13,9 @@ type Params = {
|
||||
*/
|
||||
export default async function grabUserTypes({
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_BSQLITE_USER_TYPES[]> {
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_USER_TYPES[]> {
|
||||
const res = await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USER_TYPES,
|
||||
BUN_SQLITE_COBBLE_USER_TYPES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "user_types",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { ApiReqParams, SQLInsertGenValueType } from "@/src/types";
|
||||
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
@@ -17,7 +17,7 @@ type Params = {
|
||||
|
||||
export default async function grabUsers(
|
||||
params?: Params,
|
||||
): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_USERS_JOIN>> {
|
||||
): Promise<APIResponseObject<BUN_SQLITE_COBBLE_USERS_JOIN>> {
|
||||
const { query } = params || {};
|
||||
|
||||
try {
|
||||
@@ -123,7 +123,7 @@ export default async function grabUsers(
|
||||
sql_limit += `OFFSET ${offset}\n`;
|
||||
}
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_BSQLITE_USERS_JOIN>({
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_USERS_JOIN>({
|
||||
sql: `${sql}\n${sql_limit}`,
|
||||
values,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA_PARADIGMS,
|
||||
BUN_SQLITE_COBBLE_USERS,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
@@ -26,12 +26,12 @@ import deleteMedia from "../../media/delete-media";
|
||||
type Params = {
|
||||
media_paradigms?: MediaParadigm[];
|
||||
media_type?: MediaType;
|
||||
user: BUN_SQLITE_BSQLITE_USERS | User;
|
||||
user: BUN_SQLITE_COBBLE_USERS | User;
|
||||
data?: MediaDataType;
|
||||
is_primary?: boolean;
|
||||
update_on_duplicate?: boolean;
|
||||
extract_media_text_content?: boolean;
|
||||
media?: BUN_SQLITE_BSQLITE_MEDIA;
|
||||
media?: BUN_SQLITE_COBBLE_MEDIA;
|
||||
media_name?: string;
|
||||
media_mime_type?: string;
|
||||
is_private?: boolean;
|
||||
@@ -59,7 +59,7 @@ export default async function uploadAndRecordMedia({
|
||||
thumbnail_width,
|
||||
existing_media_id,
|
||||
media_text_content: passed_media_text_content,
|
||||
}: Params): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA>> {
|
||||
}: Params): Promise<APIResponseObject<BUN_SQLITE_COBBLE_MEDIA>> {
|
||||
const now = Date.now();
|
||||
let target_file_name = `${media_name || now}`;
|
||||
let target_file_thumbnail_name = `${media_name || now}-thumb`;
|
||||
@@ -207,7 +207,7 @@ export default async function uploadAndRecordMedia({
|
||||
|
||||
if (is_primary) {
|
||||
const existing_user_primary_media = await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
TableType
|
||||
>({
|
||||
table: "media",
|
||||
@@ -227,7 +227,7 @@ export default async function uploadAndRecordMedia({
|
||||
}
|
||||
}
|
||||
|
||||
const media_data: BUN_SQLITE_BSQLITE_MEDIA = {
|
||||
const media_data: BUN_SQLITE_COBBLE_MEDIA = {
|
||||
...media,
|
||||
user_id: Number(user.id),
|
||||
is_primary: is_primary ? 1 : 0,
|
||||
@@ -239,11 +239,11 @@ export default async function uploadAndRecordMedia({
|
||||
text_content: media_text_content,
|
||||
};
|
||||
|
||||
let media_record: BUN_SQLITE_BSQLITE_MEDIA | undefined = undefined;
|
||||
let media_record: BUN_SQLITE_COBBLE_MEDIA | undefined = undefined;
|
||||
|
||||
if (existing_media_id) {
|
||||
const update_media_record = await BunSQLite.update<
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
TableType
|
||||
>({
|
||||
data: media_data,
|
||||
@@ -254,7 +254,7 @@ export default async function uploadAndRecordMedia({
|
||||
media_record = update_media_record.singleRes || undefined;
|
||||
} else {
|
||||
const new_media_record = await BunSQLite.insert<
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
TableType
|
||||
>({
|
||||
data: [media_data],
|
||||
@@ -271,7 +271,7 @@ export default async function uploadAndRecordMedia({
|
||||
|
||||
if (media_paradigms?.[0] && media_record?.id) {
|
||||
const delete_existing_paradigms = await BunSQLite.delete<
|
||||
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
|
||||
BUN_SQLITE_COBBLE_MEDIA_PARADIGMS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "media_paradigms",
|
||||
@@ -281,7 +281,7 @@ export default async function uploadAndRecordMedia({
|
||||
});
|
||||
|
||||
await BunSQLite.insert<
|
||||
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
|
||||
BUN_SQLITE_COBBLE_MEDIA_PARADIGMS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
data: media_paradigms.map((mp) => ({
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
import { formatInvoiceDate } from "@/src/functions/backend/pdf/letter-date";
|
||||
import { CompanyLetterheadDefaults } from "@/src/dict/company-dict";
|
||||
import { formatNaira } from "@/src/utils/money";
|
||||
import { ThemeColors } from "@/src/functions/backend/theme/theme-colors";
|
||||
|
||||
const letter_divider = (
|
||||
<hr
|
||||
style={{
|
||||
border: "none",
|
||||
borderTop: `1px solid ${ThemeColors.hairline}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
type InvoiceEmailContentProps = {
|
||||
invoice: {
|
||||
invoice_number?: string;
|
||||
client_name?: string;
|
||||
issue_date?: number | "";
|
||||
due_date?: number | "";
|
||||
status?: string;
|
||||
notes?: string;
|
||||
};
|
||||
items: {
|
||||
description?: string;
|
||||
quantity?: number | string | "";
|
||||
unit_price_kobo?: number | string | "";
|
||||
}[];
|
||||
};
|
||||
|
||||
type QuoteEmailContentProps = {
|
||||
quote: {
|
||||
quote_number?: string;
|
||||
client_name?: string;
|
||||
issue_date?: number | "";
|
||||
valid_until?: number | "";
|
||||
status?: string;
|
||||
notes?: string;
|
||||
};
|
||||
items: {
|
||||
description?: string;
|
||||
quantity?: number | string | "";
|
||||
unit_price_kobo?: number | string | "";
|
||||
}[];
|
||||
};
|
||||
|
||||
export function InvoiceEmailContent({
|
||||
invoice,
|
||||
items,
|
||||
}: InvoiceEmailContentProps) {
|
||||
const total_kobo = items.reduce((acc, item) => {
|
||||
return (
|
||||
acc +
|
||||
(Number(item.quantity) || 0) * (Number(item.unit_price_kobo) || 0)
|
||||
);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: "600",
|
||||
color: ThemeColors.ink,
|
||||
}}
|
||||
>
|
||||
{CompanyLetterheadDefaults.name}
|
||||
</div>
|
||||
{letter_divider}
|
||||
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
margin: "0 0 6px",
|
||||
}}
|
||||
>
|
||||
Invoice {invoice.invoice_number || ""}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.7",
|
||||
color: ThemeColors.body,
|
||||
}}
|
||||
>
|
||||
Hello {invoice.client_name || "there"},
|
||||
<br />
|
||||
Please find attached invoice {invoice.invoice_number || ""} for
|
||||
your records.
|
||||
</div>
|
||||
|
||||
{letter_divider}
|
||||
|
||||
<div style={{ fontSize: "14px", lineHeight: "1.6" }}>
|
||||
<table
|
||||
width="100%"
|
||||
cellPadding="0"
|
||||
cellSpacing="0"
|
||||
style={{ fontSize: "13px", borderCollapse: "collapse" }}
|
||||
>
|
||||
<tr
|
||||
style={{
|
||||
background: ThemeColors.ink,
|
||||
color: ThemeColors.surface,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: "8px 10px" }}>Description</td>
|
||||
<td align="right" style={{ padding: "8px 10px" }}>
|
||||
Qty
|
||||
</td>
|
||||
<td align="right" style={{ padding: "8px 10px" }}>
|
||||
Amount
|
||||
</td>
|
||||
</tr>
|
||||
{items.map((item, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
style={
|
||||
index % 2 == 0
|
||||
? { background: ThemeColors.faint }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<td style={{ padding: "7px 10px" }}>
|
||||
{item.description}
|
||||
</td>
|
||||
<td align="right" style={{ padding: "7px 10px" }}>
|
||||
{Number(item.quantity) || 0}
|
||||
</td>
|
||||
<td align="right" style={{ padding: "7px 10px" }}>
|
||||
{formatNaira(
|
||||
(Number(item.quantity) || 0) *
|
||||
(Number(item.unit_price_kobo) || 0),
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
align="right"
|
||||
style={{
|
||||
padding: "9px 10px",
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
Total (NGN)
|
||||
</td>
|
||||
<td
|
||||
align="right"
|
||||
style={{
|
||||
padding: "9px 10px",
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
{formatNaira(total_kobo)}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{letter_divider}
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.7",
|
||||
color: ThemeColors.body,
|
||||
}}
|
||||
>
|
||||
Invoice {invoice.invoice_number || ""} was issued on{" "}
|
||||
{formatInvoiceDate(invoice.issue_date || Date.now())}
|
||||
{invoice.due_date
|
||||
? ` and is due by ${formatInvoiceDate(invoice.due_date)}`
|
||||
: ""}
|
||||
.
|
||||
<br />
|
||||
<br />
|
||||
Thank you for your business.
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: ThemeColors.muted,
|
||||
borderTop: `1px solid ${ThemeColors.hairline}`,
|
||||
paddingTop: "12px",
|
||||
}}
|
||||
>
|
||||
{CompanyLetterheadDefaults.name} -{" "}
|
||||
{CompanyLetterheadDefaults.address_lines.join(", ")} |{" "}
|
||||
{CompanyLetterheadDefaults.phone} |{" "}
|
||||
{CompanyLetterheadDefaults.email} |{" "}
|
||||
{CompanyLetterheadDefaults.website}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function QuoteEmailContent({ quote, items }: QuoteEmailContentProps) {
|
||||
const total_kobo = items.reduce((acc, item) => {
|
||||
return (
|
||||
acc +
|
||||
(Number(item.quantity) || 0) * (Number(item.unit_price_kobo) || 0)
|
||||
);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: "600",
|
||||
color: ThemeColors.ink,
|
||||
}}
|
||||
>
|
||||
{CompanyLetterheadDefaults.name}
|
||||
</div>
|
||||
{letter_divider}
|
||||
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
margin: "0 0 6px",
|
||||
}}
|
||||
>
|
||||
Quotation {quote.quote_number || ""}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.7",
|
||||
color: ThemeColors.body,
|
||||
}}
|
||||
>
|
||||
Hello {quote.client_name || "there"},
|
||||
<br />
|
||||
Please find attached quotation {quote.quote_number || ""} for
|
||||
our proposed works.
|
||||
</div>
|
||||
|
||||
{letter_divider}
|
||||
|
||||
<div style={{ fontSize: "14px", lineHeight: "1.6" }}>
|
||||
<table
|
||||
width="100%"
|
||||
cellPadding="0"
|
||||
cellSpacing="0"
|
||||
style={{ fontSize: "13px", borderCollapse: "collapse" }}
|
||||
>
|
||||
<tr
|
||||
style={{
|
||||
background: ThemeColors.ink,
|
||||
color: ThemeColors.surface,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: "8px 10px" }}>Description</td>
|
||||
<td align="right" style={{ padding: "8px 10px" }}>
|
||||
Qty
|
||||
</td>
|
||||
<td align="right" style={{ padding: "8px 10px" }}>
|
||||
Amount
|
||||
</td>
|
||||
</tr>
|
||||
{items.map((item, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
style={
|
||||
index % 2 == 0
|
||||
? { background: ThemeColors.faint }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<td style={{ padding: "7px 10px" }}>
|
||||
{item.description}
|
||||
</td>
|
||||
<td align="right" style={{ padding: "7px 10px" }}>
|
||||
{Number(item.quantity) || 0}
|
||||
</td>
|
||||
<td align="right" style={{ padding: "7px 10px" }}>
|
||||
{formatNaira(
|
||||
(Number(item.quantity) || 0) *
|
||||
(Number(item.unit_price_kobo) || 0),
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
align="right"
|
||||
style={{
|
||||
padding: "9px 10px",
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
Total (NGN)
|
||||
</td>
|
||||
<td
|
||||
align="right"
|
||||
style={{
|
||||
padding: "9px 10px",
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
{formatNaira(total_kobo)}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{letter_divider}
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.7",
|
||||
color: ThemeColors.body,
|
||||
}}
|
||||
>
|
||||
Quotation {quote.quote_number || ""} was issued on{" "}
|
||||
{formatInvoiceDate(quote.issue_date || Date.now())}
|
||||
{quote.valid_until
|
||||
? ` and is valid until ${formatInvoiceDate(quote.valid_until)}`
|
||||
: ""}
|
||||
.
|
||||
<br />
|
||||
<br />
|
||||
We look forward to working with you.
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: ThemeColors.muted,
|
||||
borderTop: `1px solid ${ThemeColors.hairline}`,
|
||||
paddingTop: "12px",
|
||||
}}
|
||||
>
|
||||
{CompanyLetterheadDefaults.name} -{" "}
|
||||
{CompanyLetterheadDefaults.address_lines.join(", ")} |{" "}
|
||||
{CompanyLetterheadDefaults.phone} |{" "}
|
||||
{CompanyLetterheadDefaults.email} |{" "}
|
||||
{CompanyLetterheadDefaults.website}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
|
||||
type Params = {
|
||||
media?: BUN_SQLITE_BSQLITE_MEDIA;
|
||||
user?: BUN_SQLITE_BSQLITE_USERS_JOIN;
|
||||
media?: BUN_SQLITE_COBBLE_MEDIA;
|
||||
user?: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
};
|
||||
|
||||
export default function grabMediaURL({ media: passed_media, user }: Params) {
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import htmlToTextBlocks from "./html-blocks";
|
||||
|
||||
test("parses paragraphs and inline styles", () => {
|
||||
const blocks = htmlToTextBlocks(
|
||||
`<p>Hello <strong>world</strong> and <em>friends</em>.</p><p>Second <u>underlined</u> paragraph.</p>`,
|
||||
);
|
||||
|
||||
expect(blocks).toHaveLength(2);
|
||||
expect(blocks[0]!.type).toBe("paragraph");
|
||||
expect((blocks[0] as any).segments).toEqual([
|
||||
{ text: "Hello " },
|
||||
{ text: "world", bold: true },
|
||||
{ text: " and " },
|
||||
{ text: "friends", italic: true },
|
||||
{ text: "." },
|
||||
]);
|
||||
expect((blocks[1] as any).segments).toEqual([
|
||||
{ text: "Second " },
|
||||
{ text: "underlined", underline: true },
|
||||
{ text: " paragraph." },
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses lists with ordering", () => {
|
||||
const blocks = htmlToTextBlocks(
|
||||
`<ul><li>One</li><li>Two</li></ul><ol><li>First</li></ol>`,
|
||||
);
|
||||
|
||||
expect(blocks).toHaveLength(3);
|
||||
expect(blocks[0]!).toEqual({
|
||||
type: "list-item",
|
||||
ordered: false,
|
||||
index: 0,
|
||||
segments: [{ text: "One" }],
|
||||
});
|
||||
expect(blocks[1]!).toEqual({
|
||||
type: "list-item",
|
||||
ordered: false,
|
||||
index: 1,
|
||||
segments: [{ text: "Two" }],
|
||||
});
|
||||
expect(blocks[2]!).toEqual({
|
||||
type: "list-item",
|
||||
ordered: true,
|
||||
index: 0,
|
||||
segments: [{ text: "First" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("parses headings, blockquote, hr and br", () => {
|
||||
const blocks = htmlToTextBlocks(
|
||||
`<h2>Heading</h2><blockquote>Quoted</blockquote><hr><p>Line one<br>Line two</p>`,
|
||||
);
|
||||
|
||||
expect(blocks[0]!.type).toBe("heading");
|
||||
expect((blocks[0] as any).level).toBe(2);
|
||||
expect(blocks[1]!.type).toBe("blockquote");
|
||||
expect(blocks[2]!).toEqual({ type: "divider" });
|
||||
// <br> splits into separate paragraphs
|
||||
expect(blocks).toHaveLength(5);
|
||||
expect((blocks[3] as any).segments.map((s: any) => s.text).join("")).toBe(
|
||||
"Line one",
|
||||
);
|
||||
expect((blocks[4] as any).segments.map((s: any) => s.text).join("")).toBe(
|
||||
"Line two",
|
||||
);
|
||||
});
|
||||
|
||||
test("decodes HTML entities", () => {
|
||||
const blocks = htmlToTextBlocks(
|
||||
`<p>Fee & charges — £500 > 400</p>`,
|
||||
);
|
||||
|
||||
expect((blocks[0] as any).segments[0].text).toBe(
|
||||
"Fee & charges \u2014 \u00a3500 > 400",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses img alt text and ignores unknown tags", () => {
|
||||
const blocks = htmlToTextBlocks(
|
||||
`<p>See <img src="x.png" alt="the chart" /> below.</p><svg><circle></circle></svg>`,
|
||||
);
|
||||
|
||||
expect((blocks[0] as any).segments.map((s: any) => s.text).join("")).toBe(
|
||||
"See the chart below.",
|
||||
);
|
||||
});
|
||||
|
||||
test("returns empty for empty input", () => {
|
||||
expect(htmlToTextBlocks(undefined)).toEqual([]);
|
||||
expect(htmlToTextBlocks("")).toEqual([]);
|
||||
});
|
||||
@@ -1,262 +0,0 @@
|
||||
export type TextSegment = {
|
||||
text: string;
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
underline?: boolean;
|
||||
};
|
||||
|
||||
export type TextBlock =
|
||||
| { type: "paragraph"; segments: TextSegment[] }
|
||||
| { type: "heading"; level: number; segments: TextSegment[] }
|
||||
| { type: "list-item"; ordered: boolean; index: number; segments: TextSegment[] }
|
||||
| { type: "blockquote"; segments: TextSegment[] }
|
||||
| { type: "divider" };
|
||||
|
||||
const named_entities: Record<string, string> = {
|
||||
amp: "&",
|
||||
lt: "<",
|
||||
gt: ">",
|
||||
quot: '"',
|
||||
apos: "'",
|
||||
nbsp: " ",
|
||||
ensp: " ",
|
||||
emsp: " ",
|
||||
ndash: "\u2013",
|
||||
mdash: "\u2014",
|
||||
hellip: "\u2026",
|
||||
lsquo: "\u2018",
|
||||
rsquo: "\u2019",
|
||||
ldquo: "\u201c",
|
||||
rdquo: "\u201d",
|
||||
bull: "\u2022",
|
||||
middot: "\u00b7",
|
||||
times: "\u00d7",
|
||||
copy: "\u00a9",
|
||||
reg: "\u00ae",
|
||||
trade: "\u2122",
|
||||
deg: "\u00b0",
|
||||
sect: "\u00a7",
|
||||
para: "\u00b6",
|
||||
euro: "\u20ac",
|
||||
pound: "\u00a3",
|
||||
minus: "\u2212",
|
||||
radic: "\u221a",
|
||||
frac12: "\u00bd",
|
||||
frac14: "\u00bc",
|
||||
frac34: "\u00be",
|
||||
};
|
||||
|
||||
function decodeEntities(text: string): string {
|
||||
return text
|
||||
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) =>
|
||||
String.fromCodePoint(parseInt(hex, 16)),
|
||||
)
|
||||
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)))
|
||||
.replace(
|
||||
/&([a-zA-Z]+);/g,
|
||||
(match, name: string) => named_entities[name.toLowerCase()] || match,
|
||||
);
|
||||
}
|
||||
|
||||
const INLINE_STYLE_OPEN = ["b", "strong", "i", "em", "u", "ins"];
|
||||
const INLINE_STYLE_CLOSE = ["b", "strong", "i", "em", "u", "ins"];
|
||||
const HEADING_LEVELS: Record<string, number> = {
|
||||
h1: 1,
|
||||
h2: 2,
|
||||
h3: 3,
|
||||
h4: 4,
|
||||
h5: 5,
|
||||
h6: 6,
|
||||
};
|
||||
|
||||
/**
|
||||
* # Convert an HTML fragment (TinyMCE output) into simple text blocks
|
||||
* suitable for rendering in a PDF. Supports: p, div, br, h1-h6, ul/ol/li,
|
||||
* blockquote, hr, b/strong, i/em, u/ins, a, span, img (alt text) and
|
||||
* basic table cells (degraded to inline text separated by spaces).
|
||||
*/
|
||||
export default function htmlToTextBlocks(html?: string): TextBlock[] {
|
||||
const source = html || "";
|
||||
const blocks: TextBlock[] = [];
|
||||
|
||||
let pending_text = "";
|
||||
const style = { bold: false, italic: false, underline: false };
|
||||
let current: TextBlock | undefined;
|
||||
const list_stack: { ordered: boolean; index: number }[] = [];
|
||||
|
||||
const appendText = (raw: string) => {
|
||||
if (!raw) return;
|
||||
const decoded = decodeEntities(raw);
|
||||
if (!decoded) return;
|
||||
pending_text += decoded;
|
||||
};
|
||||
|
||||
const flushText = () => {
|
||||
if (!pending_text) return;
|
||||
if (!current) {
|
||||
current = { type: "paragraph", segments: [] };
|
||||
} else if (current.type == "divider") {
|
||||
current = { type: "paragraph", segments: [] };
|
||||
}
|
||||
current.segments.push({
|
||||
text: pending_text,
|
||||
...(style.bold ? { bold: true } : {}),
|
||||
...(style.italic ? { italic: true } : {}),
|
||||
...(style.underline ? { underline: true } : {}),
|
||||
});
|
||||
pending_text = "";
|
||||
};
|
||||
|
||||
const flushBlock = () => {
|
||||
flushText();
|
||||
if (current) {
|
||||
blocks.push(current);
|
||||
current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const startBlock = (block: TextBlock) => {
|
||||
flushBlock();
|
||||
current = block;
|
||||
};
|
||||
|
||||
const pattern = /<(\/?)([a-zA-Z0-9]+)([^>]*)>/g;
|
||||
let last = 0;
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(source)) !== null) {
|
||||
appendText(source.slice(last, match.index));
|
||||
last = match.index + (match[0] || "").length;
|
||||
|
||||
const closing = Boolean(match[1]);
|
||||
const tag = (match[2] || "").toLowerCase();
|
||||
const attrs = match[3] || "";
|
||||
|
||||
if (INLINE_STYLE_OPEN.includes(tag) && !closing) {
|
||||
if (["b", "strong"].includes(tag)) {
|
||||
flushText();
|
||||
style.bold = true;
|
||||
}
|
||||
if (["i", "em"].includes(tag)) {
|
||||
flushText();
|
||||
style.italic = true;
|
||||
}
|
||||
if (["u", "ins"].includes(tag)) {
|
||||
flushText();
|
||||
style.underline = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (INLINE_STYLE_CLOSE.includes(tag) && closing) {
|
||||
if (["b", "strong"].includes(tag)) {
|
||||
flushText();
|
||||
style.bold = false;
|
||||
}
|
||||
if (["i", "em"].includes(tag)) {
|
||||
flushText();
|
||||
style.italic = false;
|
||||
}
|
||||
if (["u", "ins"].includes(tag)) {
|
||||
flushText();
|
||||
style.underline = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (["p", "div"].includes(tag)) {
|
||||
if (closing) {
|
||||
flushBlock();
|
||||
} else {
|
||||
startBlock({ type: "paragraph", segments: [] });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HEADING_LEVELS[tag]) {
|
||||
if (closing) {
|
||||
flushBlock();
|
||||
} else {
|
||||
startBlock({
|
||||
type: "heading",
|
||||
level: HEADING_LEVELS[tag],
|
||||
segments: [],
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag == "br") {
|
||||
flushBlock();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag == "hr") {
|
||||
flushBlock();
|
||||
blocks.push({ type: "divider" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (["ul", "ol"].includes(tag)) {
|
||||
if (closing) {
|
||||
flushBlock();
|
||||
list_stack.pop();
|
||||
} else {
|
||||
flushBlock();
|
||||
list_stack.push({ ordered: tag == "ol", index: 0 });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag == "li") {
|
||||
if (closing) {
|
||||
flushBlock();
|
||||
} else {
|
||||
const list = list_stack[list_stack.length - 1] || {
|
||||
ordered: false,
|
||||
index: 0,
|
||||
};
|
||||
startBlock({
|
||||
type: "list-item",
|
||||
ordered: list.ordered,
|
||||
index: list.index,
|
||||
segments: [],
|
||||
});
|
||||
list.index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag == "blockquote") {
|
||||
if (closing) {
|
||||
flushBlock();
|
||||
} else {
|
||||
startBlock({ type: "blockquote", segments: [] });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag == "img" && !closing) {
|
||||
const alt = attrs.match(/alt\s*=\s*["']([^"']*)["']/i);
|
||||
if (alt?.[1]) appendText(` ${alt[1]} `);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (["td", "th", "p", "div", "li"].includes(tag) && !closing) {
|
||||
appendText(" ");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (["table", "thead", "tbody", "tr"].includes(tag)) {
|
||||
if (!closing && ["tr"].includes(tag)) {
|
||||
// no-op, cells add their own spacing
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
appendText(source.slice(last));
|
||||
flushBlock();
|
||||
|
||||
return blocks;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
function ordinal(n: number): string {
|
||||
const mod100 = n % 100;
|
||||
if (mod100 >= 11 && mod100 <= 13) {
|
||||
return "th";
|
||||
}
|
||||
switch (n % 10) {
|
||||
case 1:
|
||||
return "st";
|
||||
case 2:
|
||||
return "nd";
|
||||
case 3:
|
||||
return "rd";
|
||||
default:
|
||||
return "th";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* # Format a timestamp as a letter date, e.g. "22nd September, 2026".
|
||||
*/
|
||||
export function formatLetterDate(timestamp: number): string {
|
||||
const date = dayjs(timestamp);
|
||||
return `${date.date()}${ordinal(date.date())} ${date.format("MMMM, YYYY")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* # Format a timestamp as a short invoice date, e.g. "22 Sep 2026".
|
||||
*/
|
||||
export function formatInvoiceDate(
|
||||
timestamp?: number | string | "",
|
||||
): string {
|
||||
if (!timestamp) {
|
||||
return "\u2014";
|
||||
}
|
||||
return dayjs(Number(timestamp)).format("DD MMM YYYY");
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import {
|
||||
CompanyLetterheadDefaults,
|
||||
type CompanyLetterhead,
|
||||
} from "@/src/dict/company-dict";
|
||||
import {
|
||||
PdfColors,
|
||||
PdfMargin,
|
||||
PdfPageHeight,
|
||||
PdfPageWidth,
|
||||
drawBrandFooterOnAllPages,
|
||||
drawBrandHeader,
|
||||
drawTextAt,
|
||||
embedBrandLogo,
|
||||
embedPdfFonts,
|
||||
type PdfFonts,
|
||||
} from "./pdf-helpers";
|
||||
import htmlToTextBlocks from "./html-blocks";
|
||||
import { formatLetterDate } from "./letter-date";
|
||||
import {
|
||||
ensureSpace,
|
||||
renderBlocks,
|
||||
type RenderContext,
|
||||
} from "./render-blocks";
|
||||
|
||||
type TemplateOverview = {
|
||||
title?: string;
|
||||
to_name?: string;
|
||||
to_address?: string;
|
||||
from_name?: string;
|
||||
from_address?: string;
|
||||
salutation?: string;
|
||||
body_html?: string;
|
||||
closing?: string;
|
||||
signature_name?: string;
|
||||
signature_title?: string;
|
||||
};
|
||||
|
||||
export type LetterPdfParams = {
|
||||
template: TemplateOverview;
|
||||
/** Overrides the template's default recipient at send-time. */
|
||||
to_name?: string;
|
||||
to_address?: string;
|
||||
/** Unix timestamp in ms. Defaults to now. */
|
||||
letter_date?: number;
|
||||
company?: CompanyLetterhead;
|
||||
};
|
||||
|
||||
const BODY_SIZE = 11;
|
||||
const BODY_LINE_GAP = 15.5;
|
||||
|
||||
export default async function generateLetterPdf({
|
||||
template,
|
||||
to_name,
|
||||
to_address,
|
||||
letter_date,
|
||||
company,
|
||||
}: LetterPdfParams): Promise<Uint8Array> {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
const fonts = await embedPdfFonts(pdfDoc);
|
||||
const logo = await embedBrandLogo(pdfDoc);
|
||||
const company_block = {
|
||||
...CompanyLetterheadDefaults,
|
||||
...(company || {}),
|
||||
};
|
||||
|
||||
let page = pdfDoc.addPage([PdfPageWidth, PdfPageHeight]);
|
||||
const ctx: RenderContext = {
|
||||
pdfDoc,
|
||||
page,
|
||||
fonts,
|
||||
y: drawBrandHeader({
|
||||
page,
|
||||
fonts,
|
||||
options: { logo, ...company_block },
|
||||
}),
|
||||
};
|
||||
|
||||
const from_name = template.from_name?.trim();
|
||||
const from_address = template.from_address?.trim();
|
||||
|
||||
if (from_name || from_address) {
|
||||
ensureSpace(ctx, 80);
|
||||
|
||||
ctx.y += 6;
|
||||
drawTextAt(ctx.page, "From:", {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 9,
|
||||
font: fonts.bold,
|
||||
color: PdfColors.darkGray,
|
||||
});
|
||||
ctx.y += 14;
|
||||
|
||||
if (from_name) {
|
||||
drawTextAt(ctx.page, from_name, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 10.5,
|
||||
font: fonts.bold,
|
||||
color: PdfColors.black,
|
||||
});
|
||||
ctx.y += 13.5;
|
||||
}
|
||||
|
||||
if (from_address) {
|
||||
for (const line of from_address.split(/\r?\n/)) {
|
||||
drawTextAt(ctx.page, line, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 10.5,
|
||||
font: fonts.regular,
|
||||
color: PdfColors.black,
|
||||
});
|
||||
ctx.y += 13;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.y += 10;
|
||||
}
|
||||
|
||||
const date_text = formatLetterDate(letter_date || Date.now());
|
||||
|
||||
drawTextAt(ctx.page, date_text, {
|
||||
x: PdfPageWidth - PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 10.5,
|
||||
font: fonts.regular,
|
||||
color: PdfColors.black,
|
||||
align: "right",
|
||||
});
|
||||
ctx.y += 24;
|
||||
|
||||
const final_to_name = (to_name || template.to_name || "").trim();
|
||||
const final_to_address = (to_address || template.to_address || "")
|
||||
.trim();
|
||||
|
||||
if (final_to_name) {
|
||||
drawTextAt(ctx.page, final_to_name, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 11,
|
||||
font: fonts.bold,
|
||||
color: PdfColors.black,
|
||||
});
|
||||
ctx.y += 14.5;
|
||||
}
|
||||
|
||||
if (final_to_address) {
|
||||
for (const line of final_to_address.split(/\r?\n/)) {
|
||||
drawTextAt(ctx.page, line, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 10.5,
|
||||
font: fonts.regular,
|
||||
color: PdfColors.black,
|
||||
});
|
||||
ctx.y += 13;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.y += 18;
|
||||
|
||||
const salutation = template.salutation?.trim();
|
||||
|
||||
if (salutation) {
|
||||
drawTextAt(ctx.page, salutation, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: BODY_SIZE,
|
||||
font: fonts.regular,
|
||||
color: PdfColors.black,
|
||||
});
|
||||
ctx.y += BODY_LINE_GAP + 6;
|
||||
}
|
||||
|
||||
const blocks = htmlToTextBlocks(template.body_html);
|
||||
|
||||
if (blocks[0]) {
|
||||
renderBlocks({ blocks, fonts, ctx });
|
||||
}
|
||||
|
||||
const closing = template.closing?.trim();
|
||||
const signature_name = template.signature_name?.trim();
|
||||
const signature_title = template.signature_title?.trim();
|
||||
|
||||
if (closing || signature_name) {
|
||||
ensureSpace(ctx, 120);
|
||||
|
||||
ctx.y += 28;
|
||||
|
||||
if (closing) {
|
||||
drawTextAt(ctx.page, closing, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: BODY_SIZE,
|
||||
font: fonts.regular,
|
||||
color: PdfColors.black,
|
||||
});
|
||||
ctx.y += 30;
|
||||
}
|
||||
|
||||
if (signature_name) {
|
||||
drawTextAt(ctx.page, signature_name, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 11.5,
|
||||
font: fonts.bold,
|
||||
color: PdfColors.black,
|
||||
});
|
||||
ctx.y += 15;
|
||||
}
|
||||
|
||||
if (signature_title) {
|
||||
drawTextAt(ctx.page, signature_title, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: 9.5,
|
||||
font: fonts.regular,
|
||||
color: PdfColors.gray,
|
||||
});
|
||||
ctx.y += 13;
|
||||
}
|
||||
}
|
||||
|
||||
drawBrandFooterOnAllPages({
|
||||
pdfDoc,
|
||||
fonts,
|
||||
options: { ...company_block },
|
||||
});
|
||||
|
||||
return await pdfDoc.save();
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
import { PDFDocument, type PDFPage } from "pdf-lib";
|
||||
import {
|
||||
PdfColors,
|
||||
PdfMargin,
|
||||
PdfPageHeight,
|
||||
PdfPageWidth,
|
||||
drawBrandHeader,
|
||||
drawStyledLine,
|
||||
drawTextAt,
|
||||
wrapStyledSegments,
|
||||
type PdfColor,
|
||||
type PdfFonts,
|
||||
type StyledLine,
|
||||
} from "./pdf-helpers";
|
||||
import type { TextBlock } from "./html-blocks";
|
||||
|
||||
const FOOTER_SPACE = 140;
|
||||
const BODY_AVAILABLE_WIDTH = PdfPageWidth - PdfMargin * 2;
|
||||
|
||||
export type RenderContext = {
|
||||
pdfDoc: PDFDocument;
|
||||
page: PDFPage;
|
||||
y: number;
|
||||
fonts: PdfFonts;
|
||||
};
|
||||
|
||||
export type BlockRenderStyle = {
|
||||
/** Body font size in pt. */
|
||||
size: number;
|
||||
/** Vertical gap between lines within a block. */
|
||||
line_gap: number;
|
||||
/** Body text color. */
|
||||
color: PdfColor;
|
||||
blockquote_size: number;
|
||||
blockquote_indent: number;
|
||||
/** Extra space after a paragraph/blockquote block. */
|
||||
paragraph_after: number;
|
||||
/** Space before a heading block. */
|
||||
heading_level_gap: number;
|
||||
/** Extra space after a heading block. */
|
||||
heading_after: number;
|
||||
/** Extra space after a list item. */
|
||||
list_after: number;
|
||||
divider_before: number;
|
||||
divider_after: number;
|
||||
};
|
||||
|
||||
const DefaultBlockStyle: BlockRenderStyle = {
|
||||
size: 11,
|
||||
line_gap: 12.5,
|
||||
color: PdfColors.black,
|
||||
blockquote_size: 10,
|
||||
blockquote_indent: 26,
|
||||
paragraph_after: 7,
|
||||
heading_level_gap: 8,
|
||||
heading_after: 6,
|
||||
list_after: 4,
|
||||
divider_before: 8,
|
||||
divider_after: 14,
|
||||
};
|
||||
|
||||
/**
|
||||
* # Compact block style for tight content like invoice/quote notes.
|
||||
* Mirrors the ACCOUNT DETAILS section: small font, subdued color,
|
||||
* minimal margins.
|
||||
*/
|
||||
export const NotesBlockStyle: BlockRenderStyle = {
|
||||
size: 9.5,
|
||||
line_gap: 12.5,
|
||||
color: PdfColors.darkGray,
|
||||
blockquote_size: 9,
|
||||
blockquote_indent: 16,
|
||||
paragraph_after: 3,
|
||||
heading_level_gap: 5,
|
||||
heading_after: 4,
|
||||
list_after: 2,
|
||||
divider_before: 5,
|
||||
divider_after: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
* # Ensure `amount` points of vertical space remain on the current page.
|
||||
* Starts a fresh page (with letterhead) when there isn't enough room.
|
||||
*/
|
||||
export function ensureSpace(ctx: RenderContext, amount: number) {
|
||||
if (ctx.y + amount <= PdfPageHeight - FOOTER_SPACE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const page = ctx.pdfDoc.addPage([PdfPageWidth, PdfPageHeight]);
|
||||
ctx.page = page;
|
||||
ctx.y = drawBrandHeader({ page, fonts: ctx.fonts });
|
||||
}
|
||||
|
||||
/**
|
||||
* # Render HTML-derived text blocks onto a PDF document.
|
||||
* Shared by letter bodies and invoice/quote notes.
|
||||
*/
|
||||
export function renderBlocks({
|
||||
blocks,
|
||||
fonts,
|
||||
ctx,
|
||||
style = DefaultBlockStyle,
|
||||
}: {
|
||||
blocks: TextBlock[];
|
||||
fonts: PdfFonts;
|
||||
ctx: RenderContext;
|
||||
style?: BlockRenderStyle;
|
||||
}): RenderContext {
|
||||
// Standard WinAnsi fonts cannot encode the ₦ glyph; PDFs use "NGN" instead.
|
||||
for (const block of blocks) {
|
||||
if (block.type == "divider") {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const segment of block.segments) {
|
||||
segment.text = segment.text.replace(/\u20a6/g, "NGN ");
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
switch (block.type) {
|
||||
case "paragraph":
|
||||
case "blockquote": {
|
||||
if (block.segments.length == 0) {
|
||||
ctx.y += style.line_gap;
|
||||
continue;
|
||||
}
|
||||
|
||||
const is_blockquote = block.type == "blockquote";
|
||||
const indent = is_blockquote ? style.blockquote_indent : 0;
|
||||
const size = is_blockquote ? style.blockquote_size : style.size;
|
||||
|
||||
const lines = wrapStyledSegments({
|
||||
segments: block.segments,
|
||||
fonts,
|
||||
size,
|
||||
max_width: BODY_AVAILABLE_WIDTH - indent,
|
||||
});
|
||||
|
||||
for (const line of lines) {
|
||||
ensureSpace(ctx, style.line_gap);
|
||||
|
||||
drawStyledLine(ctx.page, line, {
|
||||
x: PdfMargin + indent,
|
||||
y_from_top: ctx.y,
|
||||
size,
|
||||
color: style.color,
|
||||
});
|
||||
ctx.y += style.line_gap;
|
||||
}
|
||||
|
||||
ctx.y += style.paragraph_after;
|
||||
break;
|
||||
}
|
||||
|
||||
case "heading": {
|
||||
const size = style.size + (block.level <= 2 ? 3 : 1.5);
|
||||
|
||||
const lines = wrapStyledSegments({
|
||||
segments: block.segments,
|
||||
fonts,
|
||||
size,
|
||||
max_width: BODY_AVAILABLE_WIDTH,
|
||||
});
|
||||
|
||||
ensureSpace(ctx, lines.length * (size + 8) + 24);
|
||||
ctx.y += style.heading_level_gap;
|
||||
|
||||
for (const line of lines) {
|
||||
ensureSpace(ctx, size + 7);
|
||||
|
||||
const bold_line: StyledLine = {
|
||||
words: line.words.map((word) => ({
|
||||
...word,
|
||||
font:
|
||||
word.font == fonts.italic
|
||||
? fonts.boldItalic
|
||||
: fonts.bold,
|
||||
})),
|
||||
width: line.width,
|
||||
};
|
||||
drawStyledLine(ctx.page, bold_line, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size,
|
||||
color: style.color,
|
||||
});
|
||||
ctx.y += size + 7;
|
||||
}
|
||||
|
||||
ctx.y += style.heading_after;
|
||||
break;
|
||||
}
|
||||
|
||||
case "list-item": {
|
||||
const marker = block.ordered ? `${block.index + 1}.` : "\u2022";
|
||||
const marker_text = `${marker} `;
|
||||
const marker_width = fonts.regular.widthOfTextAtSize(
|
||||
marker_text,
|
||||
style.size,
|
||||
);
|
||||
const hang_indent = 18;
|
||||
const indent = PdfMargin + hang_indent;
|
||||
|
||||
const lines = wrapStyledSegments({
|
||||
segments: block.segments,
|
||||
fonts,
|
||||
size: style.size,
|
||||
max_width:
|
||||
BODY_AVAILABLE_WIDTH - hang_indent - marker_width,
|
||||
});
|
||||
|
||||
for (const [line_index, line] of lines.entries()) {
|
||||
ensureSpace(ctx, style.line_gap);
|
||||
|
||||
if (line_index == 0) {
|
||||
drawTextAt(ctx.page, marker, {
|
||||
x: PdfMargin,
|
||||
y_from_top: ctx.y,
|
||||
size: style.size,
|
||||
font: fonts.regular,
|
||||
color: style.color,
|
||||
});
|
||||
}
|
||||
|
||||
drawStyledLine(ctx.page, line, {
|
||||
x: indent + (line_index == 0 ? marker_width : 0),
|
||||
y_from_top: ctx.y,
|
||||
size: style.size,
|
||||
color: style.color,
|
||||
});
|
||||
ctx.y += style.line_gap;
|
||||
}
|
||||
|
||||
ctx.y += style.list_after;
|
||||
break;
|
||||
}
|
||||
|
||||
case "divider": {
|
||||
ensureSpace(ctx, 30);
|
||||
ctx.y += style.divider_before;
|
||||
ctx.page.drawRectangle({
|
||||
x: PdfMargin,
|
||||
y: ctx.page.getHeight() - ctx.y - 1,
|
||||
width: BODY_AVAILABLE_WIDTH,
|
||||
height: 1,
|
||||
color: PdfColors.gray,
|
||||
});
|
||||
ctx.y += style.divider_after;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams, ClientAdminCrudParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function adminCrudHandler<
|
||||
T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS,
|
||||
>(params: ClientAdminCrudParams<T>) {
|
||||
const {
|
||||
action,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { AdminFetchParams, ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function adminFetchHandler<
|
||||
T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS,
|
||||
>({ url: passed_url, method = "GET", query, body }: AdminFetchParams<T>) {
|
||||
let url = passed_url;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
@@ -6,7 +6,7 @@ import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
export default async function clientUploadMediaHandler(params: ApiReqParams) {
|
||||
const res = await fetchApi<
|
||||
ApiReqParams,
|
||||
APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA>
|
||||
APIResponseObject<BUN_SQLITE_COBBLE_MEDIA>
|
||||
>(`/api/admin/upload-media`, {
|
||||
method: "POST",
|
||||
body: params,
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* # Client-side actions for document/invoice generation & emailing.
|
||||
* PDF endpoints return binary PDFs (not JSON), so plain fetch is used
|
||||
* here instead of `fetchApi`.
|
||||
*/
|
||||
|
||||
async function postAdminAction({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
}): Promise<{ success: boolean; msg?: string }> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
|
||||
const content_type = res.headers.get("Content-Type") || "";
|
||||
|
||||
if (content_type.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
return {
|
||||
success: Boolean(json.success),
|
||||
msg: json.msg,
|
||||
};
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message || "Request failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadGeneratedPdf({
|
||||
url,
|
||||
body,
|
||||
fallback_filename,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
fallback_filename?: string;
|
||||
}): Promise<{ success: boolean; msg?: string }> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
|
||||
const content_type = res.headers.get("Content-Type") || "";
|
||||
|
||||
if (content_type.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
return {
|
||||
success: Boolean(json.success),
|
||||
msg: json.msg,
|
||||
};
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
|
||||
if (blob.size == 0) {
|
||||
throw new Error(`Received an empty PDF`);
|
||||
}
|
||||
|
||||
const disposition = res.headers.get("Content-Disposition") || "";
|
||||
const filename_match = disposition.match(/filename="?([^";]+)"?/i);
|
||||
const filename =
|
||||
filename_match?.[1] || fallback_filename || "document.pdf";
|
||||
|
||||
const object_url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
anchor.href = object_url;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(object_url);
|
||||
}, 1000);
|
||||
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message || "Couldn't download document",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmailRecord({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
}) {
|
||||
return await postAdminAction({ url, body });
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA, BunSQLiteTables } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA, BunSQLiteTables } from "@/db/types/db";
|
||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -24,7 +24,7 @@ export default async function handleMediaServer({
|
||||
throw new Error(`No media privacy params passed`);
|
||||
}
|
||||
|
||||
let media: BUN_SQLITE_BSQLITE_MEDIA | undefined = undefined;
|
||||
let media: BUN_SQLITE_COBBLE_MEDIA | undefined = undefined;
|
||||
|
||||
if (privacy == "private") {
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
@@ -34,7 +34,7 @@ export default async function handleMediaServer({
|
||||
}
|
||||
|
||||
const media_res = await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "media",
|
||||
@@ -51,7 +51,7 @@ export default async function handleMediaServer({
|
||||
media = media_res.singleRes || undefined;
|
||||
} else {
|
||||
const media_res = await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "media",
|
||||
|
||||
@@ -3,12 +3,12 @@ import adminCrudHandler from "../functions/frontend/admin-crud-handler";
|
||||
import _ from "lodash";
|
||||
import { AppData } from "../data/app-data";
|
||||
import useStatus from "@moduletrace/twui/hooks/useStatus";
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { ApiReqParams, ClientAdminCrudParams, TableType } from "../types";
|
||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite";
|
||||
import EJSON from "../utils/ejson";
|
||||
|
||||
type Params<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS> = Pick<
|
||||
type Params<T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS> = Pick<
|
||||
ClientAdminCrudParams,
|
||||
"id" | "count"
|
||||
> & {
|
||||
@@ -22,7 +22,7 @@ type Params<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS> = Pick<
|
||||
request_query?: ApiReqParams<T>;
|
||||
};
|
||||
|
||||
export function useAdminCrudGet<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>({
|
||||
export function useAdminCrudGet<T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS>({
|
||||
table,
|
||||
sql_query,
|
||||
count: add_count_param,
|
||||
|
||||
@@ -3,11 +3,11 @@ import _ from "lodash";
|
||||
import { AppData } from "../data/app-data";
|
||||
import useStatus from "@moduletrace/twui/hooks/useStatus";
|
||||
import adminFetchHandler from "../functions/frontend/admin-fetch-handler";
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { AdminFetchParams, ApiReqParams } from "../types";
|
||||
import EJSON from "../utils/ejson";
|
||||
|
||||
type Params<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS> = Omit<
|
||||
type Params<T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS> = Omit<
|
||||
AdminFetchParams<T>,
|
||||
"method" | "body"
|
||||
> & {
|
||||
@@ -16,7 +16,7 @@ type Params<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS> = Omit<
|
||||
count?: boolean;
|
||||
};
|
||||
|
||||
export function useFetchGet<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>({
|
||||
export function useFetchGet<T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS>({
|
||||
url,
|
||||
debug,
|
||||
initial_res,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import useStatus from "@moduletrace/twui/hooks/useStatus";
|
||||
import {
|
||||
useCallback,
|
||||
@@ -15,7 +15,7 @@ import twuiSlugify from "@moduletrace/twui/utils/slugify";
|
||||
import _ from "lodash";
|
||||
import type { AppContextObject } from "../types";
|
||||
|
||||
type Params<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS> = {
|
||||
type Params<T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS> = {
|
||||
/**
|
||||
* Existing record for update. All unneccasary fields
|
||||
* like join fields should be excluded.
|
||||
@@ -48,7 +48,7 @@ type Params<T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS> = {
|
||||
};
|
||||
|
||||
export default function useFormInit<
|
||||
T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS,
|
||||
>(params?: Params<T>) {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { User } from "../types";
|
||||
import { useEffect, useState } from "react";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export function useLocalUser() {
|
||||
const [user, setUser] = useState<User>();
|
||||
const [userTypes, setUserTypes] =
|
||||
useState<BUN_SQLITE_BSQLITE_USER_TYPES[]>();
|
||||
useState<BUN_SQLITE_COBBLE_USER_TYPES[]>();
|
||||
|
||||
useEffect(() => {
|
||||
fetchApi<any, APIResponseObject>(`/api/admin/user-auth`).then((res) => {
|
||||
@@ -15,7 +15,7 @@ export function useLocalUser() {
|
||||
setUser(res.singleRes as User);
|
||||
}
|
||||
if (res.payload) {
|
||||
setUserTypes(res.payload as BUN_SQLITE_BSQLITE_USER_TYPES[]);
|
||||
setUserTypes(res.payload as BUN_SQLITE_COBBLE_USER_TYPES[]);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from "lodash";
|
||||
import useStatus from "@moduletrace/twui/hooks/useStatus";
|
||||
import adminFetchHandler from "../functions/frontend/admin-fetch-handler";
|
||||
import type { ApiReqParams } from "../types";
|
||||
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "../types/sql-joins";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "../types/sql-joins";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import EJSON from "../utils/ejson";
|
||||
|
||||
@@ -10,7 +10,7 @@ type Params = {
|
||||
query?: ApiReqParams;
|
||||
debug?: boolean;
|
||||
page?: number;
|
||||
initial_users?: BUN_SQLITE_BSQLITE_USERS_JOIN[];
|
||||
initial_users?: BUN_SQLITE_COBBLE_USERS_JOIN[];
|
||||
};
|
||||
|
||||
export function useUsersGet({
|
||||
@@ -20,7 +20,7 @@ export function useUsersGet({
|
||||
initial_users,
|
||||
}: Params) {
|
||||
const [users, setUsers] = useState<
|
||||
BUN_SQLITE_BSQLITE_USERS_JOIN[] | undefined
|
||||
BUN_SQLITE_COBBLE_USERS_JOIN[] | undefined
|
||||
>(initial_users);
|
||||
const [count, setCount] = useState<number | undefined>();
|
||||
const [page, setPage] = useState<number>(initial_page || 1);
|
||||
@@ -49,7 +49,7 @@ export function useUsersGet({
|
||||
console.log("final_query", final_query);
|
||||
}
|
||||
|
||||
adminFetchHandler<BUN_SQLITE_BSQLITE_USERS_JOIN>({
|
||||
adminFetchHandler<BUN_SQLITE_COBBLE_USERS_JOIN>({
|
||||
url: `/api/admin/grab-users`,
|
||||
query: final_query,
|
||||
})
|
||||
|
||||