ugly-app 0.1.479 → 0.1.481

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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.479";
1
+ export declare const CLI_VERSION = "0.1.481";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.479";
2
+ export const CLI_VERSION = "0.1.481";
3
3
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrapApp.d.ts","sourceRoot":"","sources":["../../src/client/bootstrapApp.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAOzE,OAAO,EAAmB,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAGnF,UAAU,mBAAmB;IAC3B,mFAAmF;IACnF,QAAQ,EAAE,eAAe,CAAC;IAE1B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,eAAe,CAAC;IAE3B,uDAAuD;IACvD,cAAc,EAAE,aAAa,CAAC;QAC5B,QAAQ,EAAE,SAAS,CAAC;QACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC;QAChC,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,CAAC,CAAC;IAEH,+EAA+E;IAC/E,MAAM,EAAE,MAAM,YAAY,CAAC;IAE3B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAE5B,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,qFAAqF;IACrF,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAEhC,iFAAiF;IACjF,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAoF/D"}
1
+ {"version":3,"file":"bootstrapApp.d.ts","sourceRoot":"","sources":["../../src/client/bootstrapApp.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAOzE,OAAO,EAAmB,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAInF,UAAU,mBAAmB;IAC3B,mFAAmF;IACnF,QAAQ,EAAE,eAAe,CAAC;IAE1B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,eAAe,CAAC;IAE3B,uDAAuD;IACvD,cAAc,EAAE,aAAa,CAAC;QAC5B,QAAQ,EAAE,SAAS,CAAC;QACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC;QAChC,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,CAAC,CAAC;IAEH,+EAA+E;IAC/E,MAAM,EAAE,MAAM,YAAY,CAAC;IAE3B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAE5B,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,qFAAqF;IACrF,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAEhC,iFAAiF;IACjF,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAsF/D"}
@@ -7,6 +7,7 @@ import { AppProvider } from './AppProvider.js';
7
7
  import { KeyboardProvider } from './components/KeyboardProvider.js';
8
8
  import { createSocket } from './createSocket.js';
9
9
  import { StringsProvider } from './StringsProvider.js';
10
+ import { installUglyStudioPreviewBridge } from './uglyStudioPreviewBridge.js';
10
11
  import { createUglyBotSocket } from './uglyBotSocket.js';
11
12
  const w = typeof window !== 'undefined' ? window : {};
12
13
  export function bootstrapApp(options) {
@@ -14,6 +15,7 @@ export function bootstrapApp(options) {
14
15
  const enableKeyboard = keyboardOpt !== false;
15
16
  const rootEl = typeof rootOpt === 'string' ? document.querySelector(rootOpt) : rootOpt;
16
17
  const root = createRoot(rootEl);
18
+ installUglyStudioPreviewBridge();
17
19
  // Initialize error/feedback reporting
18
20
  const uglyBotUrl = w['__UGLY_BOT_URL__'] ?? 'https://ugly.bot';
19
21
  const uglyBotProjectId = w['__UGLY_BOT_PROJECT_ID__'] ?? '';
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrapApp.js","sourceRoot":"","sources":["../../src/client/bootstrapApp.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bootstrapApp.js","sourceRoot":"","sources":["../../src/client/bootstrapApp.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function installUglyStudioPreviewBridge(): void;
2
+ //# sourceMappingURL=uglyStudioPreviewBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uglyStudioPreviewBridge.d.ts","sourceRoot":"","sources":["../../src/client/uglyStudioPreviewBridge.ts"],"names":[],"mappings":"AAyBA,wBAAgB,8BAA8B,IAAI,IAAI,CAgDrD"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Bridge between the ugly-studio Preview panel (parent frame) and the
3
+ * running app (this iframe). Studio's Preview posts simulated device
4
+ * insets via:
5
+ * { source: 'ugly-studio-preview-cmd', type: 'deviceFrame',
6
+ * data: { safeAreaInsetTop, safeAreaInsetBottom,
7
+ * safeAreaInsetLeft, safeAreaInsetRight,
8
+ * keyboardInsetHeight } }
9
+ *
10
+ * We apply those values two ways so every consumer in the app reacts:
11
+ *
12
+ * 1. CSS variables on `:root` — `--safe-area-inset-*` and
13
+ * `--keyboard-inset-height`. Components that style via
14
+ * `var(--safe-area-inset-bottom)` pick this up automatically.
15
+ * 2. The same custom events `KeyboardProvider` already listens for
16
+ * from the native bridge: `safe-area-insets` and `keyboard-height`.
17
+ * This drives the React context (`useSafeAreaInsets`,
18
+ * `useKeyboardHeight`) used by `KeyboardSpacer` and consumers like
19
+ * `SafeAreaTestPage`.
20
+ *
21
+ * Safe to call unconditionally — the listener is cheap, and standalone
22
+ * runs simply never receive the studio message.
23
+ */
24
+ const INSTALLED_FLAG = '__uglyStudioPreviewBridgeInstalled__';
25
+ export function installUglyStudioPreviewBridge() {
26
+ if (typeof window === 'undefined')
27
+ return;
28
+ const w = window;
29
+ if (w[INSTALLED_FLAG])
30
+ return;
31
+ w[INSTALLED_FLAG] = true;
32
+ window.addEventListener('message', (event) => {
33
+ const msg = event.data;
34
+ if (!msg || typeof msg !== 'object')
35
+ return;
36
+ if (msg.source !== 'ugly-studio-preview-cmd')
37
+ return;
38
+ if (msg.type !== 'deviceFrame')
39
+ return;
40
+ const d = msg.data ?? {};
41
+ const insets = {
42
+ top: numOr0(d.safeAreaInsetTop),
43
+ right: numOr0(d.safeAreaInsetRight),
44
+ bottom: numOr0(d.safeAreaInsetBottom),
45
+ left: numOr0(d.safeAreaInsetLeft),
46
+ };
47
+ const keyboardHeight = numOr0(d.keyboardInsetHeight);
48
+ const root = document.documentElement;
49
+ root.style.setProperty('--safe-area-inset-top', `${insets.top}px`);
50
+ root.style.setProperty('--safe-area-inset-right', `${insets.right}px`);
51
+ root.style.setProperty('--safe-area-inset-bottom', `${insets.bottom}px`);
52
+ root.style.setProperty('--safe-area-inset-left', `${insets.left}px`);
53
+ root.style.setProperty('--keyboard-inset-height', `${keyboardHeight}px`);
54
+ window.dispatchEvent(new CustomEvent('safe-area-insets', { detail: insets }));
55
+ window.dispatchEvent(new CustomEvent('keyboard-height', { detail: { height: keyboardHeight } }));
56
+ });
57
+ }
58
+ function numOr0(v) {
59
+ return typeof v === 'number' && Number.isFinite(v) ? v : 0;
60
+ }
61
+ //# sourceMappingURL=uglyStudioPreviewBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uglyStudioPreviewBridge.js","sourceRoot":"","sources":["../../src/client/uglyStudioPreviewBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAE9D,MAAM,UAAU,8BAA8B;IAC5C,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO;IAC1C,MAAM,CAAC,GAAG,MAA4C,CAAC;IACvD,IAAI,CAAC,CAAC,cAAc,CAAC;QAAE,OAAO;IAC9B,CAAC,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;IAezB,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAmB,EAAE,EAAE;QACzD,MAAM,GAAG,GAAG,KAAK,CAAC,IAA6C,CAAC;QAChE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO;QAC5C,IAAI,GAAG,CAAC,MAAM,KAAK,yBAAyB;YAAE,OAAO;QACrD,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa;YAAE,OAAO;QAEvC,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG;YACb,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC;YAC/B,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC;YACnC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC;YACrC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC;SAClC,CAAC;QACF,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC;QAErD,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC;QACtC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,uBAAuB,EAAE,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;QACnE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,yBAAyB,EAAE,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;QACvE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,0BAA0B,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QACzE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,wBAAwB,EAAE,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;QACrE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,yBAAyB,EAAE,GAAG,cAAc,IAAI,CAAC,CAAC;QAEzE,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CACxD,CAAC;QACF,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,CAAC,CAC3E,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,MAAM,CAAC,CAAU;IACxB,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.479",
3
+ "version": "0.1.481",
4
4
  "type": "module",
5
5
  "main": "./dist/server/index.js",
6
6
  "exports": {
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.479";
2
+ export const CLI_VERSION = "0.1.481";
@@ -8,6 +8,7 @@ import { AppProvider } from './AppProvider.js';
8
8
  import { KeyboardProvider } from './components/KeyboardProvider.js';
9
9
  import { createSocket } from './createSocket.js';
10
10
  import { StringsProvider, type StringsProviderConfig } from './StringsProvider.js';
11
+ import { installUglyStudioPreviewBridge } from './uglyStudioPreviewBridge.js';
11
12
  import { createUglyBotSocket } from './uglyBotSocket.js';
12
13
 
13
14
  interface BootstrapAppOptions {
@@ -64,6 +65,8 @@ export function bootstrapApp(options: BootstrapAppOptions): void {
64
65
  typeof rootOpt === 'string' ? document.querySelector(rootOpt)! : rootOpt;
65
66
  const root = createRoot(rootEl);
66
67
 
68
+ installUglyStudioPreviewBridge();
69
+
67
70
  // Initialize error/feedback reporting
68
71
  const uglyBotUrl = w['__UGLY_BOT_URL__'] ?? 'https://ugly.bot';
69
72
  const uglyBotProjectId = w['__UGLY_BOT_PROJECT_ID__'] ?? '';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Bridge between the ugly-studio Preview panel (parent frame) and the
3
+ * running app (this iframe). Studio's Preview posts simulated device
4
+ * insets via:
5
+ * { source: 'ugly-studio-preview-cmd', type: 'deviceFrame',
6
+ * data: { safeAreaInsetTop, safeAreaInsetBottom,
7
+ * safeAreaInsetLeft, safeAreaInsetRight,
8
+ * keyboardInsetHeight } }
9
+ *
10
+ * We apply those values two ways so every consumer in the app reacts:
11
+ *
12
+ * 1. CSS variables on `:root` — `--safe-area-inset-*` and
13
+ * `--keyboard-inset-height`. Components that style via
14
+ * `var(--safe-area-inset-bottom)` pick this up automatically.
15
+ * 2. The same custom events `KeyboardProvider` already listens for
16
+ * from the native bridge: `safe-area-insets` and `keyboard-height`.
17
+ * This drives the React context (`useSafeAreaInsets`,
18
+ * `useKeyboardHeight`) used by `KeyboardSpacer` and consumers like
19
+ * `SafeAreaTestPage`.
20
+ *
21
+ * Safe to call unconditionally — the listener is cheap, and standalone
22
+ * runs simply never receive the studio message.
23
+ */
24
+ const INSTALLED_FLAG = '__uglyStudioPreviewBridgeInstalled__';
25
+
26
+ export function installUglyStudioPreviewBridge(): void {
27
+ if (typeof window === 'undefined') return;
28
+ const w = window as unknown as Record<string, unknown>;
29
+ if (w[INSTALLED_FLAG]) return;
30
+ w[INSTALLED_FLAG] = true;
31
+
32
+ interface DeviceFrameData {
33
+ safeAreaInsetTop?: number;
34
+ safeAreaInsetBottom?: number;
35
+ safeAreaInsetLeft?: number;
36
+ safeAreaInsetRight?: number;
37
+ keyboardInsetHeight?: number;
38
+ }
39
+ interface DeviceFrameMessage {
40
+ source?: string;
41
+ type?: string;
42
+ data?: DeviceFrameData;
43
+ }
44
+
45
+ window.addEventListener('message', (event: MessageEvent) => {
46
+ const msg = event.data as DeviceFrameMessage | null | undefined;
47
+ if (!msg || typeof msg !== 'object') return;
48
+ if (msg.source !== 'ugly-studio-preview-cmd') return;
49
+ if (msg.type !== 'deviceFrame') return;
50
+
51
+ const d = msg.data ?? {};
52
+ const insets = {
53
+ top: numOr0(d.safeAreaInsetTop),
54
+ right: numOr0(d.safeAreaInsetRight),
55
+ bottom: numOr0(d.safeAreaInsetBottom),
56
+ left: numOr0(d.safeAreaInsetLeft),
57
+ };
58
+ const keyboardHeight = numOr0(d.keyboardInsetHeight);
59
+
60
+ const root = document.documentElement;
61
+ root.style.setProperty('--safe-area-inset-top', `${insets.top}px`);
62
+ root.style.setProperty('--safe-area-inset-right', `${insets.right}px`);
63
+ root.style.setProperty('--safe-area-inset-bottom', `${insets.bottom}px`);
64
+ root.style.setProperty('--safe-area-inset-left', `${insets.left}px`);
65
+ root.style.setProperty('--keyboard-inset-height', `${keyboardHeight}px`);
66
+
67
+ window.dispatchEvent(
68
+ new CustomEvent('safe-area-insets', { detail: insets }),
69
+ );
70
+ window.dispatchEvent(
71
+ new CustomEvent('keyboard-height', { detail: { height: keyboardHeight } }),
72
+ );
73
+ });
74
+ }
75
+
76
+ function numOr0(v: unknown): number {
77
+ return typeof v === 'number' && Number.isFinite(v) ? v : 0;
78
+ }
@@ -81,6 +81,12 @@ Every endpoint is accessible via both WebSocket (`socket.request(name, input)`)
81
81
  - Navigate: `useRouter().push('route-key', params)`
82
82
  - Popups: always use `useRouter().openPopup(<Component />, { mode: 'transient' })` — never custom fixed overlays
83
83
 
84
+ ### Home page is the primary surface
85
+ - The home route is `''` in `shared/pages.ts`, rendered by `client/pages/HomePage.tsx`.
86
+ - When building or customizing the app's main functionality, **edit `HomePage.tsx`** — replace its body with the requested UI. Do not add a new route just to land the user on it.
87
+ - Only add new pages for secondary navigation the user explicitly asks for (settings, detail views, multi-screen flows). One screen ⇒ home page edit. Multiple screens ⇒ home page + extra routes.
88
+ - Demo/test pages under `client/pages/` (`AuthDemoPage`, `TodoDemoPage`, `*TestPage`, etc.) are scaffolding — delete the ones you don't need as you build the real app.
89
+
84
90
  ## Critical rules
85
91
  - **Never** change a collection schema without running `npm run db:schema-gen` and fixing the generated migration
86
92
  - **Always** include a `schema: ZodSchema` when defining a collection — it's required
@@ -1,6 +1,37 @@
1
- import React from 'react';
1
+ import { nanoid } from 'nanoid';
2
+ import React, { useEffect, useRef, useState } from 'react';
2
3
  import { Button, Card, PageLayout, Text, useApp } from 'ugly-app/client';
3
4
 
5
+ // ─── Session helpers (used for the experiment / analytics demo below) ─────────
6
+
7
+ function getSessionId(): string {
8
+ const key = 'sessionId';
9
+ const existing = sessionStorage.getItem(key);
10
+ if (existing) return existing;
11
+ const id = nanoid();
12
+ sessionStorage.setItem(key, id);
13
+ return id;
14
+ }
15
+
16
+ // Lightweight HTTP RPC helper for pages that run before socket auth.
17
+ // For authenticated pages with a socket, use socket.request() instead.
18
+ async function rpc<T>(name: string, input: unknown): Promise<T> {
19
+ const res = await fetch(`/api/${name}`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({ input }),
23
+ });
24
+ const json = (await res.json()) as { result: T };
25
+ return json.result;
26
+ }
27
+
28
+ // CTA label per experiment branch — 'cta-test' must match the experiment id
29
+ // declared in shared/experiments.ts.
30
+ function getCtaLabel(branches: Record<string, string>): string {
31
+ if (branches['cta-test'] === 'treatment') return 'Try it free';
32
+ return 'Get started';
33
+ }
34
+
4
35
  function openLogin(): void {
5
36
  window.open(
6
37
  `https://ugly.bot/oauth?origin=${encodeURIComponent(
@@ -68,6 +99,33 @@ function AuthDemoAuthenticated(): React.ReactElement {
68
99
  }
69
100
 
70
101
  function AuthDemoUnauthenticated(): React.ReactElement {
102
+ const sessionId = useRef(getSessionId());
103
+ const [branches, setBranches] = useState<Record<string, string>>({});
104
+
105
+ // initSession returns experiment branch assignments and captures SESSION_START.
106
+ // Analytics failures must not block the UI — degrade silently.
107
+ useEffect(() => {
108
+ rpc<{ branches: Record<string, string> }>('initSession', {
109
+ sessionId: sessionId.current,
110
+ })
111
+ .then(({ branches: b }) => { setBranches(b); })
112
+ .catch(() => {
113
+ // Default branch values stay in place
114
+ });
115
+ }, []);
116
+
117
+ function handleCtaClick(): void {
118
+ void rpc<{ eventId: string }>('captureEvent', {
119
+ eventName: 'CTA_CLICK',
120
+ sessionId: sessionId.current,
121
+ properties: { page: 'auth-demo' },
122
+ }).catch((_e: unknown) => undefined);
123
+
124
+ openLogin();
125
+ }
126
+
127
+ const ctaLabel = getCtaLabel(branches);
128
+
71
129
  return (
72
130
  <PageLayout
73
131
  header={
@@ -78,9 +136,18 @@ function AuthDemoUnauthenticated(): React.ReactElement {
78
136
  >
79
137
  <div>
80
138
  <h1>Auth Demo</h1>
139
+ <Text style={{ display: 'block', marginBottom: 12 }}>
140
+ Demonstrates ugly.bot OAuth login + session-scoped experiment
141
+ analytics. The button label is driven by the <code>cta-test</code>
142
+ {' '}experiment in <code>shared/experiments.ts</code>.
143
+ </Text>
81
144
  <Card>
82
145
  <Text>You are not logged in.</Text>
83
- <Button onClick={openLogin}>Login with ugly.bot</Button>
146
+ <div style={{ marginTop: 12 }}>
147
+ <Button variant="primary" onClick={handleCtaClick}>
148
+ {ctaLabel}
149
+ </Button>
150
+ </div>
84
151
  </Card>
85
152
  </div>
86
153
  </PageLayout>
@@ -1,184 +1,20 @@
1
- import { nanoid } from 'nanoid';
2
- import React, { useEffect, useRef, useState } from 'react';
3
- import { Button, Card, PageLayout, Text, useApp } from 'ugly-app/client';
1
+ import React from 'react';
2
+ import { Card, PageLayout, Text } from 'ugly-app/client';
4
3
 
5
- // ─── Session helpers ──────────────────────────────────────────────────────────
6
-
7
- function getSessionId(): string {
8
- const key = 'sessionId';
9
- const existing = sessionStorage.getItem(key);
10
- if (existing) return existing;
11
- const id = nanoid();
12
- sessionStorage.setItem(key, id);
13
- return id;
14
- }
15
-
16
- // Lightweight HTTP RPC helper for pages that run before socket auth.
17
- // For authenticated pages with a socket, use socket.request() instead.
18
- async function rpc<T>(name: string, input: unknown): Promise<T> {
19
- const res = await fetch(`/api/${name}`, {
20
- method: 'POST',
21
- headers: { 'Content-Type': 'application/json' },
22
- body: JSON.stringify({ input }),
23
- });
24
- const json = (await res.json()) as { result: T };
25
- return json.result;
26
- }
27
-
28
- // ─── CTA label per experiment branch ─────────────────────────────────────────
29
-
30
- function getCtaLabel(branches: Record<string, string>): string {
31
- // Add more experiment-driven labels here as you add experiments.
32
- // 'cta-test' must match the experiment id in shared/experiments.ts.
33
- if (branches['cta-test'] === 'treatment') return 'Try it free';
34
- return 'Get started'; // control branch (or default while loading)
35
- }
36
-
37
- // ─── Page components ──────────────────────────────────────────────────────────
38
-
39
- const UGLY_BOT_URL = (window as unknown as Record<string, string>).__UGLY_BOT_URL__ ?? 'https://ugly.bot';
40
-
41
- function openLogin(): void {
42
- window.open(
43
- `${UGLY_BOT_URL}/oauth?origin=${encodeURIComponent(
44
- window.location.origin,
45
- )}`,
46
- 'ugly-bot-login',
47
- `width=480,height=640,left=${Math.round(
48
- window.screenX + (window.outerWidth - 480) / 2,
49
- )},top=${Math.round(window.screenY + (window.outerHeight - 640) / 2)}`,
50
- );
51
-
52
- function onMessage(event: MessageEvent): void {
53
- if (event.origin !== UGLY_BOT_URL) return;
54
- const data = event.data as { type?: string; code?: string } | null;
55
- if (!data?.type || data.type !== 'ugly-bot-oauth' || !data.code) return;
56
- window.removeEventListener('message', onMessage);
57
- void fetch('/auth/verify', {
58
- method: 'POST',
59
- headers: { 'Content-Type': 'application/json' },
60
- body: JSON.stringify({ code: data.code }),
61
- }).then((res) => {
62
- if (res.ok) window.location.reload();
63
- });
64
- }
65
- window.addEventListener('message', onMessage);
66
- }
67
-
68
- function HomePageAuthenticated(): React.ReactElement {
69
- const app = useApp();
70
-
71
- async function handleLogout(): Promise<void> {
72
- await fetch('/auth/logout', { method: 'POST' });
73
- window.location.reload();
74
- }
75
-
76
- return (
77
- <PageLayout
78
- header={
79
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 24px' }}>
80
- <Text size="lg" weight="bold">My App</Text>
81
- <Button variant="secondary" onClick={() => { void handleLogout(); }}>
82
- Logout
83
- </Button>
84
- </div>
85
- }
86
- >
87
- <HomePageBody userId={app.userId} />
88
- </PageLayout>
89
- );
90
- }
91
-
92
- function HomePageUnauthenticated(): React.ReactElement {
4
+ // This is your app's home page. Replace the body below with the UI for
5
+ // whatever you're building. The `''` route in shared/pages.ts maps here.
6
+ export default function HomePage(): React.ReactElement {
93
7
  return (
94
- <PageLayout
95
- header={
96
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 24px' }}>
97
- <Text size="lg" weight="bold">My App</Text>
98
- <Button variant="primary" onClick={openLogin}>
99
- Login
100
- </Button>
101
- </div>
102
- }
103
- >
104
- <HomePageBody userId={null} />
8
+ <PageLayout>
9
+ <div style={{ maxWidth: 640, margin: '0 auto', padding: 24 }}>
10
+ <Card>
11
+ <Text size="xl" weight="bold">Welcome</Text>
12
+ <Text style={{ marginTop: 8 }}>
13
+ This is the home page. Edit <code>client/pages/HomePage.tsx</code>
14
+ {' '}to build your app.
15
+ </Text>
16
+ </Card>
17
+ </div>
105
18
  </PageLayout>
106
19
  );
107
20
  }
108
-
109
- // Rendered for both authenticated and unauthenticated users.
110
- // The experiment CTA is intentionally shown only when userId is null.
111
- function HomePageBody({
112
- userId,
113
- }: {
114
- userId: string | null;
115
- }): React.ReactElement {
116
- const sessionId = useRef(getSessionId());
117
- const [branches, setBranches] = useState<Record<string, string>>({});
118
-
119
- // Initialise session: captures SESSION_START and returns experiment branches.
120
- // Failures are silently swallowed so analytics never block the UI.
121
- useEffect(() => {
122
- rpc<{ branches: Record<string, string> }>('initSession', {
123
- sessionId: sessionId.current,
124
- })
125
- .then(({ branches: b }) => { setBranches(b); })
126
- .catch(() => {
127
- // Degrade gracefully — UI uses default branch values
128
- });
129
- }, []);
130
-
131
- function handleCtaClick(): void {
132
- // Fire-and-forget — do not await or block on analytics
133
- void rpc<{ eventId: string }>('captureEvent', {
134
- eventName: 'CTA_CLICK',
135
- sessionId: sessionId.current,
136
- properties: { page: 'home' },
137
- }).catch((_e: unknown) => undefined);
138
-
139
- openLogin();
140
- }
141
-
142
- const ctaLabel = getCtaLabel(branches);
143
-
144
- return (
145
- <div style={{ maxWidth: 640, margin: '0 auto', padding: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
146
- <Card>
147
- <Text size="xl" weight="bold">
148
- Welcome
149
- </Text>
150
- <Text style={{ marginTop: 4 }}>
151
- {userId
152
- ? `Logged in as: ${userId}`
153
- : 'This app was built with ugly-app.'}
154
- </Text>
155
- {!userId && (
156
- <div style={{ marginTop: 12 }}>
157
- {/* CTA label is experiment-driven — see shared/experiments.ts */}
158
- <Button variant="primary" onClick={handleCtaClick}>
159
- {ctaLabel}
160
- </Button>
161
- </div>
162
- )}
163
- </Card>
164
- </div>
165
- );
166
- }
167
-
168
- export default function HomePage(): React.ReactElement {
169
- // Check both that a token exists AND that AppProvider is available.
170
- // When the token is present but invalid (e.g. expired), the socket
171
- // connection fails and we render without AppProvider — so we must
172
- // fall back to the unauthenticated view to avoid a useApp() crash
173
- // that causes an infinite reload loop.
174
- let isLoggedIn = false;
175
- try {
176
- // useApp throws if there is no AppProvider ancestor
177
- useApp();
178
- isLoggedIn = true;
179
- } catch {
180
- isLoggedIn = false;
181
- }
182
- if (isLoggedIn) return <HomePageAuthenticated />;
183
- return <HomePageUnauthenticated />;
184
- }