ugly-app 0.1.480 → 0.1.482
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/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.482";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
package/package.json
CHANGED
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.482";
|
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
-
<
|
|
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
|
|
2
|
-
import
|
|
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
|
-
//
|
|
6
|
-
|
|
7
|
-
function
|
|
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
|
-
|
|
96
|
-
<
|
|
97
|
-
<Text size="
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
}
|