update
This commit is contained in:
@@ -1 +0,0 @@
|
||||
*.js
|
||||
54
.eslintrc.js
54
.eslintrc.js
@@ -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
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.vscode
|
||||
@@ -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
|
||||
|
||||
18
.storybook/DocsContainer.tsx
Normal file
18
.storybook/DocsContainer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
@@ -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
21
.vscode/settings.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
25
Dockerfile
25
Dockerfile
@@ -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;"]
|
||||
@@ -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>
|
||||
|
||||
130
package.json
130
package.json
@@ -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
12076
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
postcss.config.js
Normal file
15
postcss.config.js
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ export function ThemeSetting() {
|
||||
return (
|
||||
<SegmentedControl
|
||||
value={colorScheme}
|
||||
onChange={(value: "light" | "dark") => toggleColorScheme(value)}
|
||||
onChange={toggleColorScheme}
|
||||
data={[
|
||||
{
|
||||
value: "light",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
15
src/main.tsx
15
src/main.tsx
@@ -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>,
|
||||
);
|
||||
|
||||
@@ -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."
|
||||
/>
|
||||
);
|
||||
}
|
||||
40
src/page/Exception/ErrorPage.module.css
Normal file
40
src/page/Exception/ErrorPage.module.css
Normal 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);
|
||||
}
|
||||
27
src/page/Exception/ErrorPage.tsx
Normal file
27
src/page/Exception/ErrorPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
src/page/Exception/NotFound.tsx
Normal file
11
src/page/Exception/NotFound.tsx
Normal 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."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"include": ["vite.config.ts"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user