Skip to content

Commit f11bd94

Browse files
committed
great progress
1 parent d1e488c commit f11bd94

File tree

152 files changed

+75994
-320
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

152 files changed

+75994
-320
lines changed

exercises/03.complex-ui/01.problem.iframe/app/routes/ui/journal-viewer.tsx

Lines changed: 299 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { useState, useTransition } from 'react'
2+
import {
3+
ErrorBoundary,
4+
useErrorBoundary,
5+
type FallbackProps,
6+
} from 'react-error-boundary'
7+
import { useDoubleCheck } from '#app/utils/misc.ts'
18
import { type Route } from './+types/journal-viewer.tsx'
29

310
export async function loader({ context }: Route.LoaderArgs) {
@@ -7,6 +14,13 @@ export async function loader({ context }: Route.LoaderArgs) {
714

815
export default function JournalViewer({ loaderData }: Route.ComponentProps) {
916
const { entries } = loaderData
17+
const [deletedEntryIds, setDeletedEntryIds] = useState<Set<number>>(
18+
() => new Set([]),
19+
)
20+
21+
const handleEntryDeleted = (entryId: number) => {
22+
setDeletedEntryIds((prev) => new Set([...prev, entryId]))
23+
}
1024

1125
return (
1226
<div className="bg-background max-h-[800px] overflow-y-auto p-4">
@@ -19,6 +33,7 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
1933
You have {entries.length} journal{' '}
2034
{entries.length === 1 ? 'entry' : 'entries'}
2135
</p>
36+
<XPostLink entryCount={entries.length} />
2237
</div>
2338

2439
{entries.length === 0 ? (
@@ -39,31 +54,297 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
3954
</div>
4055
) : (
4156
<div className="space-y-4">
42-
{entries.map((entry) => (
43-
<div
44-
key={entry.id}
45-
className="bg-card rounded-xl border p-6 shadow-sm transition-all hover:shadow-md"
46-
>
47-
<div className="flex items-start justify-between">
48-
<div className="flex-1">
49-
<div className="mb-3 flex items-center gap-3">
50-
<h3 className="text-foreground text-lg font-semibold">
51-
{entry.title}
52-
</h3>
53-
</div>
57+
{entries.map((entry) => {
58+
const isDeleted = deletedEntryIds.has(entry.id)
59+
return (
60+
<div
61+
key={entry.id}
62+
className={`bg-card rounded-xl border p-6 shadow-sm transition-all ${
63+
isDeleted ? 'bg-muted/50 opacity-50' : 'hover:shadow-md'
64+
}`}
65+
>
66+
<div className="flex items-start justify-between">
67+
<div className="flex-1">
68+
<div className="mb-3 flex items-center gap-3">
69+
<h3 className="text-foreground text-lg font-semibold">
70+
{entry.title}
71+
</h3>
72+
{isDeleted ? (
73+
<div className="text-accent-foreground bg-accent flex items-center gap-2 rounded-md px-2 py-1 text-sm">
74+
<svg
75+
className="h-3 w-3"
76+
fill="none"
77+
stroke="currentColor"
78+
viewBox="0 0 24 24"
79+
xmlns="http://www.w3.org/2000/svg"
80+
>
81+
<path
82+
strokeLinecap="round"
83+
strokeLinejoin="round"
84+
strokeWidth={2}
85+
d="M5 13l4 4L19 7"
86+
/>
87+
</svg>
88+
Deleted
89+
</div>
90+
) : null}
91+
</div>
92+
93+
<div className="mb-3 flex flex-wrap gap-2">
94+
<span className="bg-accent text-accent-foreground rounded-full px-3 py-1 text-sm">
95+
🏷️ {entry.tagCount} tag
96+
{entry.tagCount !== 1 ? 's' : ''}
97+
</span>
98+
</div>
5499

55-
<div className="mb-3 flex flex-wrap gap-2">
56-
<span className="bg-accent text-accent-foreground rounded-full px-3 py-1 text-sm">
57-
🏷️ {entry.tagCount} tag{entry.tagCount !== 1 ? 's' : ''}
58-
</span>
100+
{!isDeleted ? (
101+
<div className="mt-4 flex gap-2">
102+
<ViewEntryButton entry={entry} />
103+
<SummarizeEntryButton entry={entry} />
104+
<DeleteEntryButton
105+
entry={entry}
106+
onDeleted={() => handleEntryDeleted(entry.id)}
107+
/>
108+
</div>
109+
) : null}
59110
</div>
60111
</div>
61112
</div>
62-
</div>
63-
))}
113+
)
114+
})}
64115
</div>
65116
)}
66117
</div>
67118
</div>
68119
)
69120
}
121+
122+
function XPostLink({ entryCount }: { entryCount: number }) {
123+
return (
124+
<ErrorBoundary FallbackComponent={XPostLinkError}>
125+
<XPostLinkImpl entryCount={entryCount} />
126+
</ErrorBoundary>
127+
)
128+
}
129+
130+
function XPostLinkError({ error, resetErrorBoundary }: FallbackProps) {
131+
return (
132+
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3">
133+
<p className="text-sm font-medium">Failed to post on X</p>
134+
<p className="text-destructive/80 text-xs">{error.message}</p>
135+
<button
136+
onClick={resetErrorBoundary}
137+
className="text-destructive mt-2 cursor-pointer text-xs hover:underline"
138+
>
139+
Try again
140+
</button>
141+
</div>
142+
)
143+
}
144+
145+
function XPostLinkImpl({ entryCount }: { entryCount: number }) {
146+
const [isPending, startTransition] = useTransition()
147+
const { showBoundary } = useErrorBoundary()
148+
const handlePostOnX = () => {
149+
startTransition(async () => {
150+
try {
151+
const text = `I have ${entryCount} journal ${entryCount === 1 ? 'entry' : 'entries'} in my EpicMe journal! 📝✨`
152+
const url = new URL('https://x.com/intent/post')
153+
url.searchParams.set('text', text)
154+
155+
throw new Error(`Links not yet supported`)
156+
} catch (err) {
157+
showBoundary(err)
158+
}
159+
})
160+
}
161+
162+
return (
163+
<button
164+
onClick={handlePostOnX}
165+
disabled={isPending}
166+
className="flex cursor-pointer items-center gap-2 rounded-lg bg-black px-4 py-2 text-white transition-colors hover:bg-gray-800 disabled:cursor-not-allowed disabled:bg-gray-400"
167+
>
168+
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
169+
<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" />
170+
</svg>
171+
{isPending ? 'Posting...' : 'Post'}
172+
</button>
173+
)
174+
}
175+
176+
function DeleteEntryButton({
177+
entry,
178+
onDeleted,
179+
}: {
180+
entry: { id: number; title: string }
181+
onDeleted: () => void
182+
}) {
183+
return (
184+
<ErrorBoundary FallbackComponent={DeleteEntryError}>
185+
<DeleteEntryButtonImpl entry={entry} onDeleted={onDeleted} />
186+
</ErrorBoundary>
187+
)
188+
}
189+
190+
function DeleteEntryError({ error, resetErrorBoundary }: FallbackProps) {
191+
return (
192+
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3">
193+
<p className="text-sm font-medium">Failed to delete entry</p>
194+
<p className="text-destructive/80 text-xs">{error.message}</p>
195+
<button
196+
onClick={resetErrorBoundary}
197+
className="text-destructive mt-2 cursor-pointer text-xs hover:underline"
198+
>
199+
Try again
200+
</button>
201+
</div>
202+
)
203+
}
204+
205+
function DeleteEntryButtonImpl({
206+
entry,
207+
onDeleted,
208+
}: {
209+
entry: { id: number; title: string }
210+
onDeleted: () => void
211+
}) {
212+
const [isPending, startTransition] = useTransition()
213+
const { doubleCheck, getButtonProps } = useDoubleCheck()
214+
const { showBoundary } = useErrorBoundary()
215+
216+
const handleDelete = () => {
217+
startTransition(async () => {
218+
try {
219+
throw new Error('Calling tools is not yet supported')
220+
} catch (err) {
221+
showBoundary(err)
222+
}
223+
})
224+
}
225+
226+
return (
227+
<button
228+
{...getButtonProps({
229+
onClick: doubleCheck ? handleDelete : undefined,
230+
disabled: isPending,
231+
className: `text-sm font-medium px-3 py-1.5 rounded-md border transition-colors ${
232+
doubleCheck
233+
? 'bg-destructive text-destructive-foreground border-destructive hover:bg-destructive/90'
234+
: 'text-destructive border-destructive/20 hover:bg-destructive/10 hover:border-destructive/40'
235+
} ${isPending ? 'opacity-50 cursor-not-allowed' : ''}`,
236+
})}
237+
>
238+
{isPending ? 'Deleting...' : doubleCheck ? `Confirm?` : 'Delete'}
239+
</button>
240+
)
241+
}
242+
243+
function ViewEntryButton({ entry }: { entry: { id: number; title: string } }) {
244+
return (
245+
<ErrorBoundary FallbackComponent={ViewEntryError}>
246+
<ViewEntryButtonImpl entry={entry} />
247+
</ErrorBoundary>
248+
)
249+
}
250+
251+
function ViewEntryError({ error, resetErrorBoundary }: FallbackProps) {
252+
return (
253+
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3">
254+
<p className="text-sm font-medium">Failed to view entry</p>
255+
<p className="text-destructive/80 text-xs">{error.message}</p>
256+
<button
257+
onClick={resetErrorBoundary}
258+
className="text-destructive mt-2 cursor-pointer text-xs hover:underline"
259+
>
260+
Try again
261+
</button>
262+
</div>
263+
)
264+
}
265+
266+
function ViewEntryButtonImpl({
267+
entry,
268+
}: {
269+
entry: { id: number; title: string }
270+
}) {
271+
const [isPending, startTransition] = useTransition()
272+
const { showBoundary } = useErrorBoundary()
273+
274+
const handleViewEntry = () => {
275+
startTransition(async () => {
276+
try {
277+
throw new Error('Calling tools is not yet supported')
278+
} catch (err) {
279+
showBoundary(err)
280+
}
281+
})
282+
}
283+
284+
return (
285+
<button
286+
onClick={handleViewEntry}
287+
disabled={isPending}
288+
className="text-primary text-sm font-medium hover:underline disabled:cursor-not-allowed disabled:opacity-50"
289+
>
290+
{isPending ? 'Loading...' : 'View Details'}
291+
</button>
292+
)
293+
}
294+
295+
function SummarizeEntryButton({
296+
entry,
297+
}: {
298+
entry: { id: number; title: string }
299+
}) {
300+
return (
301+
<ErrorBoundary FallbackComponent={SummarizeEntryError}>
302+
<SummarizeEntryButtonImpl entry={entry} />
303+
</ErrorBoundary>
304+
)
305+
}
306+
307+
function SummarizeEntryError({ error, resetErrorBoundary }: FallbackProps) {
308+
return (
309+
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3">
310+
<p className="text-sm font-medium">Failed to summarize entry</p>
311+
<p className="text-destructive/80 text-xs">{error.message}</p>
312+
<button
313+
onClick={resetErrorBoundary}
314+
className="text-destructive mt-2 cursor-pointer text-xs hover:underline"
315+
>
316+
Try again
317+
</button>
318+
</div>
319+
)
320+
}
321+
322+
function SummarizeEntryButtonImpl({
323+
entry,
324+
}: {
325+
entry: { id: number; title: string }
326+
}) {
327+
const [isPending, startTransition] = useTransition()
328+
const { showBoundary } = useErrorBoundary()
329+
330+
const handleSummarize = () => {
331+
startTransition(async () => {
332+
try {
333+
// Get the full entry content first
334+
throw new Error('Sending prompts is not yet supported')
335+
} catch (err) {
336+
showBoundary(err)
337+
}
338+
})
339+
}
340+
341+
return (
342+
<button
343+
onClick={handleSummarize}
344+
disabled={isPending}
345+
className="text-primary text-sm font-medium hover:underline disabled:cursor-not-allowed disabled:opacity-50"
346+
>
347+
{isPending ? 'Summarizing...' : 'Summarize'}
348+
</button>
349+
)
350+
}

exercises/03.complex-ui/01.problem.iframe/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"agents": "^0.0.113",
2323
"isbot": "^5.1.30",
2424
"react": "^19.1.1",
25-
"react-error-boundary": "^6.0.0",
2625
"react-dom": "^19.1.1",
26+
"react-error-boundary": "^6.0.0",
2727
"react-router": "^7.8.2",
2828
"zod": "^3.25.67"
2929
},

0 commit comments

Comments
 (0)