@wenathlan/cobaia 0.0.0-stage → 2.0.155
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/App.tsx +52 -0
- package/Dockerfile +45 -0
- package/Sol/Sol.tsx +91 -0
- package/Sol/apps/apps.tsx +66 -0
- package/Sol/blog/blog.tsx +86 -0
- package/Sol/home/home.tsx +140 -0
- package/Sol/intro/intro.tsx +187 -0
- package/Sol/legal/legal.tsx +76 -0
- package/Sol/notfound/notfound.tsx +32 -0
- package/Sol/pricing/pricing.tsx +98 -0
- package/Sol/shell/Shell.tsx +161 -0
- package/Sol/sol.css +1944 -0
- package/Sol/support/support.tsx +102 -0
- package/Sol/wingets/IntroFooter.tsx +69 -0
- package/Sol/wingets/IntroPlans.tsx +93 -0
- package/Sol/wingets/IntroSplash.tsx +105 -0
- package/Sol/wingets/ShellAdsslot.tsx +47 -0
- package/Sol/wingets/ShellBrand.tsx +86 -0
- package/Sol/wingets/index.ts +18 -0
- package/_headers +4 -0
- package/_redirects +1 -0
- package/adsconfig.ts +59 -0
- package/biome.jsonc +134 -0
- package/bun.lock +2331 -0
- package/capacitor.config.ts +51 -0
- package/checkoutlinks.ts +80 -0
- package/content.ts +425 -0
- package/db.ts +219 -0
- package/devthink.toml +5 -0
- package/docs/.gitkeep +0 -0
- package/familyurl.ts +22 -0
- package/favicon.svg +1 -0
- package/icon.svg +1 -0
- package/index.html +26 -0
- package/labels.ts +291 -0
- package/manifest.webmanifest +56 -0
- package/mime.types +12 -0
- package/netlify.toml +2 -0
- package/package.json +200 -4
- package/pnpm-workspace.yaml +82 -0
- package/posts.ts +54 -0
- package/robots.txt +90 -0
- package/roster.ts +29 -0
- package/sandboxmodality.ts +138 -0
- package/schema.prisma +31 -0
- package/sitemap.xml +42 -0
- package/tests/adsconfig.test.ts +51 -0
- package/tests/checkoutlinks.test.ts +73 -0
- package/tests/familyurl.test.ts +40 -0
- package/tests/posts.test.ts +76 -0
- package/tests/sandboxmodality.test.ts +77 -0
- package/tsconfig.json +22 -0
- package/tsconfig.node.json +22 -0
- package/vercel.json +3 -0
- package/vite.config.ts +44 -0
- package/vitest.config.ts +42 -0
- package/wrangler.toml +7 -0
- package/README.md +0 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* the one conversion config of the app, living at the app root so the
|
|
3
|
+
* capacitor cli resolves it from its working directory: the mobile lane runs
|
|
4
|
+
* `npx cap add`/`npx cap sync` from the app root and every path below — webDir
|
|
5
|
+
* above all — resolves relative to that cwd.
|
|
6
|
+
*
|
|
7
|
+
* the theme build (hashed bundles emitted by vite straight into the app root,
|
|
8
|
+
* publish = ".") is the single application
|
|
9
|
+
* source of every platform: the browser, github pages, vercel, netlify, the tv
|
|
10
|
+
* and the android wrapper all render the same web bundle. the native shells
|
|
11
|
+
* stay runner-side toolchain output (generated on the mobile workflow into
|
|
12
|
+
* android/ and ios/, never committed).
|
|
13
|
+
*
|
|
14
|
+
* the shape follows the capacitor-cli config contract; the type import stays
|
|
15
|
+
* out so the web typecheck never requires the optional capacitor toolchain.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** the structural contract of the capacitor config the cli reads (the subset this repository declares). */
|
|
19
|
+
type CapacitorConfigShape = {
|
|
20
|
+
appId: string;
|
|
21
|
+
appName: string;
|
|
22
|
+
webDir: string;
|
|
23
|
+
bundledWebRuntime?: boolean;
|
|
24
|
+
server?: { androidScheme?: string };
|
|
25
|
+
android?: { allowMixedContent?: boolean; path?: string };
|
|
26
|
+
ios?: { path?: string };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const config: CapacitorConfigShape = {
|
|
30
|
+
/** the cobaia mobile identity — the id the mobile lane stamps onto the generated shells. */
|
|
31
|
+
appId: "im.cobaia.app",
|
|
32
|
+
appName: "Cobaia",
|
|
33
|
+
/** the vite build of the theme: pnpm build emits the hashed bundles at the app root. */
|
|
34
|
+
webDir: ".",
|
|
35
|
+
bundledWebRuntime: false,
|
|
36
|
+
server: {
|
|
37
|
+
androidScheme: "https",
|
|
38
|
+
},
|
|
39
|
+
/** the build paths the mobile lane answers: the android wrapper and the ios
|
|
40
|
+
* shell materialize beside this config (family-mobile.yml) and the cli
|
|
41
|
+
* reads them from these declared folders. */
|
|
42
|
+
android: {
|
|
43
|
+
allowMixedContent: false,
|
|
44
|
+
path: "android",
|
|
45
|
+
},
|
|
46
|
+
ios: {
|
|
47
|
+
path: "ios",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default config;
|
package/checkoutlinks.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkoutlinks.ts — the typed accessor of the billing configuration of
|
|
3
|
+
* the family site: the thin door the pricing page, the support page and
|
|
4
|
+
* the intro plans zone import, answering the plan table and the support
|
|
5
|
+
* channels of the content lane (content.ts — the manifest; db.ts — the
|
|
6
|
+
* content_rows table; the platform gateway — GET /api/v1/ui/content). The
|
|
7
|
+
* data never lives here anymore: checkoutlinks() and supportlinks() answer
|
|
8
|
+
* the live rows the platform served, the manifest seed otherwise (the
|
|
9
|
+
* uistore chain). The payment links themselves are configuration, never
|
|
10
|
+
* code: the owner pins the checkout hrefs (Stripe Payment Links or Paddle
|
|
11
|
+
* overlays) into the store once billing goes live. Until then every href
|
|
12
|
+
* ships empty and the interface renders the honest empty state — "checkout
|
|
13
|
+
* not configured yet" — instead of pretending.
|
|
14
|
+
*
|
|
15
|
+
* The words never ride here: every plan sentence (name, billing period,
|
|
16
|
+
* blurb, features) answers from the label store (labels.ts — the manifest
|
|
17
|
+
* the seed carries into the site database), so the pricing cards and the
|
|
18
|
+
* intro plans zone read their prose through the typed accessor and this
|
|
19
|
+
* lane keeps only the billing values — ids, prices and links.
|
|
20
|
+
*
|
|
21
|
+
* No network call ever happens here: the links are plain hrefs the visitor
|
|
22
|
+
* chooses to follow, no key is committed (not even a placeholder that looks
|
|
23
|
+
* real) and no script is loaded. The prices are placeholders (US$0 / 5 / 19
|
|
24
|
+
* per month) until the billing platform decision lands.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
type checkoutplan,
|
|
29
|
+
contenttable,
|
|
30
|
+
contentvalue,
|
|
31
|
+
seedPlans,
|
|
32
|
+
seedSupportLinks,
|
|
33
|
+
type supportlinks as supportlinkstable,
|
|
34
|
+
} from "./content.ts";
|
|
35
|
+
|
|
36
|
+
export type { checkoutplan, supportlinkstable as supportlinks };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The typed accessor of the pricing table: the live db rows when the
|
|
40
|
+
* platform answered, the manifest seed otherwise — in display order (the
|
|
41
|
+
* prose answers from the label store; only the billing values ride this
|
|
42
|
+
* lane).
|
|
43
|
+
*
|
|
44
|
+
* @returns the plans of the /pricing page.
|
|
45
|
+
*/
|
|
46
|
+
export function checkoutlinks(): readonly checkoutplan[] {
|
|
47
|
+
return contenttable("plans", seedPlans);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The typed accessor of the support channels: the live db row when the
|
|
52
|
+
* platform answered, the manifest seed otherwise — the same rule as the
|
|
53
|
+
* plans: hrefs stay empty until the owner configures them.
|
|
54
|
+
*
|
|
55
|
+
* @returns the support channels of the /support page.
|
|
56
|
+
*/
|
|
57
|
+
export function supportlinks(): Readonly<supportlinkstable> {
|
|
58
|
+
return contentvalue("supportlinks", seedSupportLinks);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Answers whether a configured link is live. An empty (or whitespace) href
|
|
63
|
+
* means the feature is not configured yet and the interface must say so.
|
|
64
|
+
*
|
|
65
|
+
* @param href the configured link href.
|
|
66
|
+
* @returns true only when the href carries a real target.
|
|
67
|
+
*/
|
|
68
|
+
export function isconfigured(href: string): boolean {
|
|
69
|
+
return href.trim().length > 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Finds one plan of the table by its stable id.
|
|
74
|
+
*
|
|
75
|
+
* @param id the plan id (free, pro, studio).
|
|
76
|
+
* @returns the plan, or undefined when no plan carries the id.
|
|
77
|
+
*/
|
|
78
|
+
export function planbyid(id: string): checkoutplan | undefined {
|
|
79
|
+
return checkoutlinks().find((plan) => plan.id === id);
|
|
80
|
+
}
|
package/content.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* content.ts — the content law of cobaia: every content row the pages
|
|
3
|
+
* render rides the database — the content_rows table of the site store
|
|
4
|
+
* (db.ts, seeded from this manifest at boot), served by the platform
|
|
5
|
+
* gateway over GET /api/v1/ui/content — and the pages read through the
|
|
6
|
+
* typed accessors (posts.ts, roster.ts, checkoutlinks.ts, adsconfig.ts —
|
|
7
|
+
* the thin doors that keep their module names, so every importer keeps
|
|
8
|
+
* working). The literals live HERE and ONLY HERE as the build seed (the
|
|
9
|
+
* labels.ts twin): the db boot ingests the seed rows into the table, the
|
|
10
|
+
* live rows answer the pages when the platform serves them, and the seed
|
|
11
|
+
* answers the offline static build (the memory-seed law — never browser
|
|
12
|
+
* storage). The typed accessors answer the live rows the platform answered
|
|
13
|
+
* and fall back to the seed, so the interface never blanks where a row
|
|
14
|
+
* belongs and the db stays the single living config of the site content.
|
|
15
|
+
* The wave moved every row verbatim from the data modules it used to ride
|
|
16
|
+
* — zero invented rows, zero dropped rows, zero restated words.
|
|
17
|
+
*
|
|
18
|
+
* the browser never imports db.ts (better-sqlite3 cannot ride the static
|
|
19
|
+
* bundle): the types come across erased, the loader pulls the live rows
|
|
20
|
+
* over the internal api, and the seed always answers when the platform is
|
|
21
|
+
* away.
|
|
22
|
+
*
|
|
23
|
+
* @section 1 seed — the manifest rows the db receives at boot
|
|
24
|
+
* @section 2 client — the live loader, the per-kind door and the mount
|
|
25
|
+
* hook the pages render through (browser side)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useEffect, useState } from "react";
|
|
29
|
+
import type { LabelKey } from "./labels.ts";
|
|
30
|
+
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
/* @section 1 — seed: the manifest rows the db receives at boot */
|
|
33
|
+
/* ------------------------------------------------------------------ */
|
|
34
|
+
|
|
35
|
+
/** one blog post: the stable slug, the honest metadata and the full body
|
|
36
|
+
* (moved verbatim from posts.ts — the seed blog of the family site). */
|
|
37
|
+
export type blogpost = {
|
|
38
|
+
/** the stable slug of the post */
|
|
39
|
+
slug: string;
|
|
40
|
+
/** the display title (sentence case, no marketing words) */
|
|
41
|
+
title: string;
|
|
42
|
+
/** the publication date as an iso date (yyyy-mm-dd) */
|
|
43
|
+
date: string;
|
|
44
|
+
/** the topic tags of the post */
|
|
45
|
+
tags: readonly string[];
|
|
46
|
+
/** the one or two sentence summary shown in the index */
|
|
47
|
+
excerpt: string;
|
|
48
|
+
/** the full body of the post, one paragraph per string */
|
|
49
|
+
body: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** one member of the directory: the deploy segment, the accent, the role
|
|
53
|
+
* (moved verbatim from roster.ts — the spec §12 table and the honest role
|
|
54
|
+
* each app declares in its own devthink.toml). */
|
|
55
|
+
export type familymember = {
|
|
56
|
+
/** the deploy unit segment (the app folder name beside this site) */
|
|
57
|
+
slug: string;
|
|
58
|
+
/** the one accent the member carries (the spec §12 table) */
|
|
59
|
+
accent: string;
|
|
60
|
+
/** the honest one-line role of the member */
|
|
61
|
+
role: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** one plan row of the pricing page: the billing values and the label keys
|
|
65
|
+
* of the plan prose (moved verbatim from checkoutlinks.ts — the store
|
|
66
|
+
* answers the words, this row answers the numbers and the link). */
|
|
67
|
+
export type checkoutplan = {
|
|
68
|
+
/** the stable plan id used by the interface and the tests */
|
|
69
|
+
id: string;
|
|
70
|
+
/** the placeholder monthly price in us dollars */
|
|
71
|
+
priceusd: number;
|
|
72
|
+
/** the label key of the display name of the plan */
|
|
73
|
+
nameKey: LabelKey;
|
|
74
|
+
/** the label key of the billing period the price reads against */
|
|
75
|
+
perKey: LabelKey;
|
|
76
|
+
/** the label key of the one-line honest promise of the plan */
|
|
77
|
+
blurbKey: LabelKey;
|
|
78
|
+
/** the label keys of the concrete features the plan includes */
|
|
79
|
+
featureKeys: readonly LabelKey[];
|
|
80
|
+
/** the payment link href — empty until the owner configures billing */
|
|
81
|
+
href: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** the support channels of the /support page (moved verbatim from
|
|
85
|
+
* checkoutlinks.ts — the same rule as the plans: hrefs stay empty until
|
|
86
|
+
* the owner configures them). */
|
|
87
|
+
export type supportlinks = {
|
|
88
|
+
/** the GitHub Sponsors profile url */
|
|
89
|
+
sponsors: string;
|
|
90
|
+
/** the Ko-fi page url */
|
|
91
|
+
kofi: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** one reserved ad slot: the config id and the reserved shape (moved
|
|
95
|
+
* verbatim from adsconfig.ts — the honest ad slot registry of the family
|
|
96
|
+
* site). */
|
|
97
|
+
export type adsslot = {
|
|
98
|
+
/** the ad network slot id — empty until the owner configures it after approval */
|
|
99
|
+
slotid: string;
|
|
100
|
+
/** the display format the reserved box mimics */
|
|
101
|
+
format: string;
|
|
102
|
+
/** the css aspect-ratio value the container reserves (never shifts layout) */
|
|
103
|
+
ratio: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** the seed posts of the family site — the full bodies, verbatim: real,
|
|
107
|
+
* written content about how the family is built, so the site is useful on
|
|
108
|
+
* its own merits (the ad networks approve content, not containers). */
|
|
109
|
+
export const seedPosts: readonly blogpost[] = [
|
|
110
|
+
{
|
|
111
|
+
slug: "ten-sites-one-engine",
|
|
112
|
+
title: "Ten sites, one engine: how the family ships on US$15 a month",
|
|
113
|
+
date: "2026-10-06",
|
|
114
|
+
tags: ["hosting", "static-sites"],
|
|
115
|
+
excerpt:
|
|
116
|
+
"The whole family fits the static hosting free tiers — the only fixed cost is the domains. How ten deployable sites stay on a US$15 a month floor.",
|
|
117
|
+
body: [
|
|
118
|
+
"Every application in the DevThink family builds down to the same shape: one html file, one stylesheet and a handful of hashed bundles at the app root. There is no server to keep warm, no container to babysit and no database bill attached to the public surface. The whole family fits the free tiers of the usual static hosts, so the only fixed cost of the operation is the domains — about US$15 a month.",
|
|
119
|
+
"That number is the budget the family was designed against. The applications share one engine: the sandbox runner and the storage layer live as library-grade modules and get assembled per deploy, so a fix made once lands on every site that imports it. Nothing is provisioned per site beyond the deploy itself. A new application is a folder in the monorepo with the same skeleton as its siblings — config files at the root, the theme in Sol/, pure logic modules beside them and tests in the tests folder.",
|
|
120
|
+
"The trade-offs are real. Static sites cannot hold state, so the public surface of every app stores nothing on the visitor machine: no cookies, no accounts, no tracking. The pieces that genuinely need a backend — the AI gateway, the storage clones — run as separate deploy units on their own free tiers, and the sites talk to them over plain https with no credentials embedded anywhere.",
|
|
121
|
+
"The accounting stays honest. US$15 a month is the floor, not the ceiling: a gateway that outgrows its free tier, or a storage clone that needs a persistent disk, moves the number. But the design keeps the moving parts few and every cost on a line item you can read. Ten working tools for the price of two coffees, and the bill never surprises anyone.",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
slug: "one-css-one-tree",
|
|
126
|
+
title: "One CSS, one tree: the design system behind the family",
|
|
127
|
+
date: "2026-09-29",
|
|
128
|
+
tags: ["design", "front-end"],
|
|
129
|
+
excerpt:
|
|
130
|
+
"A page is a folder, an anchor and one stylesheet. The token set and the flat tree that keep ten sites feeling like one family.",
|
|
131
|
+
body: [
|
|
132
|
+
"Every site in the family carries the same skeleton. A page is a folder directly under Sol/, named after the page, with an anchor file of the same name inside it. Components sit loose beside the anchor, infrastructure lives in a single shell folder and the whole theme styles from one stylesheet. There are no deeper folders. The rule sounds rigid until you maintain ten sites with it — then it is the only reason they stay consistent.",
|
|
133
|
+
"The theme runs on a fixed token set tuned against the trylle ruler: a near-black canvas, ink at #fafafa, eight surface steps from #171717 to #484848 and exactly one accent per application. Hairlines are the ink color mixed down to five and ten percent. Shadows are rings — a one-pixel outline plus doubling blur layers — instead of diffuse glows. Motion is capped at 150 milliseconds for micro-interactions on one shared cubic-bezier curve, and only transform and opacity ever animate.",
|
|
134
|
+
"Each application keeps its own signal color: the sandbox engine wears tan, the gateway violet, the game platform coral, the video studio rose. The site you are reading uses the amber sun the whole family shares. One accent per screen, spent on actions and state, never on decoration — the ninety-ten rule. Everything else stays gray on gray: the hierarchy comes from the surface ladder and the typography, not from color.",
|
|
135
|
+
"The constraints do the work. Because every token, radius, easing and spacing step is shared, a component copied between applications lands looking correct. Because the tree is flat, an audit is a directory listing. And because the stylesheet is one file, the entire design system of a site can be reviewed in a single pass. Ten sites feel like one family because, structurally, they are one.",
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
slug: "no-backend-no-tracking",
|
|
140
|
+
title: "Why every family site ships without a backend or tracking",
|
|
141
|
+
date: "2026-09-18",
|
|
142
|
+
tags: ["privacy", "architecture"],
|
|
143
|
+
excerpt:
|
|
144
|
+
"The only thing a visitor machine does is load the interface. The architecture behind static sites that store nothing — and the one honest exception reserved for ads.",
|
|
145
|
+
body: [
|
|
146
|
+
"The public surface of the DevThink family makes a promise that is easy to state and hard to keep: the only thing a visitor machine does is load the interface. No local storage, no cookies, no analytics beacons, no fingerprinting. The sites are static files served from a CDN; once the bundles arrive, nothing is written anywhere and nothing phones home.",
|
|
147
|
+
"This is partly philosophy and partly architecture. The family's tools are sandboxes, studios and gateways — their honest work happens inside the browser tab or in separate, clearly-labeled deploy units. The public sites present those tools; they do not need to know who you are. So they skip the account systems, the session storage and the third-party scripts that most portfolios bolt on by default. The result loads fast, works offline after the first load and leaks nothing.",
|
|
148
|
+
"The one exception the family is preparing for is advertising, and it is built the honest way. The family site runs reserved ad containers: fixed aspect-ratio boxes, visibly labeled as advertisement, placed at the end of content pages where they cannot be mistaken for navigation or buttons. The ad script itself is only added after the network approves the site, and it arrives through the deploy environment — it is never committed into the code. Until then the containers render as empty reserved boxes.",
|
|
149
|
+
"No automated traffic ever touches an ad surface. Clicking your own ads, or running bots against them, is invalid traffic — the one mistake that ends an ad account and a domain reputation at the same time. The family does not do it, full stop. That is the whole contract: real content on every page, honest empty states where a feature is not configured yet, and a visitor machine that stays the visitor's machine.",
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
/** the directory roster, rail order: the OS first, then the applications —
|
|
155
|
+
* verbatim from roster.ts. */
|
|
156
|
+
export const seedRoster: readonly familymember[] = [
|
|
157
|
+
{
|
|
158
|
+
slug: "devthink",
|
|
159
|
+
accent: "#f59e0b",
|
|
160
|
+
role: "the OS: the one interface where the whole family opens as windows.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
slug: "saddle",
|
|
164
|
+
accent: "#e86f2d",
|
|
165
|
+
role: "the sandbox: runs any sandbox, anywhere, no setup.",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
slug: "forge",
|
|
169
|
+
accent: "#a2cb3a",
|
|
170
|
+
role: "the execution clone: runs any binary in the family sandbox, stores nothing.",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
slug: "foundry",
|
|
174
|
+
accent: "#2fb8b5",
|
|
175
|
+
role: "the complete clone: runs, is the sandbox and keeps the network databases.",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
slug: "vault",
|
|
179
|
+
accent: "#a3d7e6",
|
|
180
|
+
role: "the storage clone: keeps every site database and receives the data backups.",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
slug: "getry",
|
|
184
|
+
accent: "#8b5cf6",
|
|
185
|
+
role: "the AI workspace: five model pools, thirty-five routes, rotating keys.",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
slug: "cadria",
|
|
189
|
+
accent: "#f472b6",
|
|
190
|
+
role: "the video and image studio: player, editor and gallery in one window.",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
slug: "debonair",
|
|
194
|
+
accent: "#d9962e",
|
|
195
|
+
role: "the audio app and DAW: studio, generate and library in one console.",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
slug: "stealthhead",
|
|
199
|
+
accent: "#e8563f",
|
|
200
|
+
role: "the FPS game platform: match, ranking, weapons and world — loads instantly.",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
slug: "argan",
|
|
204
|
+
accent: "#34d8a8",
|
|
205
|
+
role: "the DNS toolkit: zones, records, dnssec and handshake.",
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
/** the pricing table of the /pricing page, in display order — the billing
|
|
210
|
+
* values verbatim from checkoutlinks.ts (the prose answers from the label
|
|
211
|
+
* store; only the billing values ride this lane). */
|
|
212
|
+
export const seedPlans: readonly checkoutplan[] = [
|
|
213
|
+
{
|
|
214
|
+
id: "free",
|
|
215
|
+
priceusd: 0,
|
|
216
|
+
nameKey: "pricing.plan.free.name",
|
|
217
|
+
perKey: "pricing.plan.free.per",
|
|
218
|
+
blurbKey: "pricing.plan.free.blurb",
|
|
219
|
+
featureKeys: ["pricing.plan.free.feat.1", "pricing.plan.free.feat.2", "pricing.plan.free.feat.3"],
|
|
220
|
+
href: "",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "pro",
|
|
224
|
+
priceusd: 5,
|
|
225
|
+
nameKey: "pricing.plan.pro.name",
|
|
226
|
+
perKey: "pricing.plan.pro.per",
|
|
227
|
+
blurbKey: "pricing.plan.pro.blurb",
|
|
228
|
+
featureKeys: ["pricing.plan.pro.feat.1", "pricing.plan.pro.feat.2", "pricing.plan.pro.feat.3"],
|
|
229
|
+
href: "",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "studio",
|
|
233
|
+
priceusd: 19,
|
|
234
|
+
nameKey: "pricing.plan.studio.name",
|
|
235
|
+
perKey: "pricing.plan.studio.per",
|
|
236
|
+
blurbKey: "pricing.plan.studio.blurb",
|
|
237
|
+
featureKeys: ["pricing.plan.studio.feat.1", "pricing.plan.studio.feat.2", "pricing.plan.studio.feat.3"],
|
|
238
|
+
href: "",
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
/** the support channels of the /support page — verbatim from
|
|
243
|
+
* checkoutlinks.ts: hrefs stay empty until the owner configures them. */
|
|
244
|
+
export const seedSupportLinks: Readonly<supportlinks> = {
|
|
245
|
+
sponsors: "",
|
|
246
|
+
kofi: "",
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** the reserved slots per content page, keyed by the page folder name —
|
|
250
|
+
* verbatim from adsconfig.ts. */
|
|
251
|
+
export const seedAdsSlots: Readonly<Record<string, adsslot>> = {
|
|
252
|
+
home: { slotid: "", format: "display", ratio: "728 / 90" },
|
|
253
|
+
apps: { slotid: "", format: "in-feed", ratio: "3 / 1" },
|
|
254
|
+
blog: { slotid: "", format: "in-article", ratio: "3 / 1" },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** the fixed label every ad container renders, visible and honest —
|
|
258
|
+
* verbatim from adsconfig.ts. */
|
|
259
|
+
export const seedAdsLabel = "advertisement";
|
|
260
|
+
|
|
261
|
+
/** the honest note the container renders while no slot id is configured —
|
|
262
|
+
* verbatim from adsconfig.ts. */
|
|
263
|
+
export const seedAdsEmpty = "ad slot not configured yet";
|
|
264
|
+
|
|
265
|
+
/** the content kinds of the lane: the db table rows, the api answer and the
|
|
266
|
+
* accessors cite the same names (the catalog naming law the sibling sites
|
|
267
|
+
* ride — one kind per manifest table). */
|
|
268
|
+
export const CONTENTKINDS = [
|
|
269
|
+
"posts",
|
|
270
|
+
"roster",
|
|
271
|
+
"plans",
|
|
272
|
+
"supportlinks",
|
|
273
|
+
"ads.slots",
|
|
274
|
+
"ads.label",
|
|
275
|
+
"ads.empty",
|
|
276
|
+
] as const;
|
|
277
|
+
|
|
278
|
+
/** one content kind of the lane. */
|
|
279
|
+
export type contentkind = (typeof CONTENTKINDS)[number];
|
|
280
|
+
|
|
281
|
+
/** one content row as the db holds it and the api answers it: the kind
|
|
282
|
+
* names the table, the ordinal pins the row order inside the kind, and the
|
|
283
|
+
* payload carries the json encoding of the row (the manifest is the source
|
|
284
|
+
* of truth, the store serves the rows). */
|
|
285
|
+
export type contentrow = {
|
|
286
|
+
/** the kind the row belongs to */
|
|
287
|
+
kind: contentkind;
|
|
288
|
+
/** the row order inside the kind */
|
|
289
|
+
ordinal: number;
|
|
290
|
+
/** the json encoding of the row */
|
|
291
|
+
payload: string;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/* ------------------------------------------------------------------ */
|
|
295
|
+
/* @section 2 — client: the loader, the door and the mount hook */
|
|
296
|
+
/* ------------------------------------------------------------------ */
|
|
297
|
+
|
|
298
|
+
/** the live rows the platform answered, parsed per kind and ordered by
|
|
299
|
+
* ordinal (null until the first load — the offline law: the seed answers
|
|
300
|
+
* first). */
|
|
301
|
+
let contentlive: ReadonlyMap<string, readonly unknown[]> | null = null;
|
|
302
|
+
|
|
303
|
+
/** the abort window of the platform read (ms) — the page never blocks on a
|
|
304
|
+
* slow gateway (the uistore law). */
|
|
305
|
+
const READ_TIMEOUT_MS = 4000;
|
|
306
|
+
|
|
307
|
+
/** Reads the site content api base off the vite env without leaning on the
|
|
308
|
+
* vite client types: the platform gateway serving the site store over
|
|
309
|
+
* HTTPS (the VITE_CATALOG_URL seat the sibling sites ride); empty answers
|
|
310
|
+
* from the seed. */
|
|
311
|
+
function readContentBase(): string {
|
|
312
|
+
try {
|
|
313
|
+
const env = (import.meta as ImportMeta & { env?: { VITE_CATALOG_URL?: string } }).env;
|
|
314
|
+
return (typeof env?.VITE_CATALOG_URL === "string" ? env.VITE_CATALOG_URL : "").trim().replace(/\/$/, "");
|
|
315
|
+
} catch {
|
|
316
|
+
return "";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** the configured platform base (empty answers from the seed — the offline
|
|
321
|
+
* static build law). */
|
|
322
|
+
const endpoint = readContentBase();
|
|
323
|
+
|
|
324
|
+
/** the route the content lane rides: the platform gateway serves the site
|
|
325
|
+
* store over /api/v1/ui/content (the uistore route family, cobaia's noun —
|
|
326
|
+
* no server route lives in this app, the same law the onboarding tables
|
|
327
|
+
* ride). */
|
|
328
|
+
const CONTENT_PATH = "/api/v1/ui/content";
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Loads the live content rows from the site store: GET /api/v1/ui/content
|
|
332
|
+
* over the configured base (the platform gateway serving the content_rows
|
|
333
|
+
* table of the cobaia store over HTTPS). The failure keeps the seed
|
|
334
|
+
* answering (the offline build law), and the load happens once per
|
|
335
|
+
* session.
|
|
336
|
+
*
|
|
337
|
+
* @returns void — the live store fills; the seed keeps answering on any
|
|
338
|
+
* failure.
|
|
339
|
+
*/
|
|
340
|
+
export async function loadcontent(): Promise<void> {
|
|
341
|
+
if (contentlive) return;
|
|
342
|
+
if (!endpoint) return;
|
|
343
|
+
try {
|
|
344
|
+
const res = await fetch(new URL(CONTENT_PATH, endpoint).toString(), {
|
|
345
|
+
headers: { accept: "application/json" },
|
|
346
|
+
signal: AbortSignal.timeout(READ_TIMEOUT_MS),
|
|
347
|
+
});
|
|
348
|
+
if (!res.ok) return;
|
|
349
|
+
const answer = (await res.json()) as { rows?: contentrow[] };
|
|
350
|
+
if (!Array.isArray(answer.rows)) return;
|
|
351
|
+
const buckets = new Map<string, { ordinal: number; value: unknown }[]>();
|
|
352
|
+
for (const row of answer.rows) {
|
|
353
|
+
if (!row || typeof row.kind !== "string" || typeof row.ordinal !== "number") continue;
|
|
354
|
+
if (typeof row.payload !== "string") continue;
|
|
355
|
+
let value: unknown;
|
|
356
|
+
try {
|
|
357
|
+
value = JSON.parse(row.payload);
|
|
358
|
+
} catch {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const bucket = buckets.get(row.kind);
|
|
362
|
+
if (bucket) bucket.push({ ordinal: row.ordinal, value });
|
|
363
|
+
else buckets.set(row.kind, [{ ordinal: row.ordinal, value }]);
|
|
364
|
+
}
|
|
365
|
+
const rows = new Map<string, readonly unknown[]>();
|
|
366
|
+
for (const [kind, bucket] of buckets) {
|
|
367
|
+
rows.set(
|
|
368
|
+
kind,
|
|
369
|
+
bucket.sort((a, b) => a.ordinal - b.ordinal).map((entry) => entry.value),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
contentlive = rows;
|
|
373
|
+
} catch {
|
|
374
|
+
/* the platform is away: the seed keeps answering */
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Answers one content table of the lane: the live rows the platform
|
|
380
|
+
* answered, the manifest seed otherwise (the uistore chain — live first,
|
|
381
|
+
* seed always). The typed doors the pages import (posts, roster,
|
|
382
|
+
* checkoutlinks, adsconfig) project their typed view through this one
|
|
383
|
+
* seat, so no door ever answers a fixed table of its own.
|
|
384
|
+
*
|
|
385
|
+
* @param kind the content kind (one of CONTENTKINDS).
|
|
386
|
+
* @param seed the manifest seed rows of the kind.
|
|
387
|
+
* @returns the live rows when the platform answered, the seed otherwise.
|
|
388
|
+
*/
|
|
389
|
+
export function contenttable<T>(kind: contentkind, seed: readonly T[]): readonly T[] {
|
|
390
|
+
const live = contentlive?.get(kind);
|
|
391
|
+
return Array.isArray(live) ? (live as readonly T[]) : seed;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Answers one singleton content value of the lane: the live row the
|
|
396
|
+
* platform answered, the manifest seed otherwise (the one-row kinds — the
|
|
397
|
+
* support channels record, the ad slot registry, the ad contract strings).
|
|
398
|
+
*
|
|
399
|
+
* @param kind the content kind (one of CONTENTKINDS).
|
|
400
|
+
* @param seed the manifest seed value of the kind.
|
|
401
|
+
* @returns the live value when the platform answered, the seed otherwise.
|
|
402
|
+
*/
|
|
403
|
+
export function contentvalue<T>(kind: contentkind, seed: T): T {
|
|
404
|
+
const live = contentlive?.get(kind);
|
|
405
|
+
return Array.isArray(live) && live.length > 0 ? (live[0] as T) : seed;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* useContent — the mount hook of the store-riding page: it loads the live
|
|
410
|
+
* rows once per mount and re-renders the face when they land. The seed
|
|
411
|
+
* answers the first paint with the same rows the db serves, so the swap
|
|
412
|
+
* never flashes.
|
|
413
|
+
*/
|
|
414
|
+
export function useContent(): void {
|
|
415
|
+
const [, bump] = useState(0);
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
let alive = true;
|
|
418
|
+
void loadcontent().then(() => {
|
|
419
|
+
if (alive) bump((n) => n + 1);
|
|
420
|
+
});
|
|
421
|
+
return () => {
|
|
422
|
+
alive = false;
|
|
423
|
+
};
|
|
424
|
+
}, []);
|
|
425
|
+
}
|