check point, notebook basic structure defined
This commit is contained in:
1 parent
98c9328a3d
commit
5a01897a44
11 files changed
+303
-43
No files matched your search
@@ -33,4 +33,12 @@ export async function getArticleCount(): Promise<number> {
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.count;
|
||||
}
|
||||
|
||||
export async function fetchArticle(id: string): Promise<Article> {
|
||||
const response = await fetch(`/api/articles/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch article');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as RegisterImport } from './routes/register'
|
||||
import { Route as NotebookImport } from './routes/notebook'
|
||||
import { Route as NotFoundImport } from './routes/notFound'
|
||||
import { Route as LoginImport } from './routes/login'
|
||||
import { Route as DashboardImport } from './routes/dashboard'
|
||||
@@ -29,6 +30,12 @@ const RegisterRoute = RegisterImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const NotebookRoute = NotebookImport.update({
|
||||
id: '/notebook',
|
||||
path: '/notebook',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const NotFoundRoute = NotFoundImport.update({
|
||||
id: '/notFound',
|
||||
path: '/notFound',
|
||||
@@ -123,6 +130,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof NotFoundImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/notebook': {
|
||||
id: '/notebook'
|
||||
path: '/notebook'
|
||||
fullPath: '/notebook'
|
||||
preLoaderRoute: typeof NotebookImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/register': {
|
||||
id: '/register'
|
||||
path: '/register'
|
||||
@@ -180,6 +194,7 @@ export interface FileRoutesByFullPath {
|
||||
'/dashboard': typeof DashboardRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/notFound': typeof NotFoundRoute
|
||||
'/notebook': typeof NotebookRoute
|
||||
'/register': typeof RegisterRoute
|
||||
'/collections/[name]': typeof CollectionsnameRoute
|
||||
'/dashboard/': typeof DashboardIndexRoute
|
||||
@@ -191,6 +206,7 @@ export interface FileRoutesByTo {
|
||||
'/collections': typeof CollectionsRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/notFound': typeof NotFoundRoute
|
||||
'/notebook': typeof NotebookRoute
|
||||
'/register': typeof RegisterRoute
|
||||
'/collections/[name]': typeof CollectionsnameRoute
|
||||
'/dashboard': typeof DashboardIndexRoute
|
||||
@@ -204,6 +220,7 @@ export interface FileRoutesById {
|
||||
'/dashboard': typeof DashboardRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/notFound': typeof NotFoundRoute
|
||||
'/notebook': typeof NotebookRoute
|
||||
'/register': typeof RegisterRoute
|
||||
'/collections/[name]': typeof CollectionsnameRoute
|
||||
'/dashboard/': typeof DashboardIndexRoute
|
||||
@@ -218,6 +235,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard'
|
||||
| '/login'
|
||||
| '/notFound'
|
||||
| '/notebook'
|
||||
| '/register'
|
||||
| '/collections/[name]'
|
||||
| '/dashboard/'
|
||||
@@ -228,6 +246,7 @@ export interface FileRouteTypes {
|
||||
| '/collections'
|
||||
| '/login'
|
||||
| '/notFound'
|
||||
| '/notebook'
|
||||
| '/register'
|
||||
| '/collections/[name]'
|
||||
| '/dashboard'
|
||||
@@ -239,6 +258,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard'
|
||||
| '/login'
|
||||
| '/notFound'
|
||||
| '/notebook'
|
||||
| '/register'
|
||||
| '/collections/[name]'
|
||||
| '/dashboard/'
|
||||
@@ -252,6 +272,7 @@ export interface RootRouteChildren {
|
||||
DashboardRoute: typeof DashboardRouteWithChildren
|
||||
LoginRoute: typeof LoginRoute
|
||||
NotFoundRoute: typeof NotFoundRoute
|
||||
NotebookRoute: typeof NotebookRoute
|
||||
RegisterRoute: typeof RegisterRoute
|
||||
}
|
||||
|
||||
@@ -262,6 +283,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
DashboardRoute: DashboardRouteWithChildren,
|
||||
LoginRoute: LoginRoute,
|
||||
NotFoundRoute: NotFoundRoute,
|
||||
NotebookRoute: NotebookRoute,
|
||||
RegisterRoute: RegisterRoute,
|
||||
}
|
||||
|
||||
@@ -281,6 +303,7 @@ export const routeTree = rootRoute
|
||||
"/dashboard",
|
||||
"/login",
|
||||
"/notFound",
|
||||
"/notebook",
|
||||
"/register"
|
||||
]
|
||||
},
|
||||
@@ -308,6 +331,9 @@ export const routeTree = rootRoute
|
||||
"/notFound": {
|
||||
"filePath": "notFound.tsx"
|
||||
},
|
||||
"/notebook": {
|
||||
"filePath": "notebook.tsx"
|
||||
},
|
||||
"/register": {
|
||||
"filePath": "register.tsx"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { fetchArticles } from '@/api/articles'
|
||||
import { fetchArticles, PaginatedArticles } from '@/api/articles'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { addArticleToCollection, createCollection, fetchCollections } from '@/api/collections';
|
||||
import { useUser } from '@/store/user';
|
||||
@@ -61,15 +61,13 @@ function ArticlesPage() {
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['articles'],
|
||||
queryFn: async ({ pageParam = undefined }) => {
|
||||
const response = await fetchArticles(pageParam);
|
||||
return response;
|
||||
} = useInfiniteQuery<{ pages: PaginatedArticles[] }>({
|
||||
queryKey: ['articles'] as const,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
return fetchArticles(pageParam as string | undefined);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
initialPageParam: undefined as string | undefined,
|
||||
staleTime: 1000 * 60 // Cache for 1 minute
|
||||
});
|
||||
|
||||
const user = useUser();
|
||||
@@ -145,20 +143,30 @@ function ArticlesPage() {
|
||||
<p className="text-muted-foreground text-sm mb-4 line-clamp-2">
|
||||
{article.content?.substring(0, 120)}...
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">{article.source}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(article.publishedAt).toLocaleDateString()}
|
||||
</span>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Source: {article.source} | Published: {new Date(article.publishedAt).toLocaleDateString()}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleAddToCollection(article, user?.id, navigate)}
|
||||
>
|
||||
Add to Collection
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => navigate({
|
||||
to: '/notebook',
|
||||
search: { articleId: article.id }
|
||||
})}
|
||||
>
|
||||
Open in Notebook
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-4 w-full"
|
||||
onClick={() => handleAddToCollection(article, user?.id, navigate)}
|
||||
>
|
||||
Add to Collection
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -70,5 +70,8 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/typography")
|
||||
],
|
||||
}
|
||||
Reference in new issue
Block a user