Skip to content

Commit 2dc20d2

Browse files
committed
feat(cli): handle errors and make it fancy
1 parent 1fee627 commit 2dc20d2

File tree

16 files changed

+832
-359
lines changed

16 files changed

+832
-359
lines changed

.github/workflows/cli-release.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ jobs:
1818
with:
1919
fetch-depth: 0
2020
fetch-tags: false
21-
21+
2222
- name: ⎔ Setup bun
2323
uses: oven-sh/setup-bun@v2
2424

25+
- name: 📥 Download deps on Mono
26+
run: bun install --frozen-lockfile
27+
2528
- name: 📥 Download deps
2629
run: bun install --frozen-lockfile
2730
working-directory: atw-cli

atw-cli/bun.lock

Lines changed: 523 additions & 6 deletions
Large diffs are not rendered by default.

atw-cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "atw-cli",
33
"module": "index.ts",
44
"type": "module",
5-
"version": "1.0.0",
5+
"version": "1.1.0",
66
"private": true,
77
"devDependencies": {
88
"@types/bun": "latest",
@@ -16,7 +16,7 @@
1616
"commander": "^13.1.0",
1717
"ink": "^5.1.1",
1818
"ink-link": "^4.1.0",
19-
"lib": "workspace:*",
19+
"@rocicorp/zero": "^0.16.2025022602",
2020
"picocolors": "^1.1.1",
2121
"react": "^18",
2222
"react-devtools-core": "^6.1.1"
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1+
import { ATW_API_BASEURL } from "../config"
12

23
export type Event = {
34
id: string
45
slug: string
56
name: string
6-
startDate: string
7+
startDate: number
78
shortLocation: string|null
89
}
910

1011
export const registerAction = async (email: string, eventId: Event['id']) => {
11-
const response = await fetch(`https://allthingsweb.dev/api/events/${eventId}/register`, {
12+
const response = await fetch(`${ATW_API_BASEURL}/events/${eventId}/register`, {
1213
method: 'POST',
1314
body: JSON.stringify({ email })
1415
})
15-
if (!response.ok) {
16-
throw new Error('Failed to register')
17-
}
18-
const { success } = await response.json()
19-
return success
16+
return await response.json()
2017
}

atw-cli/src/commands/register.tsx

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,24 @@
11
import { Command } from "commander";
2-
import { Box, Newline, render, Text } from 'ink';
3-
import { FooterSpeakers } from "../ui/speaker";
4-
import { RegisterJourney } from "../ui/register-journey";
2+
import { render } from 'ink';
3+
import { RegisterJourney } from "../ui/journeys/register";
54
import pc from "picocolors";
6-
import { ZeroProvider } from "@rocicorp/zero/react";
7-
import { Zero } from "@rocicorp/zero";
85
import { logo } from "..";
9-
import { schema } from "@lib/zero-sync/schema";
6+
import { AppLayout } from "../ui/app-layout";
107

118
export const createRegisterCommand = (): Command => {
129
const command = new Command("register")
1310
.description("Register to an event.")
1411
.action(async () => {
1512
console.log(pc.dim(logo));
16-
const z = new Zero({
17-
userID: "anon",
18-
server: 'https://allthingsweb-sync.fly.dev',
19-
schema,
20-
kvStore: 'mem',
21-
});
2213
const { waitUntilExit, unmount } = render(
23-
<ZeroProvider zero={z}>
24-
<>
25-
<Text bold italic>Register to an event!</Text>
14+
<AppLayout title="Register to an event!">
2615
<RegisterJourney unmount={() => unmount()} />
27-
<Newline />
28-
<Box flexDirection="column" padding={1} gap={1}>
29-
<FooterSpeakers />
30-
</Box>
31-
</>
32-
</ZeroProvider>,
16+
</AppLayout>,
3317
{
3418
exitOnCtrlC: true,
3519
}
3620
);
3721
await waitUntilExit();
38-
39-
40-
render(<Box flexDirection="column" padding={1} gap={1}>
41-
<Text>Register to an event!</Text>
42-
</Box>)
4322
});
4423
return command;
4524
}

atw-cli/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const ATW_BASEURL = process.env.AWT_BASEURL || "https://allthingsweb.dev";
2+
export const ATW_API_BASEURL = process.env.ATW_API_BASEURL || `${ATW_BASEURL}/api`;

atw-cli/src/ui/app-layout.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Box} from 'ink';
2+
import { ZeroProvider } from "@rocicorp/zero/react";
3+
import { Zero } from "@rocicorp/zero";
4+
import { schema } from "@lib/zero-sync/schema";
5+
import { ThemeProvider} from '@inkjs/ui';
6+
import type React from "react";
7+
import { customTheme } from './theme';
8+
import { Header } from './components/header';
9+
import { Footer } from './footer';
10+
11+
type AppLayoutProps = {
12+
children: React.ReactNode;
13+
title: string
14+
}
15+
16+
export const AppLayout = ({children, title}: AppLayoutProps): React.ReactNode => {
17+
const z = new Zero({
18+
userID: "anon",
19+
server: 'https://allthingsweb-sync.fly.dev',
20+
schema,
21+
kvStore: 'mem',
22+
});
23+
return <ZeroProvider zero={z}>
24+
<ThemeProvider theme={customTheme}>
25+
<Header level={1}>{title}</Header>
26+
<Box flexDirection="column" padding={1}>{children}</Box>
27+
<Box flexDirection="column">
28+
<Footer />
29+
</Box>
30+
31+
</ThemeProvider>
32+
</ZeroProvider>
33+
34+
35+
}

atw-cli/src/ui/components/header.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Box, Text } from "ink";
2+
import { colors } from "../theme";
3+
4+
type HeaderProps = {
5+
level?: 1 | 2;
6+
children: React.ReactNode;
7+
}
8+
9+
export const Header = ({ level = 1, children }: HeaderProps) => {
10+
const colorMap = {
11+
1: colors.mainBlue,
12+
2: '#FF6600',
13+
}
14+
return <Box
15+
flexDirection="row"
16+
borderStyle={"single"}
17+
borderTop={false}
18+
borderLeft={false}
19+
borderRight={false}
20+
borderBottom={true}
21+
borderBottomColor={colors.mainBlue}
22+
><Text bold italic color={colorMap[level]}>{children}</Text></Box>
23+
}

atw-cli/src/ui/footer.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { schema } from "@lib/zero-sync/schema";
2+
import { useQuery, useZero } from "@rocicorp/zero/react";
3+
import { Newline, Text, Box } from "ink"
4+
import Link from "ink-link"
5+
import { useEffect, useState } from "react"
6+
import { colors } from "./theme";
7+
import { Spinner } from "@inkjs/ui";
8+
9+
export const Footer = () => {
10+
const z = useZero<typeof schema>();
11+
const [speakers] = useQuery(z.query.profiles);
12+
const [speakerIndex, setSpeakerIndex] = useState(Math.floor(Math.random() * 10) + 1)
13+
14+
useEffect(() => {
15+
const interval = setInterval(() => {
16+
setSpeakerIndex((speakerIndex + 1) % speakers.length)
17+
}, 2000)
18+
return () => clearInterval(interval)
19+
}, [speakerIndex, speakers.length])
20+
21+
const speaker = speakers[speakerIndex]
22+
23+
const link = (()=>{
24+
if (speaker?.linkedinHandle) {
25+
return `https://www.linkedin.com/in/${speaker.linkedinHandle}`
26+
}
27+
if (speaker?.twitterHandle) {
28+
return `https://x.com/${speaker.twitterHandle}`
29+
}
30+
if (speaker?.blueskyHandle) {
31+
return `https://bsky.app/profile/${speaker.blueskyHandle}`
32+
}
33+
})();
34+
return (
35+
<>
36+
<Newline />
37+
<Text dimColor>------------------------ </Text>
38+
<Box>
39+
{!speaker && <Spinner type='dots10' label="Loading speakers..." />}
40+
{speaker && <Text italic dimColor>
41+
<Text>
42+
{speaker.name} - <Text color={colors.mainOrange} dimColor>{speaker?.title}</Text>
43+
<Newline />
44+
{link && <Link url={link}>
45+
<Text color={colors.mainOrange} dimColor>{link}</Text>
46+
</Link>}
47+
</Text>
48+
</Text>}
49+
</Box>
50+
</>
51+
)
52+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
import { type Event } from "../../../actions/register"
3+
type State = {
4+
event: Event | null
5+
hasSucceeded: boolean | null
6+
isSubmitting: boolean
7+
error: string | null
8+
email: string | null
9+
}
10+
11+
export const initialState: State = {
12+
event: null,
13+
hasSucceeded: null,
14+
isSubmitting: false,
15+
error: null,
16+
email: null
17+
}
18+
19+
type Action = {
20+
type: 'SELECT_EVENT'
21+
event: Event
22+
} | {
23+
type: 'SET_EMAIL'
24+
email: string
25+
} | {
26+
type: 'HANDLE_API_RESULTS'
27+
results: {
28+
error?: string
29+
success?: boolean
30+
}
31+
}
32+
33+
export const toDate = (date: number) => `${new Date(date).toLocaleDateString()} ${new Date(date).toLocaleTimeString()}`
34+
35+
export const reducer = (state: State, action: Action): State => {
36+
switch (action.type) {
37+
case 'SELECT_EVENT':
38+
return {
39+
...state,
40+
event: action.event,
41+
}
42+
case 'SET_EMAIL':
43+
return {
44+
...state,
45+
isSubmitting: true,
46+
email: action.email
47+
}
48+
case 'HANDLE_API_RESULTS':
49+
console.log({action})
50+
return {
51+
...state,
52+
isSubmitting: false,
53+
hasSucceeded: action.results.success || false,
54+
error: action.results.error || null
55+
}
56+
default:
57+
return state
58+
}
59+
}

0 commit comments

Comments
 (0)