132 lines
3.6 KiB
TypeScript
132 lines
3.6 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { Article } from '@/types'
|
|
import { useNavigate } from '@tanstack/react-router'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { fetchArticle } from '@/api/articles'
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
export const Route = createFileRoute('/notebook')({
|
|
component: NotebookPage,
|
|
validateSearch: (search: Record<string, unknown>): { articleId?: string; article?: Article } => {
|
|
return {
|
|
articleId: search.articleId as string,
|
|
article: search.article as Article
|
|
}
|
|
},
|
|
})
|
|
|
|
function NotebookPage() {
|
|
const { articleId, article: preloadedArticle } = Route.useSearch()
|
|
const navigate = useNavigate()
|
|
|
|
const { data: fetchedArticle, isLoading, isError } = useQuery({
|
|
queryKey: ['article', articleId],
|
|
queryFn: () => {
|
|
if (!articleId) throw new Error('No article ID provided');
|
|
return fetchArticle(articleId);
|
|
},
|
|
enabled: !!articleId && !preloadedArticle,
|
|
})
|
|
|
|
const article = preloadedArticle || fetchedArticle
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-[50vh] space-y-4">
|
|
<h1 className="text-2xl font-semibold">Failed to load article</h1>
|
|
<Button onClick={() => navigate({ to: '/articles' })}>
|
|
Back to Articles
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!article) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-[50vh] space-y-4">
|
|
<h1 className="text-2xl font-semibold">No Article Selected</h1>
|
|
<Button onClick={() => navigate({ to: '/articles' })}>
|
|
Browse Articles
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const cells = [
|
|
{
|
|
cell_type: 'markdown',
|
|
metadata: {
|
|
language: 'markdown'
|
|
},
|
|
source: [
|
|
`# ${article.title}`,
|
|
'',
|
|
`Source: ${article.source}`,
|
|
`Published: ${new Date(article.publishedAt).toLocaleString()}`,
|
|
article.authors.length > 0 ? `Authors: ${article.authors.join(', ')}` : '',
|
|
article.categories.length > 0 ? `Categories: ${article.categories.join(', ')}` : '',
|
|
'',
|
|
'---',
|
|
''
|
|
]
|
|
},
|
|
{
|
|
cell_type: 'markdown',
|
|
metadata: {
|
|
language: 'markdown'
|
|
},
|
|
source: [article.content.split('\n').map(line => line.trim()).join('\n\n')]
|
|
},
|
|
{
|
|
cell_type: 'markdown',
|
|
metadata: {
|
|
language: 'markdown'
|
|
},
|
|
source: [
|
|
'',
|
|
'---',
|
|
'',
|
|
'## Additional Information',
|
|
'',
|
|
`Original Article: [${article.url}](${article.url})`
|
|
]
|
|
}
|
|
]
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 max-w-4xl">
|
|
<div className="mb-6 flex justify-between items-center">
|
|
<h1 className="text-3xl font-bold">Article Notebook</h1>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate({ to: '/articles' })}
|
|
>
|
|
Back to Articles
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-8">
|
|
{cells.map((cell, index) => (
|
|
<div
|
|
key={index}
|
|
className="prose prose-gray text-white dark:prose-invert max-w-none border rounded-lg p-8 bg-card"
|
|
>
|
|
{cell.source.map((line, i) => (
|
|
<div key={i} className="whitespace-pre-wrap">
|
|
{line}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |