This commit is contained in:
2023-11-02 14:47:08 +08:00
parent 8fa486e420
commit 03b0016db1
38 changed files with 12616 additions and 15652 deletions

View File

@@ -1 +0,0 @@
*.js

View File

@@ -1,36 +1,26 @@
// eslint-disable-next-line no-undef
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
project: "./tsconfig.json"
},
plugins: ["@typescript-eslint/eslint-plugin"],
extends: ["plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:import/recommended", "plugin:import/typescript", "plugin:prettier/recommended", "plugin:storybook/recommended"],
root: true,
env: {
node: true,
jest: true
},
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended",
"plugin:storybook/recommended",
],
ignorePatterns: ["dist", "src/gql/*"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh", "prettier"],
rules: {
"prettier/prettier": ["error", {
singleQuote: false
}],
"@typescript-eslint/explicit-function-return-type": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/quotes": [2, "double", "avoid-escape"],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }],
"quotes": [2, "double", "avoid-escape"],
"semi": ["error", "always"],
"eol-last": ["error", "always"],
"react/react-in-jsx-scope": "off",
"import/no-unresolved": "off",
"import/order": ["error", {
"newlines-between": "always",
"alphabetize": {
"order": "asc"
}
}]
}
};
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
};

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.vscode

View File

@@ -5,23 +5,31 @@
# Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
image: node:lts
before_script:
- yarn install --frozen-lockfile
stages:
- release
- test
- deploy
- release
- test
- deploy
pages:
image: node:18.17.1
before_script:
- corepack enable
- corepack prepare pnpm@latest-8 --activate
- pnpm config set store-dir .pnpm-store
script:
- yarn build
- mv dist public
- pnpm install # install dependencies
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
artifacts:
paths:
- public
- public
rules:
- if: "$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH"
- if: "$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH"
include:
- project: template/gitlabci-template
file: docker.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml

View File

@@ -0,0 +1,18 @@
import { DocsContainer as BaseContainer } from "@storybook/blocks";
import { themes } from "@storybook/theming";
import React from "react";
import { useDarkMode } from "storybook-dark-mode";
export const DocsContainer = ({ children, context }) => {
const dark = useDarkMode();
return (
<BaseContainer
context={context}
theme={dark ? themes.dark : themes.light}
>
{children}
</BaseContainer>
);
};

View File

@@ -1,20 +1,20 @@
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"storybook-dark-mode",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"storybook-addon-react-router-v6",
"storybook-dark-mode",
"storybook-react-i18next"
],
"framework": "@storybook/react",
"core": {
"builder": "@storybook/builder-vite"
framework: {
name: "@storybook/react-vite",
options: {},
},
"features": {
"storyStoreV7": true
}
}
docs: {
autodocs: "tag",
},
};
export default config;

View File

@@ -1,3 +1,3 @@
<script>
window.global = window;
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@100;400&display=swap" rel="stylesheet">

View File

@@ -1,61 +1,50 @@
import {
ActionIcon, Affix, ColorSchemeProvider, createEmotionCache, MantineProvider
} from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import React, { useState } from 'react';
import { Container, MantineProvider } from "@mantine/core";
import '@mantine/core/styles.css';
import type { Preview } from "@storybook/react";
import { useDarkMode } from 'storybook-dark-mode';
import rtlPlugin from 'stylis-plugin-rtl';
export const parameters = {
layout: 'fullscreen' ,
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
import i18n from "../src/i18n";
import React, { Fragment } from "react";
import { DocsContainer } from "./DocsContainer";
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: {
container: DocsContainer,
},
i18n
},
globals: {
locale: 'en',
locales: {
en: 'English',
zh: '中文',
},
},
};
const rtlCache = createEmotionCache({ key: 'mantine-rtl', stylisPlugins: [rtlPlugin] });
decorators: [
(Story, runtime) => {
const isDark = useDarkMode();
function ThemeWrapper(props: any) {
const [rtl, setRtl] = useState(false);
const toggleRtl = () => setRtl((r) => !r);
useHotkeys([['mod + L', toggleRtl]]);
return (
<ColorSchemeProvider colorScheme="light" toggleColorScheme={() => {}}>
<MantineProvider
theme={{
dir: rtl ? 'rtl' : 'ltr',
colorScheme: useDarkMode() ? 'dark' : 'light',
headings: { fontFamily: 'Greycliff CF, sans-serif' },
}}
emotionCache={rtl ? rtlCache : undefined}
withGlobalStyles
withNormalizeCSS
>
<Affix position={{ right: rtl ? 'unset' : 0, left: rtl ? 0 : 'unset', bottom: 0 }}>
<ActionIcon
onClick={toggleRtl}
variant="default"
style={{
borderBottom: 0,
borderRight: 0,
borderTopLeftRadius: 4,
width: 60,
fontWeight: 700,
}}
radius={0}
size={30}
>
{rtl ? 'RTL' : 'LTR'}
</ActionIcon>
</Affix>
<div dir={rtl ? 'rtl' : 'ltr'}>{props.children}</div>
const C = runtime.parameters.layout === 'fullscreen' ? Fragment : Container;
return <MantineProvider withCssVariables forceColorScheme={
isDark ? 'dark' : 'light'
}>
<C>
<Story />
</C>
</MantineProvider>
</ColorSchemeProvider>
);
}
},
]
};
export const decorators = [(renderStory: any) => <ThemeWrapper>{renderStory()}</ThemeWrapper>];
export default preview;

21
.vscode/settings.json vendored
View File

@@ -1,21 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": [
"source.organizeImports",
"source.fixAll.eslint"
],
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"cSpell.words": [
"mantine",
"MINIO",
"zincsearch"
],
"i18n-ally.localesPaths": [
"src/locales"
]
}

View File

@@ -1,25 +0,0 @@
FROM node:18-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn config set registry https://nexus.yoshino-s.xyz/repository/npm/
RUN yarn --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
FROM nginx:1.21-alpine AS runner
COPY default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /static
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets//favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Mantine App</title>
<title>DS-Next</title>
</head>
<body>
<div id="root"></div>

View File

@@ -16,75 +16,79 @@
"chromatic": "npx chromatic --project-token=180ac2186305"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@mantine/core": "6.0.0",
"@mantine/form": "^6.0.0",
"@mantine/hooks": "^6.0.0",
"@mantine/notifications": "^6.0.0",
"@reduxjs/toolkit": "^1.9.3",
"@tabler/icons-react": "^2.30.0",
"axios": "^1.3.4",
"dayjs": "^1.11.7",
"i18next": ">=21.0.0",
"i18next-browser-languagedetector": "^6.1.4",
"@mantine/core": "^7.1.7",
"@mantine/ds": "^7.1.7",
"@mantine/hooks": "^7.1.7",
"@mantine/notifications": "^7.1.7",
"@reduxjs/toolkit": "^1.9.7",
"@tabler/icons-react": "^2.40.0",
"axios": "^1.6.0",
"dayjs": "^1.11.10",
"i18next": "^23.6.0",
"i18next-browser-languagedetector": "^7.1.0",
"lodash": "^4.17.21",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-i18next": "^11.17.1",
"react-redux": "^8.0.5",
"react-router-dom": "^6.3.0",
"remark": "^14.0.2",
"remark-html": "^15.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^13.3.1",
"react-redux": "^8.1.3",
"react-router-dom": "^6.18.0",
"remark": "^15.0.1",
"remark-html": "^16.0.1"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-interactions": "^6.5.16",
"@storybook/addon-links": "^6.5.16",
"@storybook/addons": ">=6.5.0",
"@storybook/api": ">=6.5.0",
"@storybook/builder-vite": "^0.4.2",
"@storybook/components": ">=6.5.0",
"@storybook/core-events": ">=6.5.0",
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "^0.0.13",
"@storybook/theming": ">=6.5.0",
"@testing-library/dom": "^8.20.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.4.0",
"@types/lodash": "^4.14.191",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vitejs/plugin-react": "^3.1.0",
"babel-loader": "^8.3.0",
"chromatic": "^6.17.1",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5",
"@babel/core": "^7.23.2",
"@mantine/form": "^7.1.7",
"@storybook/addon-actions": "^7.5.2",
"@storybook/addon-essentials": "^7.5.2",
"@storybook/addon-interactions": "^7.5.2",
"@storybook/addon-links": "^7.5.2",
"@storybook/addons": "^7.5.2",
"@storybook/api": "^7.5.2",
"@storybook/builder-vite": "^7.5.2",
"@storybook/components": "^7.5.2",
"@storybook/core-events": "^7.5.2",
"@storybook/react": "^7.5.2",
"@storybook/testing-library": "^0.2.2",
"@storybook/theming": "^7.5.2",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^29.5.7",
"@types/lodash": "^4.14.200",
"@types/react": "^18.2.33",
"@types/react-dom": "^18.2.14",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"@vitejs/plugin-react": "^4.1.0",
"babel-loader": "^9.1.3",
"chromatic": "^7.6.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.6.11",
"i18next-http-backend": "^1.4.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-storybook": "^0.6.15",
"i18next-http-backend": "^2.3.1",
"install-peerdeps": "^3.0.3",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"prettier": "^2.8.3",
"react-router": "^6.3.0",
"storybook": "^6.5.16",
"storybook-addon-react-router-v6": "0.2.1",
"storybook-dark-mode": "^2.1.1",
"storybook-react-i18next": "1.1.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.31",
"postcss-preset-mantine": "^1.9.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.0.3",
"react-router": "^6.18.0",
"storybook": "^7.5.2",
"storybook-addon-react-router-v6": "2.0.8",
"storybook-dark-mode": "^3.0.1",
"storybook-react-i18next": "2.0.9",
"stylis-plugin-rtl": "^2.1.1",
"ts-jest": "^29.0.5",
"typescript": "^4.9.5",
"vite": "^4.1.1"
"ts-jest": "^29.1.1",
"typescript": "^5.2.2",
"vite": "^4.5.0"
}
}

12076
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

15
postcss.config.js Normal file
View File

@@ -0,0 +1,15 @@
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};

View File

@@ -1,34 +0,0 @@
import { ColorSchemeProvider, MantineProvider } from "@mantine/core";
import { useEffect } from "react";
import { usePreferenceState } from "./store/module/preference";
interface ThemeProviderProps {
children: React.ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const {
state: { colorScheme },
toggleColorScheme,
} = usePreferenceState();
useEffect(() => {
console.log(colorScheme);
}, []);
return (
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<MantineProvider
theme={{ colorScheme }}
withGlobalStyles
withNormalizeCSS
>
{children}
</MantineProvider>
</ColorSchemeProvider>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,31 +0,0 @@
import { ActionIcon, Group, useMantineColorScheme } from "@mantine/core";
import { IconMoonStars, IconSun } from "@tabler/icons-react";
export function ColorSchemeToggle() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return (
<Group position="center" mt="xl">
<ActionIcon
onClick={() => toggleColorScheme()}
size="xl"
sx={(theme) => ({
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[0],
color:
theme.colorScheme === "dark"
? theme.colors.yellow[4]
: theme.colors.blue[6],
})}
>
{colorScheme === "dark" ? (
<IconSun size={20} stroke={1.5} />
) : (
<IconMoonStars size={20} stroke={1.5} />
)}
</ActionIcon>
</Group>
);
}

View File

@@ -1,105 +1,5 @@
import {
ActionIcon,
Group,
Header,
Text,
TextInput,
createStyles,
rem,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { useDocumentTitle, useMediaQuery } from "@mantine/hooks";
import { IconSearch, IconSettings } from "@tabler/icons-react";
import { Dispatch, SetStateAction, createContext, useContext } from "react";
import { useNavigate } from "react-router";
import { Link } from "react-router-dom";
const useStyles = createStyles((theme) => ({
header: {
paddingLeft: theme.spacing.md,
paddingRight: theme.spacing.md,
},
inner: {
height: rem(56),
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
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],
},
},
}));
import { Dispatch, SetStateAction, createContext } from "react";
export const TitleContext = createContext<
[string, Dispatch<SetStateAction<string>>]
>(["DS-Next", () => 0]);
export function HeaderSearch() {
const { classes } = useStyles();
const [title, _] = useContext(TitleContext);
const isMobile = useMediaQuery("(max-width: 768px)");
useDocumentTitle(title);
const navigate = useNavigate();
const form = useForm({
initialValues: {
search: "",
},
});
function submit({ search }: { search: string }) {
console.log(search);
navigate(`/search/${encodeURIComponent(search)}`);
}
return (
<Header height={56} className={classes.header}>
<div className={classes.inner}>
<span>
<Text weight={600} component={Link} to="/">
DS-Next
</Text>
{!isMobile && (
<Text weight={600} component="span">
{" | "}
{title}
</Text>
)}
</span>
<Group>
<form onSubmit={form.onSubmit(submit)}>
<TextInput
placeholder="Search"
icon={<IconSearch size="1rem" stroke={1.5} />}
{...form.getInputProps("search")}
required
/>
</form>
<ActionIcon component={Link} to="/settings">
<IconSettings />
</ActionIcon>
</Group>
</div>
</Header>
);
}

View File

@@ -1,27 +1,10 @@
import { Card, Group, Image, Text, createStyles } from "@mantine/core";
import { Badge, Card, Group, Image, Text } from "@mantine/core";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { Link } from "react-router-dom";
dayjs.extend(relativeTime);
const useStyles = createStyles((theme) => ({
card: {
backgroundColor:
theme.colorScheme === "dark" ? theme.colors.dark[7] : theme.white,
},
title: {
fontWeight: 700,
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
lineHeight: 1.2,
},
body: {
padding: theme.spacing.md,
},
}));
export function ParagraphCard({
cover,
title,
@@ -30,56 +13,49 @@ export function ParagraphCard({
tags,
_id,
}: Paragraph) {
const { classes } = useStyles();
const url = `/paragraph/${_id}`;
return (
<Card withBorder radius="md" p={0} className={classes.card}>
<Group noWrap spacing={0}>
<Card withBorder radius="md" padding="lg" shadow="sm">
<Card.Section>
<Link to={url}>
{cover && <Image src={cover} height={140} width={140} />}
</Link>
<div className={classes.body}>
<Text transform="uppercase" color="dimmed" weight={700} size="xs">
{tags.map((tag, index) => (
<>
{index > 0 && " • "}
<Text
component={Link}
key={index}
to={`/tag/${encodeURIComponent(tag)}`}
>
{tag}
</Text>
</>
))}
</Text>
<Text
className={classes.title}
mt="xs"
mb="md"
component={Link}
to={url}
>
{title}
</Text>
<Group noWrap spacing="xs">
<Group spacing="xs" noWrap>
<Text
size="xs"
</Card.Section>
<Group justify="space-between" mt="md" mb="xs">
<Text component={Link} to={url}>
{title}
</Text>
<Group>
{tags.map((tag, index) => (
<>
<Badge
component={Link}
to={`/author/${encodeURIComponent(author)}`}
key={index}
to={`/tag/${encodeURIComponent(tag)}`}
>
{author}
</Text>
</Group>
<Text size="xs" color="dimmed">
</Text>
<Text size="xs" color="dimmed">
{dayjs().to(dayjs(time))}
</Text>
</Group>
</div>
{tag}
</Badge>
</>
))}
</Group>
</Group>
<Group>
<Group>
<Text
size="xs"
component={Link}
to={`/author/${encodeURIComponent(author)}`}
>
{author}
</Text>
</Group>
<Text size="xs" c="dimmed">
</Text>
<Text size="xs" c="dimmed">
{dayjs().to(dayjs(time))}
</Text>
</Group>
</Card>
);

View File

@@ -12,7 +12,7 @@ export function ThemeSetting() {
return (
<SegmentedControl
value={colorScheme}
onChange={(value: "light" | "dark") => toggleColorScheme(value)}
onChange={toggleColorScheme}
data={[
{
value: "light",

View File

@@ -7,11 +7,11 @@ export interface PaginationParams {
}
const api = axios.create({
withCredentials: false,
auth: {
username: "viewer",
password: "publicviewer1",
},
baseURL: "https://api.ourdomain.com",
});
api.interceptors.response.use(
@@ -36,17 +36,17 @@ api.interceptors.response.use(
autoClose: true,
});
}
}
},
);
export class SearchApi {
static async search(
baseUrl: string,
query: ZincQueryForSDK
query: ZincQueryForSDK,
): Promise<SearchResponse> {
const { data } = await api.post(
new URL("/api/paragraph/_search", baseUrl).toString(),
query
query,
);
return data;
}
@@ -61,7 +61,7 @@ export class SearchApi {
}
static async getParagraph(baseUrl: string, id: string) {
const { data } = await api.get(
new URL(`/api/paragraph/_doc/${id}`, baseUrl).toString()
new URL(`/api/paragraph/_doc/${id}`, baseUrl).toString(),
);
return data._source;
}

View File

@@ -1,11 +1,11 @@
import { Pagination } from "@mantine/core";
import { merge } from "lodash";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { SearchApi } from "./api";
import { useOptionsState } from "@/store/module/options";
import { useDebounceCallback } from "@mantine/hooks";
import { merge } from "lodash";
import { SearchApi } from "./api";
export function usePaginationData<T>(query: ZincQueryForSDK) {
const [params, setParams] = useSearchParams({
@@ -15,51 +15,49 @@ export function usePaginationData<T>(query: ZincQueryForSDK) {
const { state: options } = useOptionsState();
const [total, setTotal] = useState(0);
const [data, setData] = useState<T[]>([]);
const [skip, setSkip] = useState(0);
const [take, _] = useState(parseInt(params.get("size") || "10"));
const [page, setPage] = useState(parseInt(params.get("page") || "1"));
function refresh() {
SearchApi.search(
const update = useDebounceCallback(async function update() {
console.log("query", query, page, take, options);
const resp = await SearchApi.search(
options.zincsearchUrl,
merge({}, query, {
from: skip,
max_results: take,
})
).then((resp) => {
setTotal(resp.hits.total.value);
setData(
resp.hits.hits.map((hit) =>
SearchApi.wrapParagraph(
options.s3Url,
merge({ _id: hit._id }, hit._source)
)
) as T[]
);
});
}
merge(
{
search_type: "matchall",
sort_fields: ["-@timestamp"],
_source: ["title", "cover", "author", "tags"],
},
query,
{
from: (page - 1) * take,
max_results: take,
},
),
);
setTotal(resp.hits.total.value);
setData(
resp.hits.hits.map((hit) =>
SearchApi.wrapParagraph(
options.s3Url,
merge({ _id: hit._id, "@timestamp": hit["@timestamp"] }, hit._source),
),
) as T[],
);
}, 200);
useEffect(update, [query, page, take, options, update]);
useEffect(() => {
refresh();
}, [skip, take]);
useEffect(() => {
console.log("set params");
setParams({
size: take.toString(),
page: page.toString(),
});
}, [take, page]);
useEffect(() => {
setSkip((page - 1) * take);
}, [page]);
}, [take, page, setParams]);
return {
total,
data,
page,
refresh,
pagination: (
<Pagination
total={Math.ceil(total / take)}

View File

@@ -1,41 +1,103 @@
import { Box, Container, Flex, ScrollArea, createStyles } from "@mantine/core";
import { Suspense, useState } from "react";
import { Outlet } from "react-router";
import {
Affix,
AppShell,
Avatar,
Button,
Group,
Text,
TextInput,
Transition,
UnstyledButton,
rem,
} from "@mantine/core";
import { Suspense, useCallback, useState } from "react";
import { Outlet, useNavigate } from "react-router";
import { HeaderSearch, TitleContext } from "@/component/Header/Header";
import { TitleContext } from "@/component/Header/Header";
import Loading from "@/page/Loading";
const useStyles = createStyles((theme) => ({
contentContainer: {
backgroundColor:
theme.colorScheme === "dark" ? theme.colors.dark[7] : theme.white,
width: "100vw",
overflow: "hidden",
},
rootContainer: {
height: "100vh",
width: "100vw",
},
}));
import { useForm } from "@mantine/form";
import { useHeadroom, useWindowScroll } from "@mantine/hooks";
import { IconArrowUp, IconSearch, IconSettings } from "@tabler/icons-react";
import { Link } from "react-router-dom";
export default function MainLayout() {
const { classes } = useStyles();
const [title, setTitle] = useState("");
const pinned = useHeadroom({ fixedAt: 60 });
const [scroll, scrollTo] = useWindowScroll();
const navigate = useNavigate();
const form = useForm({
initialValues: {
search: "",
},
});
const search = useCallback(function submit({ search }: { search: string }) {
console.log(search);
navigate(`/search/${encodeURIComponent(search)}`);
}, []);
return (
<TitleContext.Provider value={[title, setTitle]}>
<Flex direction="column" className={classes.rootContainer}>
<HeaderSearch />
<Box className={classes.contentContainer}>
<AppShell
header={{ height: 60, collapsed: !pinned, offset: false }}
padding="md"
h="100vh"
>
<AppShell.Header>
<Group h="100%" justify="space-between" px="md">
<Group>
<Avatar fw={700} component={Link} to="/">
DS
</Avatar>
<Text size="lg" fw={700} ml="sm">
{title}
</Text>
</Group>
<Group>
<form onSubmit={form.onSubmit(search)}>
<TextInput
placeholder="Search"
{...form.getInputProps("search")}
leftSection={
<IconSearch
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
/>
</form>
<UnstyledButton component={Link} to="/settings">
<IconSettings />
</UnstyledButton>
</Group>
</Group>
</AppShell.Header>
<AppShell.Main pt={`calc(${rem(60)} + var(--mantine-spacing-md))`}>
<Suspense fallback={<Loading />}>
<ScrollArea h="100%" w="100vw">
<Container maw="100vw">
<Outlet />
</Container>
</ScrollArea>
<Outlet />
</Suspense>
</Box>
</Flex>
</AppShell.Main>
</AppShell>
<Affix position={{ bottom: 20, right: 20 }}>
<Transition transition="slide-up" mounted={scroll.y > 0}>
{(transitionStyles) => (
<Button
leftSection={
<IconArrowUp style={{ width: rem(16), height: rem(16) }} />
}
style={transitionStyles}
onClick={() => scrollTo({ y: 0 })}
>
Scroll to top
</Button>
)}
</Transition>
</Affix>
</TitleContext.Provider>
);
}

View File

@@ -2,16 +2,21 @@ import { Notifications } from "@mantine/notifications";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { MantineProvider, createTheme } from "@mantine/core";
import App from "./App";
import { ThemeProvider } from "./ThemeProvider";
import store from "./store";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
import "@mantine/core/styles.css";
const theme = createTheme({
/** Put your mantine theme override here */
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<ThemeProvider>
<MantineProvider withCssVariables theme={theme}>
<Notifications />
<App />
</ThemeProvider>
</Provider>
</MantineProvider>
</Provider>,
);

View File

@@ -1,106 +0,0 @@
import {
Button,
Center,
Container,
createStyles,
Group,
rem,
Text,
Title,
} from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { useNavigate, useRouteError } from "react-router-dom";
const useStyles = createStyles((theme) => ({
root: {
backgroundColor: theme.fn.variant({
variant: "filled",
color: theme.primaryColor,
}).background,
minHeight: "100vh",
},
label: {
textAlign: "center",
fontWeight: 900,
fontSize: rem(220),
lineHeight: 1,
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
color: theme.colors[theme.primaryColor][3],
[theme.fn.smallerThan("sm")]: {
fontSize: rem(120),
},
},
title: {
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
textAlign: "center",
fontWeight: 900,
fontSize: rem(38),
color: theme.white,
[theme.fn.smallerThan("sm")]: {
fontSize: rem(32),
},
},
description: {
maxWidth: rem(540),
margin: "auto",
marginTop: theme.spacing.xl,
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
color: theme.colors[theme.primaryColor][1],
},
}));
export interface ErrorPageProps {
label?: string | null;
title?: string | null;
description?: string | null;
}
export default function ErrorPage(props: ErrorPageProps) {
const navigate = useNavigate();
const { classes } = useStyles();
const error = useRouteError();
useDocumentTitle(props.label ?? "Error");
return (
<Center className={classes.root}>
<Container>
<div className={classes.label}>{props.label ?? "Error"}</div>
<Title className={classes.title}>
{props.title ?? "An error occurred"}
</Title>
<Text size="lg" align="center" className={classes.description}>
{props.description ??
error?.toString?.() ??
"An error occurred while loading the page."}
</Text>
<Group position="center">
<Button variant="white" size="md" onClick={() => navigate(0)}>
Refresh
</Button>{" "}
or
<Button variant="white" size="md" onClick={() => navigate(-1)}>
Go Back
</Button>
</Group>
</Container>
</Center>
);
}
export function NotFoundPage() {
return (
<ErrorPage
label="404"
title="Page not found"
description="The page you are looking for might have been removed, had its name changed, or is temporarily unavailable."
/>
);
}

View File

@@ -0,0 +1,40 @@
.root {
padding-top: rem(80px);
padding-bottom: rem(120px);
background-color: var(--mantine-color-blue-filled);
}
.label {
text-align: center;
font-weight: 900;
font-size: rem(220px);
line-height: 1;
margin-bottom: calc(var(--mantine-spacing-xl) * 1.5);
color: var(--mantine-color-blue-3);
@media (max-width: $mantine-breakpoint-sm) {
font-size: rem(120px);
}
}
.title {
font-family:
Greycliff CF,
var(--mantine-font-family);
text-align: center;
font-weight: 900;
font-size: rem(38px);
color: var(--mantine-color-white);
@media (max-width: $mantine-breakpoint-sm) {
font-size: rem(32px);
}
}
.description {
max-width: rem(540px);
margin: auto;
margin-top: var(--mantine-spacing-xl);
margin-bottom: calc(var(--mantine-spacing-xl) * 1.5);
color: var(--mantine-color-blue-1);
}

View File

@@ -0,0 +1,27 @@
import { Button, Container, Group, Text, Title } from "@mantine/core";
import classes from "./ErrorPage.module.css";
export interface ErrorPageProps {
label?: string | null;
title?: string | null;
description?: string | null;
}
export default function ErrorPage(props: ErrorPageProps) {
return (
<div className={classes.root}>
<Container>
<div className={classes.label}>{props.label}</div>
<Title className={classes.title}>{props.title}</Title>
<Text size="lg" ta="center" className={classes.description}>
{props.description}
</Text>
<Group justify="center">
<Button variant="white" size="md">
Refresh the page
</Button>
</Group>
</Container>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import ErrorPage from "./ErrorPage";
export default function NotFoundPage() {
return (
<ErrorPage
label="404"
title="Page not found"
description="The page you are looking for might have been removed, had its name changed, or is temporarily unavailable."
/>
);
}

View File

@@ -1,22 +1,12 @@
import { createStyles, LoadingOverlay } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
const useStyles = createStyles((theme) => ({
root: {
height: "100vh",
backgroundColor: theme.fn.variant({
variant: "filled",
color: theme.primaryColor,
}).background,
fontSize: theme.fontSizes.xl,
},
}));
import { LoadingOverlay } from "@mantine/core";
export default function Loading() {
const { classes } = useStyles();
useDocumentTitle("Loading");
return (
<div className={classes.root}>
<div
style={{
height: "100vh",
}}
>
<LoadingOverlay visible />
</div>
);

View File

@@ -5,15 +5,11 @@ import {
Text,
Title,
TypographyStylesProvider,
UnstyledButton,
createStyles,
} from "@mantine/core";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useContext, useEffect } from "react";
import { useLoaderData } from "react-router";
import { TitleContext } from "@/component/Header/Header";
import { Link } from "react-router-dom";
function stripStyles(content: string) {
@@ -54,58 +50,50 @@ function stripStyles(content: string) {
return element.innerHTML;
}
const useStyles = createStyles(() => ({
paragraph: {
lineBreak: "anywhere",
},
}));
dayjs.extend(relativeTime);
export default function ParagraphPage() {
const { classes } = useStyles();
const [_, setTitle] = useContext(TitleContext);
const paragraph = useLoaderData() as Paragraph;
useEffect(() => {
setTitle(paragraph.title);
}, [paragraph]);
return (
<Container py="2rem">
<Title mb="xl">{paragraph.title}</Title>
<Group position="apart">
<Group justify="space-between" align="center">
<Group>
<Text c="dimmed"> {dayjs().to(dayjs(paragraph.time))}</Text>
<UnstyledButton
<Badge
ml="1rem"
radius="sm"
component={Link}
to={`/author/${encodeURIComponent(
paragraph.author || "unknown"
)}`}
to={`/author/${encodeURIComponent(paragraph.author || "unknown")}`}
>
<Badge ml="1rem" radius="sm">
{paragraph.author}
</Badge>
</UnstyledButton>
{paragraph.author}
</Badge>
</Group>
<Group>
{paragraph.tags.map((tag, index) => (
<>
<Badge
component={Link}
key={index}
to={`/tag/${encodeURIComponent(tag)}`}
>
{tag}
</Badge>
</>
))}
</Group>
</Group>
<Group mb="xl">
{paragraph.tags?.map((tag) => (
<UnstyledButton
key={tag}
component={Link}
to={`/tag/${encodeURIComponent(tag)}`}
>
<Badge fz="xs" variant="dot">
{tag}
</Badge>
</UnstyledButton>
))}
</Group>
<TypographyStylesProvider>
<TypographyStylesProvider
style={{
paddingLeft: 0,
}}
>
<div
className={classes.paragraph}
style={{
lineBreak: "anywhere",
}}
dangerouslySetInnerHTML={{
__html: stripStyles(paragraph.content),
}}

View File

@@ -1,5 +1,4 @@
import { Grid, Group } from "@mantine/core";
import { merge } from "lodash";
import { useContext, useEffect } from "react";
import { useLoaderData, useLocation, useParams } from "react-router";
@@ -8,36 +7,16 @@ import { ParagraphCard } from "../component/ParagraphCard/ParagraphCard";
import { TitleContext } from "@/component/Header/Header";
import { usePaginationData } from "@/helper/hooks";
export interface SearchPageProps {
query?: ZincQueryForSDK;
}
export default function SearchPage() {
const [_title, setTitle] = useContext(TitleContext);
export default function SearchPage(props: SearchPageProps) {
const [_, setTitle] = useContext(TitleContext);
const params = useLoaderData();
const query: ZincQueryForSDK = merge(
{
search_type: "matchall",
sort_fields: ["-@timestamp"],
_source: ["title", "cover", "author", "tags"],
},
props.query,
params
);
const params = useLoaderData() as ZincQueryForSDK;
const {
page,
pagination,
refresh,
data: paragraphs,
} = usePaginationData<Paragraph>(query);
useEffect(() => {
console.log("refresh");
refresh();
}, [params]);
} = usePaginationData<Paragraph>(params);
const location = useLocation();
const param = useParams();
@@ -53,20 +32,20 @@ export default function SearchPage(props: SearchPageProps) {
}
const title = `${action} Page 1`;
setTitle(title);
}, [page]);
}, [page, location, param, setTitle]);
return (
<div>
<Grid my="md">
{paragraphs.map((paragraph) => {
return (
<Grid.Col xs={12} sm={6} key={paragraph._id}>
<ParagraphCard {...paragraph} key={paragraph._id} />
<Grid.Col span={{ base: 12, sm: 6 }} key={paragraph._id}>
<ParagraphCard {...paragraph} key={`${paragraph._id}_card`} />
</Grid.Col>
);
})}
</Grid>
<Group position="center">{pagination}</Group>
<Group justify="center">{pagination}</Group>
</div>
);
}

View File

@@ -1,12 +1,4 @@
import {
Container,
createStyles,
Paper,
Table,
Text,
TextInput,
Title,
} from "@mantine/core";
import { Container, Paper, Table, Text, TextInput, Title } from "@mantine/core";
import { ReactNode, useContext, useEffect } from "react";
import { TitleContext } from "@/component/Header/Header";
@@ -15,18 +7,6 @@ import store from "@/store";
import { useOptionsState } from "@/store/module/options";
import { setS3Url, setZincsearchUrl } from "@/store/reducer/options";
const useStyles = createStyles((theme) => ({
settingTable: {
tableLayout: "fixed",
"& thead": {
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[7]
: theme.colors.gray[3],
},
},
}));
interface SettingItem {
title: string;
description: string;
@@ -34,13 +14,12 @@ interface SettingItem {
}
export default function SettingsPage() {
const { classes } = useStyles();
const [_, setTitle] = useContext(TitleContext);
const { state: options } = useOptionsState();
useEffect(() => {
setTitle("Settings");
}, []);
}, [setTitle]);
const settings: SettingItem[] = [
{
@@ -81,30 +60,28 @@ export default function SettingsPage() {
</Title>
Customize the look and feel of your Coder deployment.
<Paper my="xl" radius="md" withBorder style={{ overflow: "hidden" }}>
<Table verticalSpacing="lg" className={classes.settingTable} striped>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<Table verticalSpacing="lg" striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Value</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{settings.map((setting) => (
<tr key={`${setting.title}`}>
<td>
<div>
<Text size="md" weight={500}>
{setting.title}
</Text>
<Text color="dimmed" size="sm">
{setting.description}
</Text>
</div>
</td>
<td>{setting.value}</td>
</tr>
<Table.Tr key={`${setting.title}`}>
<Table.Td>
<Text size="md" fw={500}>
{setting.title}
</Text>
<Text c="dimmed" size="sm">
{setting.description}
</Text>
</Table.Td>
<Table.Td>{setting.value}</Table.Td>
</Table.Tr>
))}
</tbody>
</Table.Tbody>
</Table>
</Paper>
</Container>

View File

@@ -8,10 +8,8 @@ import MainLayout from "@/layout/MainLayout";
import SearchPage from "@/page/Search";
import store from "@/store";
const NotFound = lazy(async () => ({
default: (await import("@/page/Exception")).NotFoundPage,
}));
const ErrorPage = lazy(() => import("@/page/Exception"));
const NotFound = lazy(() => import("@/page/Exception/NotFound"));
const ErrorPage = lazy(() => import("@/page/Exception/ErrorPage"));
const LoadingPage = lazy(async () => import("@/page/Loading"));
const ParagraphPage = lazy(async () => import("@/page/Paragraph"));
const SettingsPage = lazy(async () => import("@/page/Settings"));
@@ -25,6 +23,9 @@ const router = createHashRouter([
{
path: "/",
element: <SearchPage />,
loader() {
return {};
},
},
{
path: "/settings",
@@ -85,9 +86,9 @@ const router = createHashRouter([
const paragraph = await SearchApi.getParagraph(
store.getState().options.zincsearchUrl,
id
id,
).then((p) =>
SearchApi.wrapParagraph(store.getState().options.s3Url, p)
SearchApi.wrapParagraph(store.getState().options.s3Url, p),
);
console.log(paragraph.markdown);

View File

@@ -5,19 +5,21 @@ export interface OptionsState {
s3Url: string;
}
const ZINCSEARCH_URL = "https://zincsearch.yoshino-s.xyz";
const MINIO_URL = "https://minio-hdd.yoshino-s.xyz";
const optionsSlice = createSlice({
name: "stats",
initialState: {
zincsearchUrl: "https://zincsearch.yoshino-s.xyz",
s3Url: "https://minio-hdd.yoshino-s.xyz",
zincsearchUrl: ZINCSEARCH_URL,
s3Url: MINIO_URL,
} as OptionsState,
reducers: {
setZincsearchUrl: (state, action: PayloadAction<string | undefined>) => {
state.zincsearchUrl =
action.payload ?? "https://zincsearch.yoshino-s.xyz";
state.zincsearchUrl = action.payload ?? ZINCSEARCH_URL;
},
setS3Url: (state, action: PayloadAction<string | undefined>) => {
state.s3Url = action.payload ?? "https://minio-hdd.yoshino-s.xyz";
state.s3Url = action.payload ?? MINIO_URL;
},
},
});

View File

@@ -4,5 +4,5 @@
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
"include": ["vite.config.ts"],
}

14876
yarn.lock

File diff suppressed because it is too large Load Diff