Skip to content

Commit 6c83b03

Browse files
committed
feat: integrate react-error-boundary for error handling and add post functionality in journal viewer
1 parent 246aae2 commit 6c83b03

File tree

5 files changed

+190
-369
lines changed

5 files changed

+190
-369
lines changed

exercises/99.final/99.solution/app/routes/ui/journal-viewer.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { useMcpUiInit } from '#app/utils/mcp.ts'
1+
import { useTransition } from 'react'
2+
import {
3+
ErrorBoundary,
4+
useErrorBoundary,
5+
type FallbackProps,
6+
} from 'react-error-boundary'
7+
import { useMcpUiInit, navigateToLink } from '#app/utils/mcp.ts'
28
import { type Route } from './+types/journal-viewer.tsx'
39

410
export async function loader({ context }: Route.LoaderArgs) {
@@ -18,10 +24,11 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
1824
<h1 className="text-foreground mb-2 text-3xl font-bold">
1925
Your Journal
2026
</h1>
21-
<p className="text-muted-foreground">
27+
<p className="text-muted-foreground mb-4">
2228
You have {entries.length} journal{' '}
2329
{entries.length === 1 ? 'entry' : 'entries'}
2430
</p>
31+
<XPostLink entryCount={entries.length} />
2532
</div>
2633

2734
{entries.length === 0 ? (
@@ -76,3 +83,56 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
7683
</div>
7784
)
7885
}
86+
87+
function XPostLink({ entryCount }: { entryCount: number }) {
88+
return (
89+
<ErrorBoundary FallbackComponent={XPostLinkError}>
90+
<XPostLinkImpl entryCount={entryCount} />
91+
</ErrorBoundary>
92+
)
93+
}
94+
95+
function XPostLinkError({ error, resetErrorBoundary }: FallbackProps) {
96+
return (
97+
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3">
98+
<p className="text-sm font-medium">Failed to post on X</p>
99+
<p className="text-xs text-destructive/80">{error.message}</p>
100+
<button
101+
onClick={resetErrorBoundary}
102+
className="mt-2 text-xs text-destructive hover:underline cursor-pointer"
103+
>
104+
Try again
105+
</button>
106+
</div>
107+
)
108+
}
109+
110+
function XPostLinkImpl({ entryCount }: { entryCount: number }) {
111+
const [isPending, startTransition] = useTransition()
112+
const { showBoundary } = useErrorBoundary()
113+
const handlePostOnX = () => {
114+
startTransition(async () => {
115+
try {
116+
const text = `I have ${entryCount} journal ${entryCount === 1 ? 'entry' : 'entries'} in my personal journal app! 📝✨`
117+
const url = `https://x.com/intent/post?text=${encodeURIComponent(text)}`
118+
119+
await navigateToLink(url)
120+
} catch (err) {
121+
showBoundary(err)
122+
}
123+
})
124+
}
125+
126+
return (
127+
<button
128+
onClick={handlePostOnX}
129+
disabled={isPending}
130+
className="bg-black text-white px-4 py-2 rounded-lg hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors flex items-center gap-2 cursor-pointer"
131+
>
132+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
133+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
134+
</svg>
135+
{isPending ? 'Posting...' : 'Post'}
136+
</button>
137+
)
138+
}

exercises/99.final/99.solution/app/utils/mcp.ts

Lines changed: 20 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ export function useFormSubmissionCapability() {
6262
return canUseOnSubmit
6363
}
6464

65-
export function callTool(
66-
toolName: string,
67-
params: any,
65+
function createMcpMessageHandler<T>(
66+
type: string,
67+
payload: Record<string, unknown>,
6868
signal?: AbortSignal,
69-
): Promise<any> {
69+
): Promise<T> {
7070
const messageId = crypto.randomUUID()
7171

7272
return new Promise((resolve, reject) => {
@@ -75,22 +75,10 @@ export function callTool(
7575
return
7676
}
7777

78-
// Send tool call with messageId
79-
window.parent.postMessage(
80-
{
81-
type: 'tool',
82-
messageId,
83-
payload: {
84-
toolName,
85-
params,
86-
},
87-
},
88-
'*',
89-
)
78+
window.parent.postMessage({ type, messageId, payload }, '*')
9079

9180
function handleMessage(event: MessageEvent) {
9281
if (event.data.type === 'ui-message-response') {
93-
console.log(event)
9482
const {
9583
messageId: responseMessageId,
9684
payload: { response, error },
@@ -111,48 +99,24 @@ export function callTool(
11199
})
112100
}
113101

102+
export function callTool(
103+
toolName: string,
104+
params: any,
105+
signal?: AbortSignal,
106+
): Promise<any> {
107+
return createMcpMessageHandler('tool', { toolName, params }, signal)
108+
}
109+
114110
export function sendPrompt(
115111
prompt: string,
116112
signal?: AbortSignal,
117113
): Promise<void> {
118-
const messageId = crypto.randomUUID()
119-
120-
return new Promise((resolve, reject) => {
121-
if (signal?.aborted) {
122-
reject(new Error('Operation aborted'))
123-
return
124-
}
125-
126-
// Send prompt with messageId
127-
window.parent.postMessage(
128-
{
129-
type: 'prompt',
130-
messageId,
131-
payload: {
132-
prompt,
133-
},
134-
},
135-
'*',
136-
)
137-
138-
function handleMessage(event: MessageEvent) {
139-
if (event.data.type === 'ui-message-response') {
140-
const {
141-
messageId: responseMessageId,
142-
payload: { response, error },
143-
} = event.data
144-
if (responseMessageId === messageId) {
145-
window.removeEventListener('message', handleMessage)
146-
147-
if (error) {
148-
reject(new Error(error))
149-
} else {
150-
resolve(response)
151-
}
152-
}
153-
}
154-
}
114+
return createMcpMessageHandler('prompt', { prompt }, signal)
115+
}
155116

156-
window.addEventListener('message', handleMessage, { signal })
157-
})
117+
export function navigateToLink(
118+
url: string,
119+
signal?: AbortSignal,
120+
): Promise<void> {
121+
return createMcpMessageHandler('link', { url }, signal)
158122
}

exercises/99.final/99.solution/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"agents": "^0.0.111",
2525
"isbot": "^5.1.30",
2626
"react": "^18.3.1",
27+
"react-error-boundary": "^6.0.0",
2728
"react-dom": "^18.3.1",
2829
"react-router": "^7.8.2",
2930
"zod": "^3.25.67"

0 commit comments

Comments
 (0)