@bygghammar/cms-platform 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/README.md +18 -0
- package/docs/NEW-SITE.md +103 -0
- package/package.json +12 -3
- package/scripts/sync-migrations.mjs +49 -0
- package/src/backoffice/SiteSettingsView.tsx +258 -0
- package/src/config/site-config.ts +236 -0
- package/src/platform/contract.ts +0 -5
- package/src/server/site-settings-core.ts +66 -0
- package/migrations/20260811112455_217c4028-f587-4c98-9e4f-9e6c452ab761.sql +0 -20
- package/migrations/20260811114438_6687d67d-bad3-4803-9638-8b7670d2e5d4.sql +0 -86
- package/migrations/20260811220548_80d99ec2-bd4b-475d-a627-9408bb60ce56.sql +0 -1
- package/migrations/20260811222519_6b040f56-4de3-49e0-92e4-d9deec5ead6a.sql +0 -3
- package/migrations/20260812000000_seed_voyara_content.sql +0 -390
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@bygghammar/cms-platform`.
|
|
4
|
+
|
|
5
|
+
The package follows semver with the pre-1.0 convention that a **minor** bump may
|
|
6
|
+
contain breaking changes to shared views, adapters or cores; patch releases are
|
|
7
|
+
always compatible. In addition, `PLATFORM_CONTRACT_VERSION` in
|
|
8
|
+
`platform/contract` is bumped whenever a shared screen starts requiring schema
|
|
9
|
+
or adapter capabilities an older site does not have. Sites assert that constant
|
|
10
|
+
in a test and upgrade deliberately.
|
|
11
|
+
|
|
12
|
+
## 0.23.0
|
|
13
|
+
|
|
14
|
+
- Added `config/site-config`: the typed `SiteConfig` contract, `defineSiteConfig`
|
|
15
|
+
validation, feature flags, the overridable-key whitelist and
|
|
16
|
+
`resolveSiteConfig` (code defaults + database overrides).
|
|
17
|
+
- Added `server/site-settings-core`: load/save/clear for the `site_settings`
|
|
18
|
+
key/value store, running as the caller so RLS applies.
|
|
19
|
+
- Added `backoffice/SiteSettingsView`: shared site settings screen for name,
|
|
20
|
+
company details, module flags and brand tokens, with per-section reset back to
|
|
21
|
+
the code default.
|
|
22
|
+
|
|
23
|
+
## 0.22.0
|
|
24
|
+
|
|
25
|
+
- Added `docs/NEW-SITE.md`: the supported starter path for a new site (install,
|
|
26
|
+
contract assertion, migrations, theme, host registration, server cores,
|
|
27
|
+
backoffice routes, verification).
|
|
28
|
+
- Added this changelog and a written compatibility policy.
|
|
29
|
+
- Added the `cms-platform-migrations` bin, which copies missing canonical
|
|
30
|
+
migrations into a site's migration directory without overwriting or deleting
|
|
31
|
+
anything.
|
|
32
|
+
- Removed five Voyara-specific content/data migrations from the canonical set
|
|
33
|
+
(SEO copy, a homepage block seed, a locale-domain update, a booking-link
|
|
34
|
+
rewrite and the content seed). Canonical migrations are schema only; site
|
|
35
|
+
content stays in the site's own migration directory. Existing sites keep
|
|
36
|
+
those files locally and remain compatible.
|
|
37
|
+
- No runtime source changes; safe upgrade from 0.21.x.
|
|
38
|
+
|
|
39
|
+
## 0.21.0
|
|
40
|
+
|
|
41
|
+
- Added `server/forms-core`: host-agnostic forms server rules — effective
|
|
42
|
+
settings, recipient routing, notification templates, submitter detection,
|
|
43
|
+
field-removal safety, version snapshots, audit trail and the list/save/delete/
|
|
44
|
+
restore/purge/duplicate cores plus site defaults, usage and delivery logging.
|
|
45
|
+
|
|
46
|
+
## 0.20.0
|
|
47
|
+
|
|
48
|
+
- Added `server/media-usage-core` (usage scan, remote media import, reference
|
|
49
|
+
rewriting) and completed `server/media-cleanup-core`.
|
|
50
|
+
|
|
51
|
+
## 0.19.0
|
|
52
|
+
|
|
53
|
+
- Added `server/guards`, `server/redirects-core` and `server/page-seo-core`.
|
|
54
|
+
|
|
55
|
+
## 0.18.0
|
|
56
|
+
|
|
57
|
+
- Extracted `FormEditorView`, `FormsIndexView` and `RedirectsView`.
|
|
58
|
+
|
|
59
|
+
## 0.17.0
|
|
60
|
+
|
|
61
|
+
- Extracted the media library UI, including client-side optimisation and usage
|
|
62
|
+
inspection.
|
|
63
|
+
|
|
64
|
+
## 0.15.0 – 0.16.0
|
|
65
|
+
|
|
66
|
+
- Extracted `SectionFormView` and `PageEditorView`.
|
|
67
|
+
|
|
68
|
+
## 0.14.0
|
|
69
|
+
|
|
70
|
+
- Expanded the `PlatformData` host contract to every backoffice module and added
|
|
71
|
+
`assertPlatformData` / `REQUIRED_PLATFORM_DATA`.
|
|
72
|
+
|
|
73
|
+
## 0.12.0 – 0.13.0
|
|
74
|
+
|
|
75
|
+
- Extracted `ReviewImageUpload` and `PageSeoPanel`.
|
|
76
|
+
|
|
77
|
+
## 0.5.0 – 0.11.0
|
|
78
|
+
|
|
79
|
+
- Phase 1 and 2: pure forms/media/http/i18n helpers, backup core, trips and
|
|
80
|
+
navigation contracts, and the first host contract.
|
|
81
|
+
- Phase 5 groundwork: canonical migrations shipped under `migrations/` with
|
|
82
|
+
`SCHEMA_MIGRATIONS` and `PLATFORM_CONTRACT_VERSION`.
|
|
83
|
+
|
|
84
|
+
## 0.1.0
|
|
85
|
+
|
|
86
|
+
- First release: blocks, block types, icons, i18n core, UI primitives and the
|
|
87
|
+
`theme.css` design-token contract.
|
package/README.md
CHANGED
|
@@ -18,6 +18,24 @@ future sites). **Code only** — no content, no data, no secrets, no theme.
|
|
|
18
18
|
active-state helpers. Menu content and visual headers remain per-site.
|
|
19
19
|
- `src/theme.css` — the design-token contract + neutral starter theme
|
|
20
20
|
|
|
21
|
+
## Starting a new site
|
|
22
|
+
|
|
23
|
+
`docs/NEW-SITE.md` is the supported starter path: install, assert the contract,
|
|
24
|
+
apply the canonical schema with `bunx cms-platform-migrations`, import
|
|
25
|
+
`theme.css`, register host components / platform data / translations, wrap the
|
|
26
|
+
server cores and mount the shared backoffice views.
|
|
27
|
+
|
|
28
|
+
## Compatibility policy
|
|
29
|
+
|
|
30
|
+
- `CHANGELOG.md` records every release; read it before upgrading.
|
|
31
|
+
- Pre-1.0, a **minor** release may break shared views, adapters or cores; patch
|
|
32
|
+
releases are always compatible.
|
|
33
|
+
- `PLATFORM_CONTRACT_VERSION` (`platform/contract`) is bumped when a shared
|
|
34
|
+
screen starts requiring schema or adapter capabilities an older site lacks.
|
|
35
|
+
Sites pin it in a test.
|
|
36
|
+
- `migrations/` is schema only. Site content, SEO copy and seeds never belong
|
|
37
|
+
there.
|
|
38
|
+
|
|
21
39
|
## What must never live here
|
|
22
40
|
|
|
23
41
|
- colours, fonts or spacing values used by a specific site (see `theme.css`)
|
package/docs/NEW-SITE.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Starting a new site on @bygghammar/cms-platform
|
|
2
|
+
|
|
3
|
+
This is the supported path from an empty TanStack Start project to a working
|
|
4
|
+
CMS site. Every step is code or configuration in the new site; nothing here
|
|
5
|
+
copies content, media or secrets between sites.
|
|
6
|
+
|
|
7
|
+
## 0. Prerequisites
|
|
8
|
+
|
|
9
|
+
- TanStack Start v1 (React 19, Vite 7), Tailwind v4, shadcn/ui.
|
|
10
|
+
- A Supabase-compatible backend for the site (its own project/stack — never
|
|
11
|
+
shared with another site, never a production database during authoring).
|
|
12
|
+
|
|
13
|
+
## 1. Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @bygghammar/cms-platform@<exact version>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Pin an exact version and commit the lockfile. Read `CHANGELOG.md` before every
|
|
20
|
+
upgrade.
|
|
21
|
+
|
|
22
|
+
## 2. Assert the contract
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import {
|
|
26
|
+
PLATFORM_CONTRACT_VERSION,
|
|
27
|
+
SCHEMA_MIGRATIONS,
|
|
28
|
+
assertPlatformContract,
|
|
29
|
+
missingMigrations,
|
|
30
|
+
} from "@bygghammar/cms-platform/platform/contract";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Add one test that pins the contract version the site was built against and one
|
|
34
|
+
that asserts `missingMigrations(appliedFilenames)` is empty. See
|
|
35
|
+
`tests/platform-contract.test.ts` in any existing site for the exact shape.
|
|
36
|
+
|
|
37
|
+
## 3. Apply the canonical schema
|
|
38
|
+
|
|
39
|
+
The package ships the canonical migrations under `migrations/`, oldest first.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bunx cms-platform-migrations ./supabase/migrations
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The command copies any migration the target directory is missing and never
|
|
46
|
+
overwrites or deletes a file, so it is safe to re-run after an upgrade. Apply
|
|
47
|
+
the copied files with the site's normal migration tooling. Site-specific
|
|
48
|
+
migrations live alongside them with later timestamps and must stay additive.
|
|
49
|
+
|
|
50
|
+
## 4. Theme
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
/* src/styles.css */
|
|
54
|
+
@import "@bygghammar/cms-platform/theme.css";
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then override the design tokens for the new brand in the site's own layer.
|
|
58
|
+
Never edit colours, fonts or spacing inside the package.
|
|
59
|
+
|
|
60
|
+
## 5. Register the host
|
|
61
|
+
|
|
62
|
+
Three registrations, all at startup:
|
|
63
|
+
|
|
64
|
+
- **Host components** — `registerHostComponents()` from
|
|
65
|
+
`@bygghammar/cms-platform/host` for the site's link, image, video and rich
|
|
66
|
+
text primitives.
|
|
67
|
+
- **Platform data adapter** — `registerHostPlatform({ data, site })` from
|
|
68
|
+
`@bygghammar/cms-platform/platform`. `data` implements every member in
|
|
69
|
+
`REQUIRED_PLATFORM_DATA` (pages, versions, global sections, media, SEO,
|
|
70
|
+
redirects, forms); `site` describes name, `backofficeBasePath` and locales.
|
|
71
|
+
Guard it with a test that calls `assertPlatformData`.
|
|
72
|
+
- **Translations** — the site's own `sv`/`en` maps via the i18n register
|
|
73
|
+
module. Translation data is content and stays in the site.
|
|
74
|
+
|
|
75
|
+
## 6. Server functions
|
|
76
|
+
|
|
77
|
+
Wrap the shared server cores in the site's own `createServerFn` files:
|
|
78
|
+
|
|
79
|
+
| Package core | Purpose |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `server/guards` | `assertStaff`, `assertSuperadmin` |
|
|
82
|
+
| `server/forms-core` | form definitions, versions, settings, delivery log |
|
|
83
|
+
| `server/redirects-core` | path normalisation and redirect writes |
|
|
84
|
+
| `server/page-seo-core` | slug conflicts and slug-change 301s |
|
|
85
|
+
| `server/media-usage-core` | usage scan and remote import |
|
|
86
|
+
| `server/media-cleanup-core` | duplicate/rename planning and apply |
|
|
87
|
+
|
|
88
|
+
The site supplies clients, storage and page-key → path maps; the package owns
|
|
89
|
+
the rules. Keep privileged clients inside handlers.
|
|
90
|
+
|
|
91
|
+
## 7. Backoffice routes
|
|
92
|
+
|
|
93
|
+
Each backoffice route is a thin adapter around the package view
|
|
94
|
+
(`PageEditorView`, `MediaLibraryView`, `FormEditorView`, `FormsIndexView`,
|
|
95
|
+
`RedirectsView`, `SectionFormView`, `PageSeoPanel`). Routes own the path and
|
|
96
|
+
auth gate only.
|
|
97
|
+
|
|
98
|
+
## 8. Verify
|
|
99
|
+
|
|
100
|
+
1. Types pass.
|
|
101
|
+
2. Full test suite passes, including the contract tests from step 2.
|
|
102
|
+
3. `/` renders.
|
|
103
|
+
4. The backoffice signs in, and one page, one form and one media asset save.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bygghammar/cms-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"docs",
|
|
13
15
|
"migrations",
|
|
16
|
+
"scripts",
|
|
14
17
|
"src"
|
|
15
18
|
],
|
|
16
19
|
"repository": {
|
|
@@ -109,7 +112,10 @@
|
|
|
109
112
|
"./server/page-seo-core": "./src/server/page-seo-core.ts",
|
|
110
113
|
"./server/media-cleanup-core": "./src/server/media-cleanup-core.ts",
|
|
111
114
|
"./server/media-usage-core": "./src/server/media-usage-core.ts",
|
|
112
|
-
"./server/forms-core": "./src/server/forms-core.ts"
|
|
115
|
+
"./server/forms-core": "./src/server/forms-core.ts",
|
|
116
|
+
"./config/site-config": "./src/config/site-config.ts",
|
|
117
|
+
"./server/site-settings-core": "./src/server/site-settings-core.ts",
|
|
118
|
+
"./backoffice/SiteSettingsView": "./src/backoffice/SiteSettingsView.tsx"
|
|
113
119
|
},
|
|
114
120
|
"dependencies": {
|
|
115
121
|
"fflate": "^0.8.3"
|
|
@@ -181,5 +187,8 @@
|
|
|
181
187
|
"@tiptap/extension-table": {
|
|
182
188
|
"optional": true
|
|
183
189
|
}
|
|
190
|
+
},
|
|
191
|
+
"bin": {
|
|
192
|
+
"cms-platform-migrations": "scripts/sync-migrations.mjs"
|
|
184
193
|
}
|
|
185
|
-
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copy canonical platform migrations into a site's migration directory.
|
|
4
|
+
*
|
|
5
|
+
* Additive only: a file that already exists in the target is left untouched and
|
|
6
|
+
* nothing is ever deleted or overwritten, so this is safe to re-run after an
|
|
7
|
+
* upgrade.
|
|
8
|
+
*
|
|
9
|
+
* Usage: cms-platform-migrations [targetDir] [--check]
|
|
10
|
+
* targetDir defaults to ./supabase/migrations
|
|
11
|
+
* --check exits non-zero when files are missing instead of copying them
|
|
12
|
+
*/
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const source = resolve(here, "..", "migrations");
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const checkOnly = args.includes("--check");
|
|
22
|
+
const target = resolve(args.find((a) => !a.startsWith("--")) ?? "supabase/migrations");
|
|
23
|
+
|
|
24
|
+
if (!existsSync(source)) {
|
|
25
|
+
console.error(`No migrations directory in the package at ${source}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const canonical = readdirSync(source)
|
|
30
|
+
.filter((f) => f.endsWith(".sql"))
|
|
31
|
+
.sort();
|
|
32
|
+
|
|
33
|
+
const present = new Set(existsSync(target) ? readdirSync(target) : []);
|
|
34
|
+
const missing = canonical.filter((f) => !present.has(f));
|
|
35
|
+
|
|
36
|
+
if (missing.length === 0) {
|
|
37
|
+
console.log(`Up to date: ${canonical.length} canonical migrations already present.`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (checkOnly) {
|
|
42
|
+
console.error(`Missing ${missing.length} canonical migrations:`);
|
|
43
|
+
for (const f of missing) console.error(` ${f}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
mkdirSync(target, { recursive: true });
|
|
48
|
+
for (const f of missing) copyFileSync(join(source, f), join(target, f));
|
|
49
|
+
console.log(`Copied ${missing.length} migration(s) into ${target}. Apply them with your migration tooling.`);
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { Save, RotateCcw } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_FEATURE_FLAGS,
|
|
7
|
+
EMPTY_COMPANY,
|
|
8
|
+
type SiteConfig,
|
|
9
|
+
type SiteFeatureFlags,
|
|
10
|
+
type SiteSettingOverrides,
|
|
11
|
+
} from "../config/site-config";
|
|
12
|
+
|
|
13
|
+
export type SiteSettingsDeps = {
|
|
14
|
+
/** Code config with any stored overrides already applied. */
|
|
15
|
+
config: SiteConfig;
|
|
16
|
+
/** Raw stored overrides, used to show what differs from code. */
|
|
17
|
+
overrides: SiteSettingOverrides;
|
|
18
|
+
saveSettings: (input: { data: SiteSettingOverrides }) => Promise<unknown>;
|
|
19
|
+
clearSetting: (input: { data: { key: string } }) => Promise<unknown>;
|
|
20
|
+
refresh: () => Promise<unknown> | void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const FEATURE_LABELS: Record<keyof SiteFeatureFlags, string> = {
|
|
24
|
+
trips: "Trips & departures",
|
|
25
|
+
courses: "Courses",
|
|
26
|
+
reviews: "Reviews",
|
|
27
|
+
email: "Transactional email",
|
|
28
|
+
tracking: "Code & tracking",
|
|
29
|
+
backup: "Backup & restore",
|
|
30
|
+
domains: "Custom domains",
|
|
31
|
+
forms: "Forms",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const COMPANY_LABELS: Record<keyof typeof EMPTY_COMPANY, string> = {
|
|
35
|
+
legalName: "Legal name",
|
|
36
|
+
locationName: "Location name",
|
|
37
|
+
street: "Street",
|
|
38
|
+
postCode: "Post code",
|
|
39
|
+
city: "City",
|
|
40
|
+
country: "Country",
|
|
41
|
+
orgNumber: "Org. number",
|
|
42
|
+
email: "Email",
|
|
43
|
+
phone: "Phone",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const inputClass =
|
|
47
|
+
"w-full border border-ink/15 bg-white px-3 py-2 text-sm text-ink focus:border-sea focus:outline-none";
|
|
48
|
+
const labelClass = "block text-xs uppercase tracking-wider text-ink/60 mb-1";
|
|
49
|
+
const cardClass = "border border-ink/10 bg-white p-6";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Host-agnostic site settings screen. Code config is always the baseline; this
|
|
53
|
+
* screen writes only the overridable subset into the database.
|
|
54
|
+
*/
|
|
55
|
+
export function SiteSettingsView({ deps }: { deps: SiteSettingsDeps }) {
|
|
56
|
+
const { config, overrides, saveSettings, clearSetting, refresh } = deps;
|
|
57
|
+
const [name, setName] = useState(config.name);
|
|
58
|
+
const [company, setCompany] = useState({ ...EMPTY_COMPANY, ...config.company });
|
|
59
|
+
const [features, setFeatures] = useState<SiteFeatureFlags>({ ...config.features });
|
|
60
|
+
const [brand, setBrand] = useState<Record<string, string>>({ ...config.brand });
|
|
61
|
+
const [busy, setBusy] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
setName(config.name);
|
|
65
|
+
setCompany({ ...EMPTY_COMPANY, ...config.company });
|
|
66
|
+
setFeatures({ ...config.features });
|
|
67
|
+
setBrand({ ...config.brand });
|
|
68
|
+
}, [config]);
|
|
69
|
+
|
|
70
|
+
const save = async (key: string, patch: SiteSettingOverrides) => {
|
|
71
|
+
setBusy(key);
|
|
72
|
+
try {
|
|
73
|
+
await saveSettings({ data: patch });
|
|
74
|
+
toast.success("Saved");
|
|
75
|
+
await refresh();
|
|
76
|
+
} catch (e) {
|
|
77
|
+
toast.error(e instanceof Error ? e.message : "Failed to save");
|
|
78
|
+
} finally {
|
|
79
|
+
setBusy(null);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const reset = async (key: string) => {
|
|
84
|
+
setBusy(key);
|
|
85
|
+
try {
|
|
86
|
+
await clearSetting({ data: { key } });
|
|
87
|
+
toast.success("Reset to the value defined in code");
|
|
88
|
+
await refresh();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
toast.error(e instanceof Error ? e.message : "Failed to reset");
|
|
91
|
+
} finally {
|
|
92
|
+
setBusy(null);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const overridden = (key: keyof SiteSettingOverrides) => overrides[key] !== undefined;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="p-8 md:p-12 w-full">
|
|
100
|
+
<h1 className="font-serif text-3xl md:text-4xl text-ink">Site settings</h1>
|
|
101
|
+
<p className="mt-3 max-w-2xl text-sm text-ink/60">
|
|
102
|
+
These values come from the site configuration in code. Anything you change here is stored as
|
|
103
|
+
an override in the database and can be reset back to the code default.
|
|
104
|
+
</p>
|
|
105
|
+
|
|
106
|
+
<dl className="mt-8 grid gap-4 md:grid-cols-4">
|
|
107
|
+
{[
|
|
108
|
+
["Site key", config.key],
|
|
109
|
+
["Public origin", config.origin],
|
|
110
|
+
["Locales", config.locales.join(", ")],
|
|
111
|
+
["Backoffice", `${config.backofficePath} (${config.backofficeLocale})`],
|
|
112
|
+
].map(([label, value]) => (
|
|
113
|
+
<div key={label} className="border border-ink/10 bg-white p-4">
|
|
114
|
+
<dt className="text-xs uppercase tracking-wider text-ink/50">{label}</dt>
|
|
115
|
+
<dd className="mt-1 text-sm text-ink">{value}</dd>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</dl>
|
|
119
|
+
|
|
120
|
+
<section className={`mt-8 ${cardClass}`}>
|
|
121
|
+
<SectionHeader
|
|
122
|
+
title="Site name"
|
|
123
|
+
overridden={overridden("name")}
|
|
124
|
+
onReset={() => reset("name")}
|
|
125
|
+
busy={busy === "name"}
|
|
126
|
+
/>
|
|
127
|
+
<div className="mt-4 grid gap-3 md:grid-cols-[1fr_auto] md:items-end">
|
|
128
|
+
<label className="block">
|
|
129
|
+
<span className={labelClass}>Name</span>
|
|
130
|
+
<input className={inputClass} value={name} onChange={(e) => setName(e.target.value)} />
|
|
131
|
+
</label>
|
|
132
|
+
<SaveButton busy={busy === "name"} onClick={() => save("name", { name })} />
|
|
133
|
+
</div>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<section className={`mt-6 ${cardClass}`}>
|
|
137
|
+
<SectionHeader
|
|
138
|
+
title="Company & contact details"
|
|
139
|
+
overridden={overridden("company")}
|
|
140
|
+
onReset={() => reset("company")}
|
|
141
|
+
busy={busy === "company"}
|
|
142
|
+
/>
|
|
143
|
+
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
|
144
|
+
{(Object.keys(COMPANY_LABELS) as (keyof typeof EMPTY_COMPANY)[]).map((key) => (
|
|
145
|
+
<label key={key} className="block">
|
|
146
|
+
<span className={labelClass}>{COMPANY_LABELS[key]}</span>
|
|
147
|
+
<input
|
|
148
|
+
className={inputClass}
|
|
149
|
+
value={company[key] ?? ""}
|
|
150
|
+
onChange={(e) => setCompany({ ...company, [key]: e.target.value })}
|
|
151
|
+
/>
|
|
152
|
+
</label>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
<div className="mt-4">
|
|
156
|
+
<SaveButton busy={busy === "company"} onClick={() => save("company", { company })} />
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<section className={`mt-6 ${cardClass}`}>
|
|
161
|
+
<SectionHeader
|
|
162
|
+
title="Modules"
|
|
163
|
+
overridden={overridden("features")}
|
|
164
|
+
onReset={() => reset("features")}
|
|
165
|
+
busy={busy === "features"}
|
|
166
|
+
/>
|
|
167
|
+
<div className="mt-4 grid gap-3 md:grid-cols-2">
|
|
168
|
+
{(Object.keys(DEFAULT_FEATURE_FLAGS) as (keyof SiteFeatureFlags)[]).map((flag) => (
|
|
169
|
+
<label key={flag} className="flex items-center gap-3 text-sm text-ink">
|
|
170
|
+
<input
|
|
171
|
+
type="checkbox"
|
|
172
|
+
checked={features[flag]}
|
|
173
|
+
onChange={(e) => setFeatures({ ...features, [flag]: e.target.checked })}
|
|
174
|
+
/>
|
|
175
|
+
{FEATURE_LABELS[flag]}
|
|
176
|
+
</label>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
<div className="mt-4">
|
|
180
|
+
<SaveButton busy={busy === "features"} onClick={() => save("features", { features })} />
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
{Object.keys(brand).length > 0 && (
|
|
185
|
+
<section className={`mt-6 ${cardClass}`}>
|
|
186
|
+
<SectionHeader
|
|
187
|
+
title="Brand tokens"
|
|
188
|
+
overridden={overridden("brand")}
|
|
189
|
+
onReset={() => reset("brand")}
|
|
190
|
+
busy={busy === "brand"}
|
|
191
|
+
/>
|
|
192
|
+
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
|
193
|
+
{Object.keys(brand).map((token) => (
|
|
194
|
+
<label key={token} className="block">
|
|
195
|
+
<span className={labelClass}>{token}</span>
|
|
196
|
+
<div className="flex items-center gap-2">
|
|
197
|
+
<span
|
|
198
|
+
className="size-8 shrink-0 border border-ink/15"
|
|
199
|
+
style={{ background: brand[token] }}
|
|
200
|
+
/>
|
|
201
|
+
<input
|
|
202
|
+
className={inputClass}
|
|
203
|
+
value={brand[token] ?? ""}
|
|
204
|
+
onChange={(e) => setBrand({ ...brand, [token]: e.target.value })}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
</label>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
<div className="mt-4">
|
|
211
|
+
<SaveButton busy={busy === "brand"} onClick={() => save("brand", { brand })} />
|
|
212
|
+
</div>
|
|
213
|
+
</section>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function SectionHeader({
|
|
220
|
+
title,
|
|
221
|
+
overridden,
|
|
222
|
+
onReset,
|
|
223
|
+
busy,
|
|
224
|
+
}: {
|
|
225
|
+
title: string;
|
|
226
|
+
overridden: boolean;
|
|
227
|
+
onReset: () => void;
|
|
228
|
+
busy: boolean;
|
|
229
|
+
}) {
|
|
230
|
+
return (
|
|
231
|
+
<div className="flex items-center justify-between gap-4">
|
|
232
|
+
<h2 className="font-serif text-xl text-ink">{title}</h2>
|
|
233
|
+
{overridden && (
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={onReset}
|
|
237
|
+
disabled={busy}
|
|
238
|
+
className="inline-flex items-center gap-2 border border-ink/15 px-3 py-1.5 text-xs uppercase tracking-wider text-ink/70 hover:border-sea disabled:opacity-50"
|
|
239
|
+
>
|
|
240
|
+
<RotateCcw className="size-3.5" /> Reset to code default
|
|
241
|
+
</button>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function SaveButton({ busy, onClick }: { busy: boolean; onClick: () => void }) {
|
|
248
|
+
return (
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
onClick={onClick}
|
|
252
|
+
disabled={busy}
|
|
253
|
+
className="inline-flex items-center gap-2 bg-ink px-4 py-2 text-xs uppercase tracking-wider text-white hover:bg-sea disabled:opacity-50"
|
|
254
|
+
>
|
|
255
|
+
<Save className="size-4" /> {busy ? "Saving…" : "Save"}
|
|
256
|
+
</button>
|
|
257
|
+
);
|
|
258
|
+
}
|