search
This commit is contained in:
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.env
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
||||
2
.env
2
.env
@@ -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
|
||||
13
Dockerfile
13
Dockerfile
@@ -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
|
||||
|
||||
|
||||
94
components/Header/Header.tsx
Normal file
94
components/Header/Header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -5,5 +5,6 @@ services:
|
||||
container_name: ds-next
|
||||
restart: always
|
||||
ports:
|
||||
- "9090:8080"
|
||||
|
||||
- "9090:3000"
|
||||
env_file:
|
||||
- .env
|
||||
50
migrate.ts
Normal file
50
migrate.ts
Normal 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();
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
70
pages/search/[word].tsx
Normal 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
61
pages/tag copy/[name].tsx
Normal 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 } };
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export async function getServerSideProps(
|
||||
|
||||
const condition = {
|
||||
tags: {
|
||||
has: tag,
|
||||
contains: tag,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
@@ -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")
|
||||
);
|
||||
17
prisma/migrations/20230316135037_dev/migration.sql
Normal file
17
prisma/migrations/20230316135037_dev/migration.sql
Normal 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;
|
||||
2
prisma/migrations/20230316135533_dev/migration.sql
Normal file
2
prisma/migrations/20230316135533_dev/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Paragraph` MODIFY `content` LONGTEXT NOT NULL;
|
||||
@@ -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"
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user