This commit is contained in:
2023-03-16 14:11:46 +00:00
parent 1dcb2b6c3d
commit 53128a92de
22 changed files with 358 additions and 68 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.env
Dockerfile
docker-compose.yml
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

2
.env
View File

@@ -8,4 +8,4 @@ MINIO_ACCESS_KEY=spider
MINIO_SECRET_KEY=spiderman!
MINIO_ENDPOINT=http://docker.pve:9001/api/v1
MINIO_ENABLED=1
DATABASE_URL=postgres://postgres:postgres@database.pve/spider
DATABASE_URL=mysql://vault:vault@database.pve/spider

View File

@@ -4,22 +4,17 @@ FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
COPY package.json yarn.lock ./
RUN yarn config set registry https://nexus.yoshino-s.xyz/repository/npm/
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
RUN yarn --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
RUN yarn prisma generate && yarn build
FROM base AS runner
WORKDIR /app

View File

@@ -0,0 +1,94 @@
import { ActionIcon, createStyles, Group, Header, rem, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { IconSearch } from "@tabler/icons";
import { useRouter } from "next/router";
const useStyles = createStyles((theme) => ({
header: {
paddingLeft: theme.spacing.md,
paddingRight: theme.spacing.md,
},
inner: {
height: rem(56),
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
links: {
[theme.fn.smallerThan("md")]: {
display: "none",
},
},
search: {
[theme.fn.smallerThan("xs")]: {
display: "none",
},
},
link: {
display: "block",
lineHeight: 1,
padding: `${rem(8)} ${rem(12)}`,
borderRadius: theme.radius.sm,
textDecoration: "none",
color: theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.colors.gray[7],
fontSize: theme.fontSizes.sm,
fontWeight: 500,
"&:hover": {
backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0],
},
},
}));
interface HeaderSearchProps {
links: { link: string; label: string }[];
}
export function HeaderSearch({ links }: HeaderSearchProps) {
const { classes } = useStyles();
const router = useRouter();
const form = useForm({
initialValues: {
search: "",
},
});
const items = links.map((link) => (
<a key={link.label} href={link.link} className={classes.link}>
{link.label}
</a>
));
function submit({ search }: { search: string }) {
router.push({
pathname: "/search/[word]",
query: { word: search },
});
}
return (
<Header height={56} className={classes.header} mb={120}>
<div className={classes.inner}>
<Group spacing={5}>{items}</Group>
<form onSubmit={form.onSubmit(submit)}>
<Group>
<TextInput
className={classes.search}
placeholder="Search"
required
{...form.getInputProps("search")}
/>
<ActionIcon type="submit">
<IconSearch />
</ActionIcon>
</Group>
</form>
</div>
</Header>
);
}

View File

@@ -52,7 +52,7 @@ export default function ParagraphGrid({
<Grid my="md">
{paragraphs.map((paragraph) => {
return (
<Grid.Col span={4} key={paragraph.id}>
<Grid.Col xs={12} sm={4} lg={3} key={paragraph.id}>
<ParagraphCard
title={paragraph.title}
id={paragraph.id}

View File

@@ -5,5 +5,6 @@ services:
container_name: ds-next
restart: always
ports:
- "9090:8080"
- "9090:3000"
env_file:
- .env

50
migrate.ts Normal file
View File

@@ -0,0 +1,50 @@
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline/promises";
import { Paragraph, PrismaClient } from "@prisma/client";
import { prismaClient } from "./lib/db";
const client = new PrismaClient();
async function processLineByLine() {
await prismaClient.$executeRaw`delete from Paragraph`;
const fileStream = createReadStream("../paragraph.json");
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity,
});
let idx = 1;
let submitData: Paragraph[] = [];
for await (const line of rl) {
const data = JSON.parse(line);
idx++;
submitData.push({
id: data.id,
content: data.content,
cover: data.cover,
title: data.title,
tags: data.tags.join(","),
author: data.author,
markdown: data.markdown,
time: new Date(data.time.$date),
});
if (idx % 1000 === 0) {
console.log(idx);
await client.paragraph.createMany({
data: submitData,
});
submitData = [];
}
}
await client.paragraph.createMany({
data: submitData,
});
}
processLineByLine();

View File

@@ -1,5 +1,6 @@
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
// eslint-disable-next-line @typescript-eslint/no-var-requires
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
module.exports = withBundleAnalyzer({
@@ -7,4 +8,5 @@ module.exports = withBundleAnalyzer({
eslint: {
ignoreDuringBuilds: true,
},
output: "standalone",
});

View File

@@ -24,6 +24,7 @@
"@mantine/carousel": "6.0.0",
"@mantine/core": "6.0.0",
"@mantine/dates": "6.0.0",
"@mantine/form": "^6.0.2",
"@mantine/hooks": "6.0.0",
"@mantine/next": "6.0.0",
"@mantine/notifications": "6.0.0",

View File

@@ -5,6 +5,8 @@ import NextApp, { AppContext, AppProps } from "next/app";
import Head from "next/head";
import { useState } from "react";
import { HeaderSearch } from "../components/Header/Header";
export default function App(props: AppProps & { colorScheme: ColorScheme }) {
const { Component, pageProps } = props;
const [colorScheme, setColorScheme] = useState<ColorScheme>(props.colorScheme);
@@ -25,6 +27,18 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
<MantineProvider theme={{ colorScheme }} withGlobalStyles withNormalizeCSS>
<HeaderSearch
links={[
{
link: "/",
label: "Home",
},
{
link: "/statistic",
label: "Statistic",
},
]}
/>
<Component {...pageProps} />
<Notifications />
</MantineProvider>

View File

@@ -1,7 +1,5 @@
import { Button } from "@mantine/core";
import { Paragraph } from "@prisma/client";
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import { useRouter } from "next/router";
import ParagraphGrid from "../components/ParagraphGrid/ParagraphGrid";
import { prismaClient } from "../lib/db";
@@ -14,15 +12,7 @@ interface ListProps {
}
export default function HomePage(props: ListProps) {
const router = useRouter();
return (
<ParagraphGrid
title="DS-Next"
titleAction={<Button onClick={() => router.push("/statistic")}>Statistic</Button>}
{...props}
/>
);
return <ParagraphGrid title="DS-Next" {...props} />;
}
export async function getServerSideProps(

View File

@@ -48,7 +48,7 @@ export default function ParagraphPage({ paragraph }: { paragraph: Paragraph }) {
</Button>
</Group>
<Group mb="xl">
{paragraph.tags.map((tag) => (
{paragraph.tags.split(",").map((tag) => (
<UnstyledButton
key={tag}
onClick={() =>

70
pages/search/[word].tsx Normal file
View File

@@ -0,0 +1,70 @@
import { Paragraph } from "@prisma/client";
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import ParagraphGrid from "../../components/ParagraphGrid/ParagraphGrid";
import { prismaClient } from "../../lib/db";
interface ListProps {
paragraphs: Omit<Paragraph, "content" | "markdown">[];
skip: number;
take: number;
total: number;
word: string;
}
export default function TagPage(props: ListProps) {
return <ParagraphGrid title={`DS-Next | Search ${props.word}`} {...props} />;
}
export async function getServerSideProps(
ctx: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<ListProps>> {
const skip = Number(ctx.query.skip ?? 0);
const take = Number(ctx.query.take ?? 12);
const word = ctx.params?.word;
if (typeof word !== "string") {
return { notFound: true };
}
const condition = {
content: {
search: word,
},
tags: {
search: word,
},
author: {
search: word,
},
title: {
search: word,
},
};
const [total, paragraphs] = await Promise.all([
prismaClient.paragraph.count({
where: condition,
}),
await prismaClient.paragraph.findMany({
where: condition,
skip: Number(skip),
take: Number(take),
orderBy: {
time: "desc",
},
select: {
id: true,
title: true,
tags: true,
time: true,
author: true,
cover: true,
},
}),
]);
paragraphs.forEach((paragraph) => {
paragraph.time = paragraph.time.getTime() as any;
});
return { props: { word, paragraphs, skip, take, total } };
}

61
pages/tag copy/[name].tsx Normal file
View File

@@ -0,0 +1,61 @@
import { Paragraph } from "@prisma/client";
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import ParagraphGrid from "../../components/ParagraphGrid/ParagraphGrid";
import { prismaClient } from "../../lib/db";
interface ListProps {
paragraphs: Omit<Paragraph, "content" | "markdown">[];
skip: number;
take: number;
total: number;
tag: string;
}
export default function TagPage(props: ListProps) {
return <ParagraphGrid title={`DS-Next | Tag ${props.tag}`} {...props} />;
}
export async function getServerSideProps(
ctx: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<ListProps>> {
const skip = Number(ctx.query.skip ?? 0);
const take = Number(ctx.query.take ?? 12);
const tag = ctx.params?.name;
if (!tag || typeof tag !== "string") {
return { notFound: true };
}
const condition = {
tags: {
contains: tag,
},
};
const [total, paragraphs] = await Promise.all([
prismaClient.paragraph.count({
where: condition,
}),
await prismaClient.paragraph.findMany({
where: condition,
skip: Number(skip),
take: Number(take),
orderBy: {
time: "desc",
},
select: {
id: true,
title: true,
tags: true,
time: true,
author: true,
cover: true,
},
}),
]);
paragraphs.forEach((paragraph) => {
paragraph.time = paragraph.time.getTime() as any;
});
return { props: { tag, paragraphs, skip, take, total } };
}

View File

@@ -29,7 +29,7 @@ export async function getServerSideProps(
const condition = {
tags: {
has: tag,
contains: tag,
},
};

View File

@@ -1,13 +0,0 @@
-- CreateTable
CREATE TABLE "paragraph" (
"id" TEXT NOT NULL,
"author" TEXT NOT NULL,
"content" TEXT NOT NULL,
"cover" TEXT,
"markdown" BOOLEAN,
"tags" TEXT[],
"time" DATE NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "paragraph_pkey" PRIMARY KEY ("id")
);

View File

@@ -1,22 +0,0 @@
/*
Warnings:
- You are about to drop the `paragraph` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "paragraph";
-- CreateTable
CREATE TABLE "Paragraph" (
"id" TEXT NOT NULL,
"author" TEXT NOT NULL,
"content" TEXT NOT NULL,
"cover" TEXT,
"markdown" BOOLEAN,
"tags" TEXT[],
"time" DATE NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Paragraph_pkey" PRIMARY KEY ("id")
);

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE `Paragraph` (
`id` VARCHAR(191) NOT NULL,
`author` VARCHAR(191) NOT NULL,
`content` VARCHAR(191) NOT NULL,
`cover` VARCHAR(191) NULL,
`markdown` BOOLEAN NULL,
`tags` VARCHAR(191) NOT NULL,
`time` DATE NOT NULL,
`title` VARCHAR(191) NOT NULL,
INDEX `Paragraph_author_idx`(`author`),
INDEX `Paragraph_time_idx`(`time`),
INDEX `Paragraph_title_idx`(`title`),
FULLTEXT INDEX `Paragraph_content_author_title_tags_idx`(`content`, `author`, `title`, `tags`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Paragraph` MODIFY `content` LONGTEXT NOT NULL;

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
provider = "mysql"

View File

@@ -1,19 +1,25 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["fullTextSearch", "fullTextIndex"]
}
datasource db {
provider = "postgres"
provider = "mysql"
url = env("DATABASE_URL")
}
model Paragraph {
id String @id
author String
content String
content String @db.LongText
cover String?
markdown Boolean?
tags String[]
tags String
time DateTime @db.Date
title String
@@index([author])
@@index([time])
@@index([title])
@@fulltext([content, author, title, tags])
}

View File

@@ -1745,6 +1745,14 @@
dependencies:
"@mantine/utils" "6.0.0"
"@mantine/form@^6.0.2":
version "6.0.2"
resolved "https://nexus.yoshino-s.xyz/repository/npm/@mantine/form/-/form-6.0.2.tgz#560b0e2b9e5775ec564e87c2c4d8bcf46e43f805"
integrity sha512-M18h8NBhV3P1iq+A1xS1x4dFN+xREu9k6/NmY5ftwYf4UwhVgiscKQhJ28ZC+m+oAtMZmdBhsHM5LVnFquPQJA==
dependencies:
fast-deep-equal "^3.1.3"
klona "^2.0.5"
"@mantine/hooks@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-6.0.0.tgz#08b67946e0b45f67181efa9e37df68f92a8ee6d1"
@@ -8678,6 +8686,11 @@ klona@^2.0.4:
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
klona@^2.0.5:
version "2.0.6"
resolved "https://nexus.yoshino-s.xyz/repository/npm/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"