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'
18import { type Route } from './+types/journal-viewer.tsx'
29
310export async function loader ( { context } : Route . LoaderArgs ) {
@@ -7,6 +14,13 @@ export async function loader({ context }: Route.LoaderArgs) {
714
815export 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+ }
0 commit comments