[FA-18] Frontend bootstrapped
This commit is contained in:
17
fictionarchive-web/src/pages/NotFoundPage.tsx
Normal file
17
fictionarchive-web/src/pages/NotFoundPage.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
|
||||
|
||||
export function NotFoundPage() {
|
||||
return (
|
||||
<Card className="shadow-md shadow-primary/10">
|
||||
<CardHeader>
|
||||
<CardTitle>Page not found</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The page you are looking for does not exist. Head back to the novels
|
||||
list to continue.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
fictionarchive-web/src/pages/NovelDetailPage.tsx
Normal file
17
fictionarchive-web/src/pages/NovelDetailPage.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
|
||||
|
||||
export function NovelDetailPage() {
|
||||
return (
|
||||
<Card className="shadow-md shadow-primary/10">
|
||||
<CardHeader>
|
||||
<CardTitle>Novel details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Detail view coming soon. Select a novel to explore chapters and
|
||||
metadata once implemented.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
114
fictionarchive-web/src/pages/NovelsPage.tsx
Normal file
114
fictionarchive-web/src/pages/NovelsPage.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { useNovelsQuery } from '../__generated__/graphql'
|
||||
import { NovelCard } from '../components/NovelCard'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
|
||||
|
||||
const PAGE_SIZE = 12
|
||||
|
||||
export function NovelsPage() {
|
||||
const [after, setAfter] = useState<string | null>(null)
|
||||
const { data, loading, error, fetchMore } = useNovelsQuery({
|
||||
variables: { first: PAGE_SIZE, after },
|
||||
notifyOnNetworkStatusChange: true,
|
||||
})
|
||||
|
||||
const edges = data?.novels?.edges ?? []
|
||||
const pageInfo = data?.novels?.pageInfo
|
||||
const hasNextPage = pageInfo?.hasNextPage ?? false
|
||||
const endCursor = pageInfo?.endCursor ?? null
|
||||
|
||||
const novels = useMemo(
|
||||
() => edges.map((edge) => edge?.node).filter(Boolean),
|
||||
[edges]
|
||||
)
|
||||
|
||||
async function handleLoadMore() {
|
||||
if (!hasNextPage || !endCursor) return
|
||||
const result = await fetchMore({
|
||||
variables: { after: endCursor, first: PAGE_SIZE },
|
||||
updateQuery: (prev, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult?.novels?.edges) return prev
|
||||
const mergedEdges = [
|
||||
...(prev.novels?.edges ?? []),
|
||||
...fetchMoreResult.novels.edges,
|
||||
]
|
||||
return {
|
||||
...prev,
|
||||
novels: {
|
||||
...fetchMoreResult.novels,
|
||||
edges: mergedEdges,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
setAfter(result.data?.novels?.pageInfo?.endCursor ?? null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card className="shadow-md shadow-primary/10">
|
||||
<CardHeader>
|
||||
<CardTitle>Latest Novels</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Novels that have recently been updated.
|
||||
</p>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{loading && !data && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<p className="py-4 text-sm text-muted-foreground">
|
||||
Loading novels...
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Card className="border-destructive/40 bg-destructive/5">
|
||||
<CardContent>
|
||||
<p className="py-4 text-sm text-destructive">
|
||||
Could not load novels: {error.message}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!loading && novels.length === 0 && !error && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<p className="py-4 text-sm text-muted-foreground">
|
||||
No novels found yet. Try adding content to the gateway.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{novels.length > 0 && (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{novels.map(
|
||||
(novel) =>
|
||||
novel && <NovelCard key={novel.id.toString()} novel={novel} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
onClick={handleLoadMore}
|
||||
variant="outline"
|
||||
disabled={loading}
|
||||
className="min-w-[160px]"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Load more'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user