Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit 707560e

Browse files
committed
feat: add manga detail page
1 parent bc5b14c commit 707560e

File tree

9 files changed

+203
-21
lines changed

9 files changed

+203
-21
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"execa": "5.1.1",
5959
"express": "^4.18.1",
6060
"json-diff": "^0.9.0",
61+
"mantine-datatable": "^1.7.9",
6162
"meow": "^9.0.0",
6263
"moment": "^2.29.4",
6364
"next": "12.3.1",

prisma/schema.prisma

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ model Manga {
2626
source String
2727
library Library @relation(fields: [libraryId], references: [id], onDelete: Cascade)
2828
libraryId Int
29-
chapter Chapter[]
29+
chapters Chapter[]
3030
metadata Metadata @relation(fields: [metadataId], references: [id])
3131
metadataId Int @unique
3232
}

src/components/chaptersTable.tsx

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Box } from '@mantine/core';
2+
import { Prisma } from '@prisma/client';
3+
import { DataTable } from 'mantine-datatable';
4+
5+
import dayjs from 'dayjs';
6+
7+
import prettyBytes from 'pretty-bytes';
8+
import { useEffect, useState } from 'react';
9+
import { showNotification } from '@mantine/notifications';
10+
import { sanitizer } from '../utils/sanitize';
11+
12+
const mangaWithMetadataAndChaptersLibrary = Prisma.validator<Prisma.MangaArgs>()({
13+
include: { metadata: true, chapters: true, library: true },
14+
});
15+
16+
export type MangaWithMetadataAndChaptersLibrary = Prisma.MangaGetPayload<typeof mangaWithMetadataAndChaptersLibrary>;
17+
18+
const PAGE_SIZE = 100;
19+
20+
export function ChaptersTable({ manga }: { manga: MangaWithMetadataAndChaptersLibrary }) {
21+
const [page, setPage] = useState(1);
22+
const [records, setRecords] = useState(manga.chapters.slice(0, PAGE_SIZE));
23+
24+
useEffect(() => {
25+
const from = (page - 1) * PAGE_SIZE;
26+
const to = from + PAGE_SIZE;
27+
setRecords(manga.chapters.slice(from, to));
28+
}, [manga.chapters, page]);
29+
30+
return (
31+
<Box sx={{ height: 700, marginTop: 30 }}>
32+
<DataTable
33+
withBorder
34+
withColumnBorders
35+
striped
36+
highlightOnHover
37+
records={records}
38+
recordsPerPage={PAGE_SIZE}
39+
page={page}
40+
totalRecords={manga.chapters.length}
41+
onPageChange={(p) => setPage(p)}
42+
columns={[
43+
{ accessor: 'Download Date', render: ({ createdAt }) => dayjs(createdAt).fromNow() },
44+
{ accessor: 'Chapter', render: ({ index }) => `No #${index + 1}` },
45+
{
46+
accessor: 'File',
47+
render: ({ fileName }) => `${manga.library.path}/${sanitizer(manga.title)}/${fileName}`,
48+
},
49+
{ accessor: 'Size', render: ({ size }) => prettyBytes(size) },
50+
]}
51+
rowContextMenu={{
52+
items: () => [
53+
{
54+
key: 'download',
55+
title: 'Download Again',
56+
onClick: () => showNotification({ message: `Chapter queued for the download` }),
57+
},
58+
],
59+
}}
60+
/>
61+
</Box>
62+
);
63+
}

src/components/mangaDetail.tsx

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Badge, createStyles, Divider, Grid, Group, Image, Text, Title, Tooltip } from '@mantine/core';
2+
import { Prisma } from '@prisma/client';
3+
4+
const useStyles = createStyles((_theme) => ({
5+
root: {},
6+
placeHolder: {
7+
alignItems: 'start !important',
8+
justifyContent: 'flex-start !important',
9+
},
10+
}));
11+
12+
const mangaWithMetadataAndChapters = Prisma.validator<Prisma.MangaArgs>()({
13+
include: { metadata: true, chapters: true },
14+
});
15+
16+
export type MangaWithMetadataAndChapters = Prisma.MangaGetPayload<typeof mangaWithMetadataAndChapters>;
17+
18+
export function MangaDetail({ manga }: { manga: MangaWithMetadataAndChapters }) {
19+
const { classes } = useStyles();
20+
21+
return (
22+
<Grid className={classes.root}>
23+
<Grid.Col span="auto" style={{ maxWidth: 300 }}>
24+
<Image
25+
classNames={{
26+
placeholder: classes.placeHolder,
27+
}}
28+
sx={(theme) => ({
29+
width: 210,
30+
boxShadow: theme.shadows.xl,
31+
})}
32+
withPlaceholder
33+
placeholder={
34+
<Image
35+
sx={(theme) => ({
36+
width: 210,
37+
boxShadow: theme.shadows.xl,
38+
})}
39+
src="/cover-not-found.jpg"
40+
alt={manga.title}
41+
/>
42+
}
43+
src={manga.metadata.cover}
44+
/>
45+
</Grid.Col>
46+
<Grid.Col span="auto">
47+
<Divider mb="xs" labelPosition="left" label={<Title order={3}>{manga.title}</Title>} />
48+
<Group spacing="xs">
49+
{manga.metadata.synonyms.map((synonym) => (
50+
<Tooltip label={synonym} key={synonym}>
51+
<div style={{ maxWidth: 100 }}>
52+
<Badge color="blue" variant="filled" size="sm" fullWidth>
53+
{synonym}
54+
</Badge>
55+
</div>
56+
</Tooltip>
57+
))}
58+
</Group>
59+
<Divider variant="dashed" my="xs" label="Status" />
60+
<Badge color="cyan" variant="filled" size="sm">
61+
{manga.metadata.status}
62+
</Badge>
63+
64+
<Divider variant="dashed" my="xs" label="Chapters" />
65+
<Text>
66+
There are &nbsp;
67+
<Badge color="teal" variant="outline" size="lg">
68+
{manga.chapters.length}
69+
</Badge>
70+
&nbsp; chapters
71+
</Text>
72+
<Divider variant="dashed" my="xs" label="Summary" />
73+
<Text size="sm">{manga.metadata.summary || 'No summary...'}</Text>
74+
<Divider variant="dashed" my="xs" label="Genres" />
75+
<Group spacing="xs">
76+
{manga.metadata.genres.map((genre) => (
77+
<Tooltip label={genre} key={genre}>
78+
<div style={{ maxWidth: 100 }}>
79+
<Badge color="indigo" variant="light" size="xs" fullWidth>
80+
{genre}
81+
</Badge>
82+
</div>
83+
</Tooltip>
84+
))}
85+
</Group>
86+
<Divider variant="dashed" my="xs" label="Tags" />
87+
<Group spacing="xs">
88+
{manga.metadata.tags.map((tag) => (
89+
<Tooltip label={tag} key={tag}>
90+
<div style={{ maxWidth: 100 }}>
91+
<Badge color="violet" variant="light" size="xs" fullWidth>
92+
{tag}
93+
</Badge>
94+
</div>
95+
</Tooltip>
96+
))}
97+
</Group>
98+
</Grid.Col>
99+
</Grid>
100+
);
101+
}

src/components/navbar.tsx

+14-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Image,
88
MantineColor,
99
Navbar,
10+
ScrollArea,
1011
SimpleGrid,
1112
Skeleton,
1213
Text,
@@ -37,7 +38,6 @@ const useStyles = createStyles((theme) => ({
3738
navbar: {
3839
paddingTop: 0,
3940
boxShadow: theme.shadows.md,
40-
overflowY: 'auto',
4141
fontSize: theme.fontSizes.xs,
4242
},
4343
history: {
@@ -261,20 +261,20 @@ export function KaizokuNavbar() {
261261
{activityQuery.data && <Activity data={activityQuery.data} />}
262262
</Navbar.Section>
263263

264-
<Navbar.Section>
265-
<Divider
266-
mb="md"
267-
labelPosition="left"
268-
mt="md"
269-
variant="solid"
270-
label={
271-
<Text color="dimmed" size="md" weight={500}>
272-
Latest Downloads
273-
</Text>
274-
}
275-
/>
264+
<Divider
265+
mb="md"
266+
labelPosition="left"
267+
mt="md"
268+
variant="solid"
269+
label={
270+
<Text color="dimmed" size="md" weight={500}>
271+
Latest Downloads
272+
</Text>
273+
}
274+
/>
275+
<Navbar.Section grow component={ScrollArea}>
276276
{historyQuery.isLoading && <NavBarSkeleton />}
277-
{historyQuery.data && <History data={historyQuery.data} />}
277+
<Box mx={8}>{historyQuery.data && <History data={historyQuery.data} />}</Box>
278278
</Navbar.Section>
279279
</Navbar>
280280
);

src/pages/manga/[id].tsx

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { Code, LoadingOverlay } from '@mantine/core';
1+
import { Grid, LoadingOverlay } from '@mantine/core';
22
import { useRouter } from 'next/router';
3+
import { ChaptersTable } from '../../components/chaptersTable';
4+
import { MangaDetail } from '../../components/mangaDetail';
35
import { trpc } from '../../utils/trpc';
46

57
export default function LibraryPage() {
@@ -20,9 +22,19 @@ export default function LibraryPage() {
2022
return <LoadingOverlay visible overlayBlur={2} />;
2123
}
2224

23-
if (mangaQuery.isError) {
25+
if (mangaQuery.isError || !mangaQuery.data) {
2426
router.push('/404');
27+
return null;
2528
}
2629

27-
return <Code>{JSON.stringify(mangaQuery.data, null, 2)}</Code>;
30+
return (
31+
<Grid gutter={5}>
32+
<Grid.Col span={12}>
33+
<MangaDetail manga={mangaQuery.data} />
34+
</Grid.Col>
35+
<Grid.Col span={12}>
36+
<ChaptersTable manga={mangaQuery.data} />
37+
</Grid.Col>
38+
</Grid>
39+
);
2840
}

src/server/trpc/router/manga.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ export const mangaRouter = t.router({
3535
const { id } = input;
3636
return ctx.prisma.manga.findUniqueOrThrow({
3737
include: {
38-
chapter: {
38+
chapters: {
3939
orderBy: {
40-
index: 'asc',
40+
index: 'desc',
4141
},
4242
},
4343
library: true,
@@ -176,7 +176,7 @@ export const mangaRouter = t.router({
176176
orderBy: {
177177
createdAt: 'desc',
178178
},
179-
take: 10,
179+
take: 50,
180180
include: {
181181
manga: {
182182
include: {

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3891,6 +3891,11 @@ make-error@^1.1.1:
38913891
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
38923892
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
38933893

3894+
mantine-datatable@^1.7.9:
3895+
version "1.7.9"
3896+
resolved "https://registry.yarnpkg.com/mantine-datatable/-/mantine-datatable-1.7.9.tgz#4e97fbfc67c2f8777ac2745199210bdd5c775cff"
3897+
integrity sha512-D4lkckNBPtTZl9cy9ONGZ1D/qFcOLvyfqcXI/6VM17o3VLyNQDEBzsikv1NVgTZKrtqDKLGonkLJAMuxVuekKA==
3898+
38943899
map-obj@^1.0.0:
38953900
version "1.0.1"
38963901
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"

0 commit comments

Comments
 (0)