1+ import { 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 ) {
@@ -19,6 +26,7 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
1926 You have { entries . length } journal{ ' ' }
2027 { entries . length === 1 ? 'entry' : 'entries' }
2128 </ p >
29+ < XPostLink entryCount = { entries . length } />
2230 </ div >
2331
2432 { entries . length === 0 ? (
@@ -57,6 +65,12 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
5765 🏷️ { entry . tagCount } tag{ entry . tagCount !== 1 ? 's' : '' }
5866 </ span >
5967 </ div >
68+
69+ < div className = "mt-4 flex gap-2" >
70+ < ViewEntryButton entry = { entry } />
71+ < SummarizeEntryButton entry = { entry } />
72+ < DeleteEntryButton entry = { entry } />
73+ </ div >
6074 </ div >
6175 </ div >
6276 </ div >
@@ -67,3 +81,229 @@ export default function JournalViewer({ loaderData }: Route.ComponentProps) {
6781 </ div >
6882 )
6983}
84+
85+ function XPostLink ( { entryCount } : { entryCount : number } ) {
86+ return (
87+ < ErrorBoundary FallbackComponent = { XPostLinkError } >
88+ < XPostLinkImpl entryCount = { entryCount } />
89+ </ ErrorBoundary >
90+ )
91+ }
92+
93+ function XPostLinkError ( { error, resetErrorBoundary } : FallbackProps ) {
94+ return (
95+ < div className = "bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3" >
96+ < p className = "text-sm font-medium" > Failed to post on X</ p >
97+ < p className = "text-destructive/80 text-xs" > { error . message } </ p >
98+ < button
99+ onClick = { resetErrorBoundary }
100+ className = "text-destructive mt-2 cursor-pointer text-xs hover:underline"
101+ >
102+ Try again
103+ </ button >
104+ </ div >
105+ )
106+ }
107+
108+ function XPostLinkImpl ( { entryCount } : { entryCount : number } ) {
109+ const [ isPending , startTransition ] = useTransition ( )
110+ const { showBoundary } = useErrorBoundary ( )
111+ const handlePostOnX = ( ) => {
112+ startTransition ( async ( ) => {
113+ try {
114+ const text = `I have ${ entryCount } journal ${ entryCount === 1 ? 'entry' : 'entries' } in my EpicMe journal! 📝✨`
115+ const url = new URL ( 'https://x.com/intent/post' )
116+ url . searchParams . set ( 'text' , text )
117+
118+ throw new Error ( `Links not yet supported` )
119+ } catch ( err ) {
120+ showBoundary ( err )
121+ }
122+ } )
123+ }
124+
125+ return (
126+ < button
127+ onClick = { handlePostOnX }
128+ disabled = { isPending }
129+ 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"
130+ >
131+ < svg className = "h-5 w-5" fill = "currentColor" viewBox = "0 0 24 24" >
132+ < 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" />
133+ </ svg >
134+ { isPending ? 'Posting...' : 'Post' }
135+ </ button >
136+ )
137+ }
138+
139+ function DeleteEntryButton ( {
140+ entry,
141+ } : {
142+ entry : { id : number ; title : string }
143+ } ) {
144+ return (
145+ < ErrorBoundary FallbackComponent = { DeleteEntryError } >
146+ < DeleteEntryButtonImpl entry = { entry } />
147+ </ ErrorBoundary >
148+ )
149+ }
150+
151+ function DeleteEntryError ( { error, resetErrorBoundary } : FallbackProps ) {
152+ return (
153+ < div className = "bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3" >
154+ < p className = "text-sm font-medium" > Failed to delete entry</ p >
155+ < p className = "text-destructive/80 text-xs" > { error . message } </ p >
156+ < button
157+ onClick = { resetErrorBoundary }
158+ className = "text-destructive mt-2 cursor-pointer text-xs hover:underline"
159+ >
160+ Try again
161+ </ button >
162+ </ div >
163+ )
164+ }
165+
166+ function DeleteEntryButtonImpl ( {
167+ entry,
168+ } : {
169+ entry : { id : number ; title : string }
170+ } ) {
171+ const [ isPending , startTransition ] = useTransition ( )
172+ const { doubleCheck, getButtonProps } = useDoubleCheck ( )
173+ const { showBoundary } = useErrorBoundary ( )
174+
175+ const handleDelete = ( ) => {
176+ startTransition ( async ( ) => {
177+ try {
178+ throw new Error ( 'Calling tools is not yet supported' )
179+ } catch ( err ) {
180+ showBoundary ( err )
181+ }
182+ } )
183+ }
184+
185+ return (
186+ < button
187+ { ...getButtonProps ( {
188+ onClick : doubleCheck ? handleDelete : undefined ,
189+ disabled : isPending ,
190+ className : `text-sm font-medium px-3 py-1.5 rounded-md border transition-colors ${
191+ doubleCheck
192+ ? 'bg-destructive text-destructive-foreground border-destructive hover:bg-destructive/90'
193+ : 'text-destructive border-destructive/20 hover:bg-destructive/10 hover:border-destructive/40'
194+ } ${ isPending ? 'opacity-50 cursor-not-allowed' : '' } `,
195+ } ) }
196+ >
197+ { isPending ? 'Deleting...' : doubleCheck ? `Confirm?` : 'Delete' }
198+ </ button >
199+ )
200+ }
201+
202+ function ViewEntryButton ( { entry } : { entry : { id : number ; title : string } } ) {
203+ return (
204+ < ErrorBoundary FallbackComponent = { ViewEntryError } >
205+ < ViewEntryButtonImpl entry = { entry } />
206+ </ ErrorBoundary >
207+ )
208+ }
209+
210+ function ViewEntryError ( { error, resetErrorBoundary } : FallbackProps ) {
211+ return (
212+ < div className = "bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3" >
213+ < p className = "text-sm font-medium" > Failed to view entry</ p >
214+ < p className = "text-destructive/80 text-xs" > { error . message } </ p >
215+ < button
216+ onClick = { resetErrorBoundary }
217+ className = "text-destructive mt-2 cursor-pointer text-xs hover:underline"
218+ >
219+ Try again
220+ </ button >
221+ </ div >
222+ )
223+ }
224+
225+ function ViewEntryButtonImpl ( {
226+ entry,
227+ } : {
228+ entry : { id : number ; title : string }
229+ } ) {
230+ const [ isPending , startTransition ] = useTransition ( )
231+ const { showBoundary } = useErrorBoundary ( )
232+
233+ const handleViewEntry = ( ) => {
234+ startTransition ( async ( ) => {
235+ try {
236+ throw new Error ( 'Calling tools is not yet supported' )
237+ } catch ( err ) {
238+ showBoundary ( err )
239+ }
240+ } )
241+ }
242+
243+ return (
244+ < button
245+ onClick = { handleViewEntry }
246+ disabled = { isPending }
247+ className = "text-primary text-sm font-medium hover:underline disabled:cursor-not-allowed disabled:opacity-50"
248+ >
249+ { isPending ? 'Loading...' : 'View Details' }
250+ </ button >
251+ )
252+ }
253+
254+ function SummarizeEntryButton ( {
255+ entry,
256+ } : {
257+ entry : { id : number ; title : string }
258+ } ) {
259+ return (
260+ < ErrorBoundary FallbackComponent = { SummarizeEntryError } >
261+ < SummarizeEntryButtonImpl entry = { entry } />
262+ </ ErrorBoundary >
263+ )
264+ }
265+
266+ function SummarizeEntryError ( { error, resetErrorBoundary } : FallbackProps ) {
267+ return (
268+ < div className = "bg-destructive/10 border-destructive/20 text-destructive rounded-lg border p-3" >
269+ < p className = "text-sm font-medium" > Failed to summarize entry</ p >
270+ < p className = "text-destructive/80 text-xs" > { error . message } </ p >
271+ < button
272+ onClick = { resetErrorBoundary }
273+ className = "text-destructive mt-2 cursor-pointer text-xs hover:underline"
274+ >
275+ Try again
276+ </ button >
277+ </ div >
278+ )
279+ }
280+
281+ function SummarizeEntryButtonImpl ( {
282+ entry,
283+ } : {
284+ entry : { id : number ; title : string }
285+ } ) {
286+ const [ isPending , startTransition ] = useTransition ( )
287+ const { showBoundary } = useErrorBoundary ( )
288+
289+ const handleSummarize = ( ) => {
290+ startTransition ( async ( ) => {
291+ try {
292+ // Get the full entry content first
293+ throw new Error ( 'Sending prompts is not yet supported' )
294+ } catch ( err ) {
295+ showBoundary ( err )
296+ }
297+ } )
298+ }
299+
300+ return (
301+ < button
302+ onClick = { handleSummarize }
303+ disabled = { isPending }
304+ className = "text-primary text-sm font-medium hover:underline disabled:cursor-not-allowed disabled:opacity-50"
305+ >
306+ { isPending ? 'Summarizing...' : 'Summarize' }
307+ </ button >
308+ )
309+ }
0 commit comments