[FA-18] Frontend bootstrapped

This commit is contained in:
gamer147
2025-11-24 13:25:29 -05:00
parent 16ed16ff62
commit e8596b67c4
42 changed files with 9747 additions and 2 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}