Compare commits
14 Commits
a5ced3cf95
...
7f34a2f4a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
7f34a2f4a0
|
|||
| 5fc4d09c39 | |||
| 977492e78c | |||
| d84b92fe4f | |||
| 86fe107ec2 | |||
| 0460443a9d | |||
|
a9d6ab7b34
|
|||
|
d22d717849
|
|||
| d8f109cdb4 | |||
| 23d93ec5b5 | |||
| 4e16d6a8cc | |||
| 733c663ec2 | |||
| dd4d57fb05 | |||
| b249cccef3 |
1
.eslintcache
Normal file
1
.eslintcache
Normal file
File diff suppressed because one or more lines are too long
@@ -1,26 +0,0 @@
|
|||||||
// eslint-disable-next-line no-undef
|
|
||||||
module.exports = {
|
|
||||||
root: 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: {
|
|
||||||
"react-refresh/only-export-components": [
|
|
||||||
"warn",
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,3 +26,4 @@ dist-ssr
|
|||||||
.vscode
|
.vscode
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.env.sentry-build-plugin
|
.env.sentry-build-plugin
|
||||||
|
stats.html
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# The Docker image that will be used to build your app
|
|
||||||
image: node:18.17.1
|
|
||||||
# Functions that should be executed before the build script is run
|
|
||||||
before_script:
|
|
||||||
- corepack enable
|
|
||||||
- corepack prepare pnpm@latest-8 --activate
|
|
||||||
- pnpm config set store-dir .pnpm-store
|
|
||||||
pages:
|
|
||||||
script:
|
|
||||||
- pnpm install
|
|
||||||
- pnpm build
|
|
||||||
- mv dist public
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
# The folder that contains the files to be exposed at the Page URL
|
|
||||||
- public
|
|
||||||
rules:
|
|
||||||
# This ensures that only pushes to the default branch will trigger
|
|
||||||
# a pages deploy
|
|
||||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
|
|
||||||
9
.react-router/types/+future.ts
Normal file
9
.react-router/types/+future.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import "react-router";
|
||||||
|
|
||||||
|
declare module "react-router" {
|
||||||
|
interface Future {
|
||||||
|
unstable_middleware: false
|
||||||
|
}
|
||||||
|
}
|
||||||
47
.react-router/types/+routes.ts
Normal file
47
.react-router/types/+routes.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import "react-router"
|
||||||
|
|
||||||
|
declare module "react-router" {
|
||||||
|
interface Register {
|
||||||
|
pages: Pages
|
||||||
|
routeFiles: RouteFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pages = {
|
||||||
|
"/": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/settings": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/paragraph/:id": {
|
||||||
|
params: {
|
||||||
|
"id": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type RouteFiles = {
|
||||||
|
"root.tsx": {
|
||||||
|
id: "root";
|
||||||
|
page: "/" | "/settings" | "/paragraph/:id";
|
||||||
|
};
|
||||||
|
"./layouts/Main.layout.tsx": {
|
||||||
|
id: "layouts/Main.layout";
|
||||||
|
page: "/" | "/settings" | "/paragraph/:id";
|
||||||
|
};
|
||||||
|
"./pages/Search.page.tsx": {
|
||||||
|
id: "pages/Search.page";
|
||||||
|
page: "/";
|
||||||
|
};
|
||||||
|
"./pages/Settings.page.tsx": {
|
||||||
|
id: "pages/Settings.page";
|
||||||
|
page: "/settings";
|
||||||
|
};
|
||||||
|
"./pages/Paragraph.page.tsx": {
|
||||||
|
id: "pages/Paragraph.page";
|
||||||
|
page: "/paragraph/:id";
|
||||||
|
};
|
||||||
|
};
|
||||||
17
.react-router/types/+server-build.d.ts
vendored
Normal file
17
.react-router/types/+server-build.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
declare module "virtual:react-router/server-build" {
|
||||||
|
import { ServerBuild } from "react-router";
|
||||||
|
export const assets: ServerBuild["assets"];
|
||||||
|
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
|
||||||
|
export const basename: ServerBuild["basename"];
|
||||||
|
export const entry: ServerBuild["entry"];
|
||||||
|
export const future: ServerBuild["future"];
|
||||||
|
export const isSpaMode: ServerBuild["isSpaMode"];
|
||||||
|
export const prerender: ServerBuild["prerender"];
|
||||||
|
export const publicPath: ServerBuild["publicPath"];
|
||||||
|
export const routeDiscovery: ServerBuild["routeDiscovery"];
|
||||||
|
export const routes: ServerBuild["routes"];
|
||||||
|
export const ssr: ServerBuild["ssr"];
|
||||||
|
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
|
||||||
|
}
|
||||||
59
.react-router/types/app/+types/root.ts
Normal file
59
.react-router/types/app/+types/root.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../root.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "root.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../root.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// unstable_middleware
|
||||||
|
export type unstable_MiddlewareFunction = Annotations["unstable_MiddlewareFunction"];
|
||||||
|
|
||||||
|
// unstable_clientMiddleware
|
||||||
|
export type unstable_ClientMiddlewareFunction = Annotations["unstable_ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
62
.react-router/types/app/layouts/+types/Main.layout.ts
Normal file
62
.react-router/types/app/layouts/+types/Main.layout.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../Main.layout.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "./layouts/Main.layout.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "layouts/Main.layout";
|
||||||
|
module: typeof import("../Main.layout.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// unstable_middleware
|
||||||
|
export type unstable_MiddlewareFunction = Annotations["unstable_MiddlewareFunction"];
|
||||||
|
|
||||||
|
// unstable_clientMiddleware
|
||||||
|
export type unstable_ClientMiddlewareFunction = Annotations["unstable_ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
65
.react-router/types/app/pages/+types/Paragraph.page.ts
Normal file
65
.react-router/types/app/pages/+types/Paragraph.page.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../Paragraph.page.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "./pages/Paragraph.page.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "layouts/Main.layout";
|
||||||
|
module: typeof import("../../layouts/Main.layout.js");
|
||||||
|
}, {
|
||||||
|
id: "pages/Paragraph.page";
|
||||||
|
module: typeof import("../Paragraph.page.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// unstable_middleware
|
||||||
|
export type unstable_MiddlewareFunction = Annotations["unstable_MiddlewareFunction"];
|
||||||
|
|
||||||
|
// unstable_clientMiddleware
|
||||||
|
export type unstable_ClientMiddlewareFunction = Annotations["unstable_ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
65
.react-router/types/app/pages/+types/Search.page.ts
Normal file
65
.react-router/types/app/pages/+types/Search.page.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../Search.page.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "./pages/Search.page.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "layouts/Main.layout";
|
||||||
|
module: typeof import("../../layouts/Main.layout.js");
|
||||||
|
}, {
|
||||||
|
id: "pages/Search.page";
|
||||||
|
module: typeof import("../Search.page.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// unstable_middleware
|
||||||
|
export type unstable_MiddlewareFunction = Annotations["unstable_MiddlewareFunction"];
|
||||||
|
|
||||||
|
// unstable_clientMiddleware
|
||||||
|
export type unstable_ClientMiddlewareFunction = Annotations["unstable_ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
65
.react-router/types/app/pages/+types/Settings.page.ts
Normal file
65
.react-router/types/app/pages/+types/Settings.page.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../Settings.page.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "./pages/Settings.page.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "layouts/Main.layout";
|
||||||
|
module: typeof import("../../layouts/Main.layout.js");
|
||||||
|
}, {
|
||||||
|
id: "pages/Settings.page";
|
||||||
|
module: typeof import("../Settings.page.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// unstable_middleware
|
||||||
|
export type unstable_MiddlewareFunction = Annotations["unstable_MiddlewareFunction"];
|
||||||
|
|
||||||
|
// unstable_clientMiddleware
|
||||||
|
export type unstable_ClientMiddlewareFunction = Annotations["unstable_ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
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/addon-essentials",
|
|
||||||
"@storybook/addon-interactions",
|
|
||||||
"storybook-dark-mode",
|
|
||||||
"storybook-react-i18next"
|
|
||||||
],
|
|
||||||
framework: {
|
|
||||||
name: "@storybook/react-vite",
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
autodocs: "tag",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<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,50 +0,0 @@
|
|||||||
import { Container, MantineProvider } from "@mantine/core";
|
|
||||||
import '@mantine/core/styles.css';
|
|
||||||
import type { Preview } from "@storybook/react";
|
|
||||||
import { useDarkMode } from 'storybook-dark-mode';
|
|
||||||
|
|
||||||
|
|
||||||
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: '中文',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
decorators: [
|
|
||||||
(Story, runtime) => {
|
|
||||||
const isDark = useDarkMode();
|
|
||||||
|
|
||||||
const C = runtime.parameters.layout === 'fullscreen' ? Fragment : Container;
|
|
||||||
|
|
||||||
return <MantineProvider withCssVariables forceColorScheme={
|
|
||||||
isDark ? 'dark' : 'light'
|
|
||||||
}>
|
|
||||||
<C>
|
|
||||||
<Story />
|
|
||||||
</C>
|
|
||||||
</MantineProvider>
|
|
||||||
},
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export default preview;
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dispatch, SetStateAction, createContext } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
export const TitleContext = createContext<
|
export const TitleContext = createContext<
|
||||||
[string, Dispatch<SetStateAction<string>>]
|
[string, Dispatch<SetStateAction<string>>]
|
||||||
29
app/component/Hits/Hits.module.css
Normal file
29
app/component/Hits/Hits.module.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.img-wrapper{
|
||||||
|
column-count: 1;
|
||||||
|
column-gap: 10px;
|
||||||
|
|
||||||
|
@media screen and (min-width: 500px) {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 720px) {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 1100px) {
|
||||||
|
column-count: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter-reset: count;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (max-width: $mantine-breakpoint-md) {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
@media (max-width: $mantine-breakpoint-sm) {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>div{
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/component/Hits/Hits.tsx
Normal file
21
app/component/Hits/Hits.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Box } from "@mantine/core";
|
||||||
|
|
||||||
|
import type { UseHitsProps } from "react-instantsearch";
|
||||||
|
import { useHits } from "react-instantsearch";
|
||||||
|
|
||||||
|
import { ParagraphCard } from "../ParagraphCard/ParagraphCard";
|
||||||
|
|
||||||
|
import styles from "./Hits.module.css";
|
||||||
|
|
||||||
|
export default function Hits(props: UseHitsProps<Paragraph>) {
|
||||||
|
const { results } = useHits(props);
|
||||||
|
return (
|
||||||
|
<Box className={styles["img-wrapper"]}>
|
||||||
|
{results?.hits?.map((hit) => (
|
||||||
|
<Box key={hit.id}>
|
||||||
|
<ParagraphCard {...hit} key={`paragraph-card-${hit.id}`} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
app/component/Pagination/Pagination.tsx
Normal file
20
app/component/Pagination/Pagination.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Center, Group, Pagination as MantinePagination } from "@mantine/core";
|
||||||
|
|
||||||
|
import type { UsePaginationProps } from "react-instantsearch";
|
||||||
|
import { usePagination } from "react-instantsearch";
|
||||||
|
|
||||||
|
export default function Pagination(props: UsePaginationProps) {
|
||||||
|
const { currentRefinement, nbPages, refine } = usePagination(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Group>
|
||||||
|
<MantinePagination
|
||||||
|
total={nbPages}
|
||||||
|
value={currentRefinement + 1}
|
||||||
|
onChange={(value) => refine(value - 1)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,54 +1,45 @@
|
|||||||
import { Badge, Card, Group, Image, Text } from "@mantine/core";
|
import { Badge, Card, Group, Image, Text } from "@mantine/core";
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useContentFix } from "@/hooks/useContentFix";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export function ParagraphCard({
|
export function ParagraphCard({
|
||||||
cover,
|
cover,
|
||||||
title,
|
title,
|
||||||
"@timestamp": time,
|
time,
|
||||||
author,
|
author,
|
||||||
tags,
|
tags,
|
||||||
_id,
|
id,
|
||||||
}: Paragraph) {
|
}: Paragraph) {
|
||||||
const url = `/paragraph/${_id}`;
|
const url = `/paragraph/${id}`;
|
||||||
|
cover = useContentFix(cover) ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card withBorder radius="md" padding="lg" shadow="sm">
|
<Card withBorder radius="md" padding="lg" shadow="sm">
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Link to={url}>
|
<Link to={url} target="_blank">
|
||||||
{cover && <Image src={cover} height={140} width={140} />}
|
{cover && <Image src={cover} height={140} width={140} />}
|
||||||
</Link>
|
</Link>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Group justify="space-between" mt="md" mb="xs">
|
<Group justify="space-between" mt="md" mb="xs">
|
||||||
<Text component={Link} to={url}>
|
<Text component={Link} to={url} target="_blank">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Group>
|
<Group>
|
||||||
{tags.map((tag, index) => (
|
{tags.map((tag, index) => (
|
||||||
<>
|
<Badge key={index}>{tag}</Badge>
|
||||||
<Badge
|
|
||||||
component={Link}
|
|
||||||
key={index}
|
|
||||||
to={`/tag/${encodeURIComponent(tag)}`}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
</>
|
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Group>
|
<Group>
|
||||||
<Text
|
<Text size="xs">{author}</Text>
|
||||||
size="xs"
|
|
||||||
component={Link}
|
|
||||||
to={`/author/${encodeURIComponent(author)}`}
|
|
||||||
>
|
|
||||||
{author}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
•
|
•
|
||||||
130
app/component/Refinement/Refinement.tsx
Normal file
130
app/component/Refinement/Refinement.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ComboboxItem,
|
||||||
|
ComboboxLikeRenderOptionInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
CheckIcon,
|
||||||
|
Group,
|
||||||
|
MultiSelect,
|
||||||
|
Select,
|
||||||
|
rem,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useHitsPerPage,
|
||||||
|
useRefinementList,
|
||||||
|
useSortBy,
|
||||||
|
} from "react-instantsearch";
|
||||||
|
|
||||||
|
import { SourceLabelMap } from "@/constants";
|
||||||
|
|
||||||
|
const sortItems = [
|
||||||
|
{ value: "paragraph:time:desc", label: "Newest" },
|
||||||
|
{ value: "paragraph:time:asc", label: "Oldest" },
|
||||||
|
];
|
||||||
|
const hitsPerPageItems = [
|
||||||
|
{ value: 20, label: "20", default: true },
|
||||||
|
{ value: 40, label: "40" },
|
||||||
|
{ value: 60, label: "60" },
|
||||||
|
{ value: 100, label: "100" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Refinement() {
|
||||||
|
const { items, refine } = useRefinementList({ attribute: "source" });
|
||||||
|
const { currentRefinement, refine: refineSortBy } = useSortBy({
|
||||||
|
items: sortItems,
|
||||||
|
});
|
||||||
|
const hitsPerPage = useHitsPerPage({
|
||||||
|
items: hitsPerPageItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentVal = items
|
||||||
|
.filter((item) => item.isRefined)
|
||||||
|
.map((item) => item.value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refineSortBy(sortItems[0].value);
|
||||||
|
}, [refineSortBy]);
|
||||||
|
|
||||||
|
function SelectItem(props: ComboboxLikeRenderOptionInput<ComboboxItem>) {
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" w="100%">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
gap: "0.5em",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentVal.includes(props.option.value) && (
|
||||||
|
<CheckIcon
|
||||||
|
style={{
|
||||||
|
opacity: "0.4",
|
||||||
|
width: "0.8em",
|
||||||
|
minWidth: "0.8em",
|
||||||
|
height: "0.8em",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.option.label}
|
||||||
|
</Box>
|
||||||
|
<Badge>
|
||||||
|
{items.find((item) => item.value === props.option.value)?.count}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" align="center" my="md">
|
||||||
|
<Group>
|
||||||
|
<Select
|
||||||
|
data={sortItems}
|
||||||
|
value={currentRefinement}
|
||||||
|
defaultValue={sortItems[0].value}
|
||||||
|
onChange={(value) => value && refineSortBy(value)}
|
||||||
|
label="Sort by"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
styles={{
|
||||||
|
wrapper: {
|
||||||
|
width: rem(300),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
data={items.map((item) => ({
|
||||||
|
value: item.label,
|
||||||
|
label: SourceLabelMap[item.label],
|
||||||
|
}))}
|
||||||
|
renderOption={SelectItem}
|
||||||
|
value={currentVal}
|
||||||
|
label="Source"
|
||||||
|
clearable
|
||||||
|
onChange={(values) => {
|
||||||
|
const diff = values
|
||||||
|
.filter((value) => !currentVal.includes(value))
|
||||||
|
.concat(currentVal.filter((value) => !values.includes(value)));
|
||||||
|
diff.forEach((value) => refine(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group w={rem(96)}>
|
||||||
|
<Select
|
||||||
|
data={hitsPerPageItems.map((item) => ({
|
||||||
|
value: item.value.toString(),
|
||||||
|
label: item.label.toString(),
|
||||||
|
}))}
|
||||||
|
value={hitsPerPage.items
|
||||||
|
.find((item) => item.isRefined)
|
||||||
|
?.value.toString()}
|
||||||
|
onChange={(value) => value && hitsPerPage.refine(parseInt(value))}
|
||||||
|
label="Hits per page"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
app/component/ScrollTop/ScrollTop.tsx
Normal file
25
app/component/ScrollTop/ScrollTop.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Affix, Button, Transition, rem } from "@mantine/core";
|
||||||
|
import { useWindowScroll } from "@mantine/hooks";
|
||||||
|
import { TbArrowUp } from "react-icons/tb";
|
||||||
|
|
||||||
|
export default function ScrollTop() {
|
||||||
|
const [scroll, scrollTo] = useWindowScroll();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Affix position={{ bottom: 20, right: 20 }}>
|
||||||
|
<Transition transition="slide-up" mounted={scroll.y > 0}>
|
||||||
|
{(transitionStyles) => (
|
||||||
|
<Button
|
||||||
|
leftSection={
|
||||||
|
<TbArrowUp style={{ width: rem(16), height: rem(16) }} />
|
||||||
|
}
|
||||||
|
style={transitionStyles}
|
||||||
|
onClick={() => scrollTo({ y: 0 })}
|
||||||
|
>
|
||||||
|
Scroll to top
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
</Affix>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
app/component/SearchBox/SearchBox.tsx
Normal file
41
app/component/SearchBox/SearchBox.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { TextInput, rem } from "@mantine/core";
|
||||||
|
import { TbSearch } from "react-icons/tb";
|
||||||
|
|
||||||
|
import type { UseSearchBoxProps } from "react-instantsearch";
|
||||||
|
import { useSearchBox } from "react-instantsearch";
|
||||||
|
|
||||||
|
export default function SearchBox(props: UseSearchBoxProps) {
|
||||||
|
const { query, refine } = useSearchBox(props);
|
||||||
|
const [inputValue, setInputValue] = useState(query);
|
||||||
|
|
||||||
|
const setQuery = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setInputValue(value);
|
||||||
|
refine(value);
|
||||||
|
},
|
||||||
|
[refine, setInputValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
type="search"
|
||||||
|
leftSection={<TbSearch style={{ width: rem(16), height: rem(16) }} />}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(event) => setQuery(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconMoon, IconRobot, IconSun } from "@tabler/icons-react";
|
import { TbMoon, TbRobot, TbSun } from "react-icons/tb";
|
||||||
|
|
||||||
export function ThemeSetting() {
|
export function ThemeSetting() {
|
||||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||||
@@ -18,7 +18,7 @@ export function ThemeSetting() {
|
|||||||
value: "light",
|
value: "light",
|
||||||
label: (
|
label: (
|
||||||
<Center>
|
<Center>
|
||||||
<IconSun size="1rem" stroke={1.5} />
|
<TbSun size="1rem" />
|
||||||
<Box ml={10}>Light</Box>
|
<Box ml={10}>Light</Box>
|
||||||
</Center>
|
</Center>
|
||||||
),
|
),
|
||||||
@@ -27,7 +27,7 @@ export function ThemeSetting() {
|
|||||||
value: "auto",
|
value: "auto",
|
||||||
label: (
|
label: (
|
||||||
<Center>
|
<Center>
|
||||||
<IconRobot size="1rem" stroke={1.5} />
|
<TbRobot size="1rem" />
|
||||||
<Box ml={10}>Auto</Box>
|
<Box ml={10}>Auto</Box>
|
||||||
</Center>
|
</Center>
|
||||||
),
|
),
|
||||||
@@ -36,7 +36,7 @@ export function ThemeSetting() {
|
|||||||
value: "dark",
|
value: "dark",
|
||||||
label: (
|
label: (
|
||||||
<Center>
|
<Center>
|
||||||
<IconMoon size="1rem" stroke={1.5} />
|
<TbMoon size="1rem" />
|
||||||
<Box ml={10}>Dark</Box>
|
<Box ml={10}>Dark</Box>
|
||||||
</Center>
|
</Center>
|
||||||
),
|
),
|
||||||
9
app/constants.ts
Normal file
9
app/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const SourceLabelMap: Record<string, string> = {
|
||||||
|
tttang: "跳跳糖",
|
||||||
|
secin: "Sec-In",
|
||||||
|
seebug: "Seebug",
|
||||||
|
wechat: "微信公众号",
|
||||||
|
xianzhi: "先知",
|
||||||
|
anquanke: "安全客",
|
||||||
|
freebuf: "FreeBuf",
|
||||||
|
};
|
||||||
14
app/entry.client.tsx
Normal file
14
app/entry.client.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { StrictMode, startTransition } from "react";
|
||||||
|
|
||||||
|
import { hydrateRoot } from "react-dom/client";
|
||||||
|
import { HydratedRouter } from "react-router/dom";
|
||||||
|
|
||||||
|
import "./main.css";
|
||||||
|
startTransition(() => {
|
||||||
|
hydrateRoot(
|
||||||
|
document,
|
||||||
|
<StrictMode>
|
||||||
|
<HydratedRouter />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
});
|
||||||
71
app/entry.server.tsx
Normal file
71
app/entry.server.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { PassThrough } from "node:stream";
|
||||||
|
|
||||||
|
import type { AppLoadContext, EntryContext } from "react-router";
|
||||||
|
import { ServerRouter } from "react-router";
|
||||||
|
|
||||||
|
import { createReadableStreamFromReadable } from "@react-router/node";
|
||||||
|
import { isbot } from "isbot";
|
||||||
|
import type { RenderToPipeableStreamOptions } from "react-dom/server";
|
||||||
|
import { renderToPipeableStream } from "react-dom/server";
|
||||||
|
|
||||||
|
export const streamTimeout = 5_000;
|
||||||
|
|
||||||
|
export default function handleRequest(
|
||||||
|
request: Request,
|
||||||
|
responseStatusCode: number,
|
||||||
|
responseHeaders: Headers,
|
||||||
|
routerContext: EntryContext,
|
||||||
|
_loadContext: AppLoadContext,
|
||||||
|
// If you have middleware enabled:
|
||||||
|
// loadContext: unstable_RouterContextProvider
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let shellRendered = false;
|
||||||
|
const userAgent = request.headers.get("user-agent");
|
||||||
|
|
||||||
|
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
|
||||||
|
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
||||||
|
const readyOption: keyof RenderToPipeableStreamOptions =
|
||||||
|
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
|
||||||
|
? "onAllReady"
|
||||||
|
: "onShellReady";
|
||||||
|
|
||||||
|
const { pipe, abort } = renderToPipeableStream(
|
||||||
|
<ServerRouter context={routerContext} url={request.url} />,
|
||||||
|
{
|
||||||
|
[readyOption]() {
|
||||||
|
shellRendered = true;
|
||||||
|
const body = new PassThrough();
|
||||||
|
const stream = createReadableStreamFromReadable(body);
|
||||||
|
|
||||||
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
new Response(stream, {
|
||||||
|
headers: responseHeaders,
|
||||||
|
status: responseStatusCode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
pipe(body);
|
||||||
|
},
|
||||||
|
onShellError(error: unknown) {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
onError(error: unknown) {
|
||||||
|
responseStatusCode = 500;
|
||||||
|
// Log streaming rendering errors from inside the shell. Don't log
|
||||||
|
// errors encountered during initial shell rendering since they'll
|
||||||
|
// reject and get logged in handleDocumentRequest.
|
||||||
|
if (shellRendered) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Abort the rendering stream after the `streamTimeout` so it has time to
|
||||||
|
// flush down the rejected boundaries
|
||||||
|
setTimeout(abort, streamTimeout + 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
9
app/hooks/useContentFix.ts
Normal file
9
app/hooks/useContentFix.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import useConfigStore from "@/store/config";
|
||||||
|
|
||||||
|
export function useContentFix(content?: string | null) {
|
||||||
|
const s3Url = useConfigStore((store) => store.s3Url);
|
||||||
|
return content?.replace(
|
||||||
|
/https?:\/\/(?:minio-hdd)\.yoshino-s\.(?:online|xyz)\//g,
|
||||||
|
s3Url,
|
||||||
|
);
|
||||||
|
}
|
||||||
129
app/layouts/Main.layout.tsx
Normal file
129
app/layouts/Main.layout.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { Suspense, useEffect, useState } from "react";
|
||||||
|
import { Outlet, useLocation } from "react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AppShell,
|
||||||
|
Avatar,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
UnstyledButton,
|
||||||
|
rem,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useHeadroom } from "@mantine/hooks";
|
||||||
|
import { TbSettings } from "react-icons/tb";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
InstantMeiliSearchInstance,
|
||||||
|
MeiliSearch,
|
||||||
|
} from "@meilisearch/instant-meilisearch";
|
||||||
|
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch";
|
||||||
|
import { singleIndex } from "instantsearch.js/es/lib/stateMappings";
|
||||||
|
import { InstantSearch } from "react-instantsearch";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import ScrollTop from "@/component/ScrollTop/ScrollTop";
|
||||||
|
import SearchBox from "@/component/SearchBox/SearchBox";
|
||||||
|
import Loading from "@/pages/Loading";
|
||||||
|
import useConfigStore from "@/store/config";
|
||||||
|
import { MeilisearchProvider } from "@/utils/meilisearchContext";
|
||||||
|
|
||||||
|
export default function MainLayout() {
|
||||||
|
const pinned = useHeadroom({ fixedAt: 60 });
|
||||||
|
const selector = useConfigStore();
|
||||||
|
|
||||||
|
const path = useLocation().pathname;
|
||||||
|
|
||||||
|
const isSearchPage = path === "/";
|
||||||
|
|
||||||
|
const [searchClient, setSearchClient] =
|
||||||
|
useState<InstantMeiliSearchInstance>();
|
||||||
|
const [meilisearch, setMeilisearch] = useState<MeiliSearch | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { meilisearchUrl, meilisearchToken, enableHybridSearch } = selector;
|
||||||
|
const { searchClient, meiliSearchInstance } = instantMeiliSearch(
|
||||||
|
meilisearchUrl,
|
||||||
|
meilisearchToken,
|
||||||
|
{
|
||||||
|
finitePagination: true,
|
||||||
|
meiliSearchParams: {
|
||||||
|
hybrid: enableHybridSearch
|
||||||
|
? {
|
||||||
|
embedder: "cloudflare",
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
attributesToRetrieve: [
|
||||||
|
"cover",
|
||||||
|
"title",
|
||||||
|
"time",
|
||||||
|
"author",
|
||||||
|
"tags",
|
||||||
|
"id",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setSearchClient(searchClient);
|
||||||
|
setMeilisearch(meiliSearchInstance);
|
||||||
|
}, [selector, setSearchClient]);
|
||||||
|
|
||||||
|
const shell = (
|
||||||
|
<>
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60, collapsed: !pinned, offset: false }}
|
||||||
|
h="100vh"
|
||||||
|
>
|
||||||
|
<AppShell.Header>
|
||||||
|
<Group h="100%" justify="space-between" px="md">
|
||||||
|
<Group>
|
||||||
|
<Avatar fw={700} component={Link} to="/">
|
||||||
|
DS
|
||||||
|
</Avatar>
|
||||||
|
</Group>
|
||||||
|
<Group>
|
||||||
|
{isSearchPage && <SearchBox />}
|
||||||
|
<UnstyledButton component={Link} to="/settings">
|
||||||
|
<Center>
|
||||||
|
<TbSettings />
|
||||||
|
</Center>
|
||||||
|
</UnstyledButton>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
<AppShell.Main
|
||||||
|
pt={`calc(${rem(60)} + var(--mantine-spacing-md))`}
|
||||||
|
pb={rem(60)}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<MeilisearchProvider value={meilisearch}>
|
||||||
|
<Outlet />
|
||||||
|
</MeilisearchProvider>
|
||||||
|
</Suspense>
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
<ScrollTop />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{searchClient &&
|
||||||
|
(isSearchPage ? (
|
||||||
|
<InstantSearch
|
||||||
|
searchClient={searchClient as any}
|
||||||
|
indexName="paragraph"
|
||||||
|
routing={{
|
||||||
|
stateMapping: singleIndex("paragraph"),
|
||||||
|
}}
|
||||||
|
future={{
|
||||||
|
preserveSharedStateOnUnmount: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shell}
|
||||||
|
</InstantSearch>
|
||||||
|
) : (
|
||||||
|
shell
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
app/main.css
Normal file
7
app/main.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@layer mantine-core, mantine-dates, mantine-datatable, mantine-contextmenu, mantine-notifications, mantine-spotlight, mantine-split-pane;
|
||||||
|
@import "@mantine/core/styles.css" layer(mantine-core);
|
||||||
|
@import "@mantine/dates/styles.css" layer(mantine-dates);
|
||||||
|
@import "mantine-datatable/styles.css" layer(mantine-datatable);
|
||||||
|
@import "mantine-contextmenu/styles.css" layer(mantine-contextmenu);
|
||||||
|
@import "@mantine/notifications/styles.css" layer(mantine-notifications);
|
||||||
|
@import "@mantine/spotlight/styles.css" layer(mantine-spotlight);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Container, Group, Text, Title } from "@mantine/core";
|
import { Button, Container, Group, Text, Title } from "@mantine/core";
|
||||||
|
|
||||||
import classes from "./ErrorPage.module.css";
|
import classes from "./ErrorPage.module.css";
|
||||||
|
|
||||||
export interface ErrorPageProps {
|
export interface ErrorPageProps {
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TitleContext } from "@/component/Header/Header";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Container,
|
Container,
|
||||||
@@ -7,12 +8,13 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
TypographyStylesProvider,
|
TypographyStylesProvider,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import { useContext, useEffect } from "react";
|
|
||||||
import { useLoaderData } from "react-router";
|
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import dayjs from "dayjs";
|
||||||
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import useConfigStore from "@/store/config";
|
||||||
|
import { useMeilisearch } from "@/utils/meilisearchContext";
|
||||||
|
import { markdownToHtml } from "@/utils/remark";
|
||||||
|
|
||||||
function stripStyles(content: string) {
|
function stripStyles(content: string) {
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
@@ -52,36 +54,57 @@ function stripStyles(content: string) {
|
|||||||
return element.innerHTML;
|
return element.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
export default function ParagraphPage() {
|
export default function ParagraphPage() {
|
||||||
const [_title, setTitle] = useContext(TitleContext);
|
const { id } = useParams<{ id: string }>();
|
||||||
const paragraph = useLoaderData() as Paragraph;
|
const s3Url = useConfigStore((store) => store.s3Url);
|
||||||
|
|
||||||
|
const meilisearch = useMeilisearch();
|
||||||
|
|
||||||
|
const [paragraph, setParagraph] = useState<Paragraph | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(paragraph.title);
|
async function fetchParagraph() {
|
||||||
}, [setTitle, paragraph.title]);
|
if (!meilisearch || !id) return;
|
||||||
|
const paragraph: Paragraph = await meilisearch
|
||||||
|
.index("paragraph")
|
||||||
|
.getDocument(id);
|
||||||
|
|
||||||
|
if (paragraph.markdown) {
|
||||||
|
paragraph.content = await markdownToHtml(paragraph.content);
|
||||||
|
paragraph.content = stripStyles(paragraph.content)?.replace(
|
||||||
|
/https?:\/\/(?:minio-hdd)\.yoshino-s\.(?:online|xyz)\//g,
|
||||||
|
s3Url,
|
||||||
|
);
|
||||||
|
console.log(paragraph.content);
|
||||||
|
} else {
|
||||||
|
// paragraph.content = "NO HTML!";
|
||||||
|
}
|
||||||
|
|
||||||
|
setParagraph(paragraph);
|
||||||
|
}
|
||||||
|
fetchParagraph();
|
||||||
|
}, [id, meilisearch, s3Url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container py="2rem">
|
<Container py="2rem">
|
||||||
<Title>{paragraph.title}</Title>
|
<Title>{paragraph?.title}</Title>
|
||||||
<Group justify="space-between" align="center" my="md">
|
<Group justify="space-between" align="center" my="md">
|
||||||
<Group>
|
<Group>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{" "}
|
{" "}
|
||||||
{dayjs().to(dayjs(paragraph["@timestamp"]))}
|
{dayjs().to(dayjs(paragraph?.time))}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
ml="1rem"
|
ml="1rem"
|
||||||
size="sm"
|
size="sm"
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/author/${encodeURIComponent(paragraph.author || "unknown")}`}
|
to={`/author/${encodeURIComponent(paragraph?.author || "unknown")}`}
|
||||||
>
|
>
|
||||||
{paragraph.author}
|
{paragraph?.author}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group>
|
||||||
{paragraph.tags.map((tag, index) => (
|
{paragraph?.tags.map((tag, index) => (
|
||||||
<>
|
<>
|
||||||
<Badge
|
<Badge
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -93,8 +116,8 @@ export default function ParagraphPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
{paragraph.source_url && (
|
{paragraph?.source_url && (
|
||||||
<a href={paragraph.source_url}>Goto Source</a>
|
<a href={paragraph?.source_url}>Goto Source</a>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -109,7 +132,7 @@ export default function ParagraphPage() {
|
|||||||
lineBreak: "anywhere",
|
lineBreak: "anywhere",
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: stripStyles(paragraph.content),
|
__html: paragraph?.content ?? "<p>Content not found</p>",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TypographyStylesProvider>
|
</TypographyStylesProvider>
|
||||||
19
app/pages/Search.page.tsx
Normal file
19
app/pages/Search.page.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Container, Grid, Stack } from "@mantine/core";
|
||||||
|
|
||||||
|
import Hits from "@/component/Hits/Hits";
|
||||||
|
import Pagination from "@/component/Pagination/Pagination";
|
||||||
|
import Refinement from "@/component/Refinement/Refinement";
|
||||||
|
|
||||||
|
export default function SearchPage() {
|
||||||
|
return (
|
||||||
|
<Container size="xl">
|
||||||
|
<Stack>
|
||||||
|
<Refinement />
|
||||||
|
<Grid my="md">
|
||||||
|
<Hits />
|
||||||
|
</Grid>
|
||||||
|
<Pagination />
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
220
app/pages/Settings.page.tsx
Normal file
220
app/pages/Settings.page.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
PasswordInput,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
rem,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { TbBrandGithub, TbMail } from "react-icons/tb";
|
||||||
|
|
||||||
|
import { Meilisearch } from "meilisearch";
|
||||||
|
|
||||||
|
import { TitleContext } from "@/component/Header/Header";
|
||||||
|
import { ThemeSetting } from "@/component/Settings/Theme";
|
||||||
|
import useConfigStore from "@/store/config";
|
||||||
|
|
||||||
|
interface SettingItem {
|
||||||
|
title?: ReactNode;
|
||||||
|
description?: ReactNode;
|
||||||
|
value?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const [_, setTitle] = useContext(TitleContext);
|
||||||
|
const {
|
||||||
|
setS3Url,
|
||||||
|
setEnableHybridSearch,
|
||||||
|
setMeilisearchToken,
|
||||||
|
setMeilisearchUrl,
|
||||||
|
...options
|
||||||
|
} = useConfigStore();
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle("Settings");
|
||||||
|
}, [setTitle]);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: options,
|
||||||
|
validateInputOnBlur: true,
|
||||||
|
validateInputOnChange: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [meilisearchVersion, setMeilisearchVersion] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = async (v: typeof options) => {
|
||||||
|
try {
|
||||||
|
new URL(v.s3Url, location.origin);
|
||||||
|
form.clearFieldError("s3Url");
|
||||||
|
} catch (e) {
|
||||||
|
form.setFieldError("s3Url", "Invalid Minio URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new Meilisearch({
|
||||||
|
host: v.meilisearchUrl,
|
||||||
|
apiKey: v.meilisearchToken,
|
||||||
|
});
|
||||||
|
const version = await client.getVersion();
|
||||||
|
setMeilisearchVersion(version.pkgVersion);
|
||||||
|
form.clearFieldError("meilisearchUrl");
|
||||||
|
form.clearFieldError("meilisearchToken");
|
||||||
|
} catch (e) {
|
||||||
|
form.setFieldError("meilisearchUrl", "Invalid Meilisearch URL");
|
||||||
|
form.setFieldError("meilisearchToken", "Invalid Meilisearch Token");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.errors.length !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.s3Url !== options.s3Url) {
|
||||||
|
setS3Url(v.s3Url);
|
||||||
|
}
|
||||||
|
if (v.meilisearchUrl !== options.meilisearchUrl) {
|
||||||
|
setMeilisearchUrl(v.meilisearchUrl);
|
||||||
|
}
|
||||||
|
if (v.meilisearchToken !== options.meilisearchToken) {
|
||||||
|
setMeilisearchToken(v.meilisearchToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings: SettingItem[][] = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Theme",
|
||||||
|
description: "Change the theme of your UI",
|
||||||
|
value: <ThemeSetting />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Enable Hybrid Search",
|
||||||
|
description: "Enable hybrid search for Meilisearch",
|
||||||
|
value: (
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
checked={options.enableHybridSearch}
|
||||||
|
onChange={(value) => {
|
||||||
|
const v = value.currentTarget.checked;
|
||||||
|
setEnableHybridSearch(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Minio URL",
|
||||||
|
description: "The URL of your Minio instance",
|
||||||
|
value: <TextInput {...form.getInputProps("s3Url")} required />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Meilisearch URL",
|
||||||
|
description: "The URL of your Meilisearch instance",
|
||||||
|
value: <TextInput {...form.getInputProps("meilisearchUrl")} required />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Meilisearch Token",
|
||||||
|
description: "The token of your Meilisearch instance",
|
||||||
|
value: (
|
||||||
|
<PasswordInput {...form.getInputProps("meilisearchToken")} required />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <Button type="submit">Test And Save</Button>,
|
||||||
|
value: meilisearchVersion ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: "var(--mantine-color-green-6)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Meilisearch V{meilisearchVersion}
|
||||||
|
</span>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Made With ❤️ By",
|
||||||
|
description: (
|
||||||
|
<a href="https://github.com/yoshino-s">
|
||||||
|
https://github.com/yoshino-s
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
value: (
|
||||||
|
<ActionIcon.Group>
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Gallery"
|
||||||
|
component="a"
|
||||||
|
href="https://github.com/yoshino-s"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<TbBrandGithub style={{ width: rem(20) }} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Settings"
|
||||||
|
component="a"
|
||||||
|
href="mailto:yoshino.prog@gmail.com"
|
||||||
|
>
|
||||||
|
<TbMail style={{ width: rem(20) }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</ActionIcon.Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title mt="lg" order={1}>
|
||||||
|
Settings
|
||||||
|
</Title>
|
||||||
|
Customize the look and feel of your Coder deployment.
|
||||||
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
|
{settings.map((settingItem, index) => (
|
||||||
|
<Paper
|
||||||
|
my="xl"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
style={{ overflow: "hidden" }}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<Table verticalSpacing="lg" striped>
|
||||||
|
<Table.Tbody>
|
||||||
|
{settingItem.map((setting) => (
|
||||||
|
<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>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
app/root.tsx
Normal file
97
app/root.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Links,
|
||||||
|
Meta,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
isRouteErrorResponse,
|
||||||
|
} from "react-router";
|
||||||
|
|
||||||
|
import { MantineProvider } from "@mantine/core";
|
||||||
|
import { useDocumentTitle } from "@mantine/hooks";
|
||||||
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
import { ContextMenuProvider } from "mantine-contextmenu";
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import duration from "dayjs/plugin/duration";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
|
||||||
|
import { theme } from "@/theme";
|
||||||
|
import { type Metadata, MetaContext } from "@/utils/useMetadata";
|
||||||
|
|
||||||
|
import type { Route } from "./+types/root";
|
||||||
|
|
||||||
|
import "dayjs/locale/en";
|
||||||
|
import "dayjs/locale/zh-cn";
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
{import.meta.env.DEV && (
|
||||||
|
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
|
||||||
|
)}
|
||||||
|
</head>
|
||||||
|
<body style={{ colorScheme: "dark" }}>
|
||||||
|
{children}
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [metadata, setMetadata] = useState<Metadata>({
|
||||||
|
title: "Mantine App",
|
||||||
|
});
|
||||||
|
useDocumentTitle(metadata.title ?? "Mantine App");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider theme={theme} withCssVariables>
|
||||||
|
<ContextMenuProvider>
|
||||||
|
<MetaContext.Provider value={[metadata, setMetadata]}>
|
||||||
|
<Notifications />
|
||||||
|
<Outlet />
|
||||||
|
</MetaContext.Provider>
|
||||||
|
</ContextMenuProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
let message = "Oops!";
|
||||||
|
let details = "An unexpected error occurred.";
|
||||||
|
let stack: string | undefined;
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
message = error.status === 404 ? "404" : "Error";
|
||||||
|
details =
|
||||||
|
error.status === 404
|
||||||
|
? "The requested page could not be found."
|
||||||
|
: error.statusText || details;
|
||||||
|
} else if (error && error instanceof Error) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
details = error.message;
|
||||||
|
stack = error.stack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>{message}</h1>
|
||||||
|
<p>{details}</p>
|
||||||
|
{stack && (
|
||||||
|
<pre>
|
||||||
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
app/routes.ts
Normal file
14
app/routes.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {
|
||||||
|
type RouteConfig,
|
||||||
|
index,
|
||||||
|
layout,
|
||||||
|
route,
|
||||||
|
} from "@react-router/dev/routes";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
layout("./layouts/Main.layout.tsx", [
|
||||||
|
index("./pages/Search.page.tsx"),
|
||||||
|
route("settings", "./pages/Settings.page.tsx"),
|
||||||
|
route("paragraph/:id", "./pages/Paragraph.page.tsx"),
|
||||||
|
]),
|
||||||
|
] satisfies RouteConfig;
|
||||||
41
app/store/config.ts
Normal file
41
app/store/config.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { type ExtractState, create } from "zustand";
|
||||||
|
import { combine, devtools, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export type ConfigStore = ExtractState<typeof useConfigStore>;
|
||||||
|
|
||||||
|
const useConfigStore = create(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
combine(
|
||||||
|
{
|
||||||
|
meilisearchUrl: "https://meilisearch.yoshino-s.xyz/",
|
||||||
|
meilisearchToken:
|
||||||
|
"70014cdf1f1fb94b6ed420e11abf2e74e0dfa7bc00ddd77f213599c50bd1e26f",
|
||||||
|
s3Url: "https://minio-hdd.yoshino-s.xyz/",
|
||||||
|
enableHybridSearch: true,
|
||||||
|
},
|
||||||
|
(set) => ({
|
||||||
|
setMeilisearchUrl: (url: string | undefined) =>
|
||||||
|
set((state) => ({
|
||||||
|
meilisearchUrl: url ?? state.meilisearchUrl,
|
||||||
|
})),
|
||||||
|
setMeilisearchToken: (token: string | undefined) =>
|
||||||
|
set((state) => ({
|
||||||
|
meilisearchToken: token ?? state.meilisearchToken,
|
||||||
|
})),
|
||||||
|
setS3Url: (url: string | undefined) =>
|
||||||
|
set((state) => ({
|
||||||
|
s3Url: url ?? state.s3Url,
|
||||||
|
})),
|
||||||
|
setEnableHybridSearch: (enable: boolean | undefined) =>
|
||||||
|
set((state) => ({
|
||||||
|
enableHybridSearch: enable ?? state.enableHybridSearch,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{ name: "ds-pages" },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useConfigStore;
|
||||||
1
app/store/index.ts
Normal file
1
app/store/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./config";
|
||||||
5
app/theme.ts
Normal file
5
app/theme.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createTheme } from "@mantine/core";
|
||||||
|
|
||||||
|
export const theme = createTheme({
|
||||||
|
/** Put your mantine theme override here */
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
declare interface Paragraph {
|
declare interface Paragraph {
|
||||||
_id: string;
|
id: string;
|
||||||
"@timestamp": string;
|
time: string;
|
||||||
content: string;
|
content: string;
|
||||||
markdown: string;
|
markdown: string;
|
||||||
title: string;
|
title: string;
|
||||||
11
app/utils/meilisearchContext.ts
Normal file
11
app/utils/meilisearchContext.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
import type { Meilisearch } from "meilisearch";
|
||||||
|
|
||||||
|
export const MeilisearchContext = createContext<Meilisearch | null>(null);
|
||||||
|
|
||||||
|
export const MeilisearchProvider = MeilisearchContext.Provider;
|
||||||
|
|
||||||
|
export function useMeilisearch() {
|
||||||
|
return useContext(MeilisearchContext);
|
||||||
|
}
|
||||||
17
app/utils/remark.ts
Normal file
17
app/utils/remark.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import rehypeStarryNight from "rehype-starry-night";
|
||||||
|
import rehypeStringify from "rehype-stringify";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkRehype from "remark-rehype";
|
||||||
|
import { unified } from "unified";
|
||||||
|
|
||||||
|
export async function markdownToHtml(markdown: string) {
|
||||||
|
const result = await unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(remarkGfm)
|
||||||
|
.use(rehypeStarryNight)
|
||||||
|
.use(rehypeStringify)
|
||||||
|
.process(markdown);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
27
app/utils/useMetadata.ts
Normal file
27
app/utils/useMetadata.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export interface Metadata {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
rightActions?: React.ReactNode;
|
||||||
|
withTabs?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MetaContext = createContext<
|
||||||
|
[Metadata, Dispatch<SetStateAction<Metadata>>]
|
||||||
|
>([{}, () => {}]);
|
||||||
|
|
||||||
|
export default function useMetadata(): [
|
||||||
|
Metadata,
|
||||||
|
Dispatch<SetStateAction<Metadata>>,
|
||||||
|
] {
|
||||||
|
const [meta, setMeta] = useContext(MetaContext);
|
||||||
|
|
||||||
|
return [meta, setMeta];
|
||||||
|
}
|
||||||
0
src/vite-env.d.ts → app/vite-env.d.ts
vendored
0
src/vite-env.d.ts → app/vite-env.d.ts
vendored
14
default.conf
14
default.conf
@@ -1,14 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
|
|
||||||
autoindex off;
|
|
||||||
|
|
||||||
server_name _;
|
|
||||||
server_tokens off;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
root /static;
|
|
||||||
index index.html;
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
78
eslint.config.js
Normal file
78
eslint.config.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import eslint from "@eslint/js";
|
||||||
|
import * as pluginImportX from "eslint-plugin-import-x";
|
||||||
|
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||||
|
import * as reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import tsEslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tsEslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
tsEslint.configs.recommended,
|
||||||
|
pluginImportX.flatConfigs.recommended,
|
||||||
|
pluginImportX.flatConfigs.typescript,
|
||||||
|
reactHooks.configs["recommended-latest"],
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"prettier/prettier": ["error", {}],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": "error",
|
||||||
|
"import-x/no-named-as-default-member": "off",
|
||||||
|
"import-x/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
pathGroupsExcludedImportTypes: ["builtin"],
|
||||||
|
pathGroups: [
|
||||||
|
{
|
||||||
|
pattern: "@/**",
|
||||||
|
group: "external",
|
||||||
|
position: "after",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "{react,react-router}",
|
||||||
|
group: "external",
|
||||||
|
position: "before",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "{{@mantine,mantine-*,react-icons}/**,mantine-*}",
|
||||||
|
group: "external",
|
||||||
|
position: "before",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "{@connectrpc,@tanstack,@buf,@bufbuild}/**",
|
||||||
|
group: "external",
|
||||||
|
position: "before",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"newlines-between": "always",
|
||||||
|
alphabetize: {
|
||||||
|
order: "asc",
|
||||||
|
},
|
||||||
|
named: {
|
||||||
|
enabled: true,
|
||||||
|
types: "types-first",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ["./tsconfig.eslint.json"],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
".react-router",
|
||||||
|
"app/gen",
|
||||||
|
"build",
|
||||||
|
"output",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
13
index.html
13
index.html
@@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<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>DS-Next</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'jest-environment-jsdom',
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
|
||||||
moduleNameMapper: {
|
|
||||||
'^@test-utils': '<rootDir>/test-utils',
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
'^.+\\.ts?$': 'ts-jest',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
require('@testing-library/jest-dom/extend-expect');
|
|
||||||
|
|
||||||
const { getComputedStyle } = window;
|
|
||||||
window.getComputedStyle = (elt) => getComputedStyle(elt);
|
|
||||||
|
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
|
||||||
writable: true,
|
|
||||||
value: jest.fn().mockImplementation((query) => ({
|
|
||||||
matches: false,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: jest.fn(),
|
|
||||||
removeListener: jest.fn(),
|
|
||||||
addEventListener: jest.fn(),
|
|
||||||
removeEventListener: jest.fn(),
|
|
||||||
dispatchEvent: jest.fn(),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
class ResizeObserver {
|
|
||||||
observe() {}
|
|
||||||
unobserve() {}
|
|
||||||
disconnect() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.ResizeObserver = ResizeObserver;
|
|
||||||
190
package.json
190
package.json
@@ -1,95 +1,121 @@
|
|||||||
{
|
{
|
||||||
"name": "codesecer-ui",
|
"name": "ds-pages",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "react-router dev --watch",
|
||||||
"build": "tsc && vite build",
|
"build": "react-router build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint src",
|
"lint": "pnpm run '/lint:.*/'",
|
||||||
"jest": "jest",
|
"lint:eslint": "eslint . --ext .ts,.tsx --cache",
|
||||||
"jest:watch": "jest --watch",
|
"lint:stylelint": "stylelint '**/*.css' --cache && prettier --check 'app/**/*.css' --tab-width 4",
|
||||||
"test": "pnpm typecheck && pnpm lint",
|
"lint-fix": "pnpm run \"/lint-fix:.*/\"",
|
||||||
"storybook": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006",
|
"lint-fix:eslint": "eslint . --ext .ts,.tsx --fix --cache",
|
||||||
"build-storybook": "build-storybook",
|
"lint-fix:stylelint": "stylelint '**/*.css' --fix --cache && prettier --write 'app/**/*.css' --tab-width 4",
|
||||||
"chromatic": "npx chromatic --project-token=180ac2186305"
|
"vitest": "vitest run",
|
||||||
|
"vitest:watch": "vitest",
|
||||||
|
"test": "pnpm run '/^(typecheck|lint)$/'",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.6.2",
|
"@mantine/core": "^8.1.2",
|
||||||
"@mantine/hooks": "^7.6.2",
|
"@mantine/dates": "^8.1.2",
|
||||||
"@mantine/notifications": "^7.6.2",
|
"@mantine/form": "^8.1.2",
|
||||||
"@reduxjs/toolkit": "^2.2.1",
|
"@mantine/hooks": "^8.1.2",
|
||||||
"@sentry/react": "^7.108.0",
|
"@mantine/modals": "^8.1.2",
|
||||||
"@sentry/vite-plugin": "^2.16.0",
|
"@mantine/notifications": "^8.1.2",
|
||||||
"@tabler/icons-react": "^3.1.0",
|
"@mantine/spotlight": "^8.1.2",
|
||||||
"axios": "^1.6.8",
|
"@meilisearch/instant-meilisearch": "^0.27.0",
|
||||||
"dayjs": "^1.11.10",
|
"@microlink/react-json-view": "^1.26.2",
|
||||||
"i18next": "^23.10.1",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"lodash": "^4.17.21",
|
"@react-router/node": "^7.6.3",
|
||||||
"react": "^18.2.0",
|
"@react-router/serve": "^7.6.3",
|
||||||
"react-dom": "^18.2.0",
|
"@sentry/react": "^9.33.0",
|
||||||
"react-i18next": "^14.1.0",
|
"@sentry/react-router": "^9.33.0",
|
||||||
"react-redux": "^9.1.0",
|
"@tanstack/react-query": "^5.81.5",
|
||||||
"react-router-dom": "^6.22.3",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"remark": "^15.0.1",
|
"@types/sarif": "^2.1.7",
|
||||||
"remark-html": "^16.0.1"
|
"axios": "^1.10.0",
|
||||||
|
"canvas-confetti": "^1.9.3",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"instantsearch.js": "^4.79.1",
|
||||||
|
"isbot": "^5.1.28",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"mantine-contextmenu": "^8.1.1",
|
||||||
|
"mantine-datatable": "^8.1.2",
|
||||||
|
"meilisearch": "^0.51.0",
|
||||||
|
"monaco-editor": "^0.52.2",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-instantsearch": "^7.16.1",
|
||||||
|
"react-router": "^7.6.3",
|
||||||
|
"react-router-dom": "^7.6.3",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rehype-starry-night": "^2.2.0",
|
||||||
|
"rehype-stringify": "^10.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-rehype": "^11.1.2",
|
||||||
|
"unified": "^11.0.5",
|
||||||
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.1",
|
"@eslint/js": "^9.30.0",
|
||||||
"@mantine/form": "^7.6.2",
|
"@react-router/dev": "^7.6.3",
|
||||||
"@storybook/addon-actions": "^8.0.2",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@storybook/addon-essentials": "^8.0.2",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@storybook/addon-interactions": "^8.0.2",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@storybook/addon-links": "^8.0.2",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@storybook/addons": "^7.6.17",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@storybook/api": "^7.6.17",
|
"@types/json-bigint": "^1.0.4",
|
||||||
"@storybook/builder-vite": "^8.0.2",
|
"@types/node": "^24.0.7",
|
||||||
"@storybook/components": "^8.0.2",
|
"@types/react": "^19.1.8",
|
||||||
"@storybook/core-events": "^8.0.2",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@storybook/react": "^8.0.2",
|
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||||
"@storybook/testing-library": "^0.2.2",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"@storybook/theming": "^8.0.2",
|
"eslint": "^9.30.0",
|
||||||
"@testing-library/dom": "^9.3.4",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"@testing-library/react": "^14.2.2",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"eslint-plugin-import-x": "^4.16.1",
|
||||||
"@types/jest": "^29.5.12",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"@types/lodash": "^4.17.0",
|
"eslint-plugin-prettier": "^5.5.1",
|
||||||
"@types/react": "^18.2.67",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"@types/react-dom": "^18.2.22",
|
"eslint-plugin-react-hooks": "^6.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
"husky": "^9.1.7",
|
||||||
"@typescript-eslint/parser": "^7.3.1",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"jsdom": "^26.1.0",
|
||||||
"babel-loader": "^9.1.3",
|
"postcss": "^8.5.6",
|
||||||
"chromatic": "^11.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
"eslint": "^8.57.0",
|
"postcss-preset-mantine": "1.17.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
|
||||||
"eslint-plugin-import": "^2.29.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
|
||||||
"eslint-plugin-react": "^7.34.1",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
|
||||||
"eslint-plugin-storybook": "^0.8.0",
|
|
||||||
"i18next-http-backend": "^2.5.0",
|
|
||||||
"install-peerdeps": "^3.0.3",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
|
||||||
"postcss": "^8.4.37",
|
|
||||||
"postcss-preset-mantine": "^1.13.0",
|
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.6.2",
|
||||||
"react-router": "^6.22.3",
|
"prop-types": "^15.8.1",
|
||||||
"storybook": "^8.0.2",
|
"stylelint": "^16.21.0",
|
||||||
"storybook-dark-mode": "^4.0.1",
|
"stylelint-config-standard-scss": "^15.0.1",
|
||||||
"storybook-react-i18next": "3.0.1",
|
"tw-animate-css": "^1.3.4",
|
||||||
"stylis-plugin-rtl": "^2.1.1",
|
"typescript": "^5.8.3",
|
||||||
"ts-jest": "^29.1.2",
|
"typescript-eslint": "^8.35.0",
|
||||||
"typescript": "^5.4.2",
|
"vite": "^7.0.0",
|
||||||
"vite": "^5.2.0"
|
"vite-plugin-devtools-json": "^0.2.0",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@sentry/cli",
|
||||||
|
"es5-ext",
|
||||||
|
"esbuild",
|
||||||
|
"unrs-resolver"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18787
pnpm-lock.yaml
generated
18787
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
export default {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"postcss-preset-mantine": {},
|
"postcss-preset-mantine": {},
|
||||||
"postcss-simple-vars": {
|
"postcss-simple-vars": {
|
||||||
13
react-router.config.ts
Normal file
13
react-router.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Config } from "@react-router/dev/config";
|
||||||
|
import { sentryOnBuildEnd } from "@sentry/react-router";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
appDirectory: "app",
|
||||||
|
ssr: false,
|
||||||
|
|
||||||
|
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
|
||||||
|
// ...
|
||||||
|
// Call this at the end of the hook
|
||||||
|
await sentryOnBuildEnd({ viteConfig, reactRouterConfig, buildManifest });
|
||||||
|
},
|
||||||
|
} satisfies Config;
|
||||||
13
src/App.tsx
13
src/App.tsx
@@ -1,13 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { RouterProvider } from "react-router-dom";
|
|
||||||
|
|
||||||
import Loading from "./page/Loading";
|
|
||||||
import router from "./router";
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<Loading />}>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { notifications } from "@mantine/notifications";
|
|
||||||
import axios, { AxiosResponse } from "axios";
|
|
||||||
import { merge } from "lodash";
|
|
||||||
|
|
||||||
export interface PaginationParams {
|
|
||||||
skip?: number;
|
|
||||||
take?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
withCredentials: false,
|
|
||||||
auth: {
|
|
||||||
username: "viewer",
|
|
||||||
password: "publicviewer1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
api.interceptors.response.use(
|
|
||||||
(value: AxiosResponse<any, any>) => {
|
|
||||||
if (value.data.error) {
|
|
||||||
notifications.show({
|
|
||||||
title: "API Error on " + value.config.url,
|
|
||||||
message: value.data.error,
|
|
||||||
color: "red",
|
|
||||||
autoClose: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
const value: AxiosResponse<any, any> = error.response;
|
|
||||||
if (value.data.status !== 200) {
|
|
||||||
notifications.show({
|
|
||||||
title: "API Error on " + value.config.url,
|
|
||||||
message: JSON.stringify(value.data),
|
|
||||||
color: "red",
|
|
||||||
autoClose: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export class SearchApi {
|
|
||||||
static async search(
|
|
||||||
baseUrl: string,
|
|
||||||
query: ZincQueryForSDK,
|
|
||||||
): Promise<SearchResponse> {
|
|
||||||
const { data } = await api.post(
|
|
||||||
new URL("/api/paragraph/_search", baseUrl).toString(),
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
static wrapParagraph(s3Url: string, paragraph: Paragraph) {
|
|
||||||
const RE = /https:\/\/s3\.yoshino-s\.xyz/g;
|
|
||||||
if (paragraph.cover) {
|
|
||||||
paragraph.cover = paragraph.cover.replace(RE, s3Url);
|
|
||||||
}
|
|
||||||
paragraph.content = paragraph.content?.replace(RE, s3Url);
|
|
||||||
|
|
||||||
return paragraph;
|
|
||||||
}
|
|
||||||
static async getParagraph(baseUrl: string, id: string) {
|
|
||||||
const { data } = await api.get(
|
|
||||||
new URL(`/api/paragraph/_doc/${id}`, baseUrl).toString(),
|
|
||||||
);
|
|
||||||
return merge(data._source, {
|
|
||||||
_id: data._id,
|
|
||||||
"@timestamp": data["@timestamp"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { Pagination } from "@mantine/core";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useOptionsState } from "@/store/module/options";
|
|
||||||
import { useDebounceCallback, useMediaQuery } from "@mantine/hooks";
|
|
||||||
import { merge } from "lodash";
|
|
||||||
import { SearchApi } from "./api";
|
|
||||||
|
|
||||||
export function usePaginationData<T>(query: ZincQueryForSDK) {
|
|
||||||
const [params, setParams] = useSearchParams({
|
|
||||||
page: "1",
|
|
||||||
size: "10",
|
|
||||||
});
|
|
||||||
const { state: options } = useOptionsState();
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [data, setData] = useState<T[]>([]);
|
|
||||||
const [take, _] = useState(parseInt(params.get("size") || "10"));
|
|
||||||
const [page, setPage] = useState(parseInt(params.get("page") || "1"));
|
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
||||||
|
|
||||||
const update = useDebounceCallback(async function update() {
|
|
||||||
console.log("query", query, page, take, options);
|
|
||||||
const resp = await SearchApi.search(
|
|
||||||
options.zincsearchUrl,
|
|
||||||
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(() => {
|
|
||||||
setParams({
|
|
||||||
size: take.toString(),
|
|
||||||
page: page.toString(),
|
|
||||||
});
|
|
||||||
}, [take, page, setParams]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
page,
|
|
||||||
pagination: (
|
|
||||||
<Pagination
|
|
||||||
size={isMobile ? "sm" : "md"}
|
|
||||||
total={Math.ceil(total / take)}
|
|
||||||
onChange={setPage}
|
|
||||||
value={page}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import {
|
|
||||||
Affix,
|
|
||||||
AppShell,
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Center,
|
|
||||||
Group,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Transition,
|
|
||||||
UnstyledButton,
|
|
||||||
rem,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { Suspense, useCallback, useState } from "react";
|
|
||||||
import { Outlet, useNavigate } from "react-router";
|
|
||||||
|
|
||||||
import { TitleContext } from "@/component/Header/Header";
|
|
||||||
import Loading from "@/page/Loading";
|
|
||||||
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import { useHeadroom, useMediaQuery, useWindowScroll } from "@mantine/hooks";
|
|
||||||
import { IconArrowUp, IconSearch, IconSettings } from "@tabler/icons-react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function MainLayout() {
|
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
const pinned = useHeadroom({ fixedAt: 60 });
|
|
||||||
|
|
||||||
const [scroll, scrollTo] = useWindowScroll();
|
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
search: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const search = useCallback(
|
|
||||||
function submit({ search }: { search: string }) {
|
|
||||||
console.log(search);
|
|
||||||
navigate(`/search/${encodeURIComponent(search)}`);
|
|
||||||
},
|
|
||||||
[navigate],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TitleContext.Provider value={[title, setTitle]}>
|
|
||||||
<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>
|
|
||||||
{!isMobile && (
|
|
||||||
<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">
|
|
||||||
<Center>
|
|
||||||
<IconSettings />
|
|
||||||
</Center>
|
|
||||||
</UnstyledButton>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
<AppShell.Main pt={`calc(${rem(60)} + var(--mantine-spacing-md))`}>
|
|
||||||
<Suspense fallback={<Loading />}>
|
|
||||||
<Outlet />
|
|
||||||
</Suspense>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
23
src/main.tsx
23
src/main.tsx
@@ -1,23 +0,0 @@
|
|||||||
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 "./sentry";
|
|
||||||
import store from "./store";
|
|
||||||
|
|
||||||
import "@mantine/core/styles.css";
|
|
||||||
|
|
||||||
const theme = createTheme({
|
|
||||||
/** Put your mantine theme override here */
|
|
||||||
});
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<MantineProvider withCssVariables theme={theme}>
|
|
||||||
<Notifications />
|
|
||||||
<App />
|
|
||||||
</MantineProvider>
|
|
||||||
</Provider>,
|
|
||||||
);
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Container, Grid, Group } from "@mantine/core";
|
|
||||||
import { useContext, useEffect } from "react";
|
|
||||||
import { useLoaderData, useLocation, useParams } from "react-router";
|
|
||||||
|
|
||||||
import { ParagraphCard } from "../component/ParagraphCard/ParagraphCard";
|
|
||||||
|
|
||||||
import { TitleContext } from "@/component/Header/Header";
|
|
||||||
import { usePaginationData } from "@/helper/hooks";
|
|
||||||
|
|
||||||
export default function SearchPage() {
|
|
||||||
const [_title, setTitle] = useContext(TitleContext);
|
|
||||||
|
|
||||||
const params = useLoaderData() as ZincQueryForSDK;
|
|
||||||
|
|
||||||
const {
|
|
||||||
page,
|
|
||||||
pagination,
|
|
||||||
data: paragraphs,
|
|
||||||
} = usePaginationData<Paragraph>(params);
|
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
const param = useParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let action = "Index";
|
|
||||||
if (location.pathname.startsWith("/search/")) {
|
|
||||||
action = "Search";
|
|
||||||
} else if (location.pathname.startsWith("/tag/")) {
|
|
||||||
action = `Tag ${param.tag}`;
|
|
||||||
} else if (location.pathname.startsWith("/author/")) {
|
|
||||||
action = `Author ${param.author}`;
|
|
||||||
}
|
|
||||||
const title = `${action} Page ${page}`;
|
|
||||||
setTitle(title);
|
|
||||||
}, [page, location, param, setTitle]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Grid my="md">
|
|
||||||
{paragraphs.map((paragraph) => {
|
|
||||||
return (
|
|
||||||
<Grid.Col span={{ base: 12, sm: 6 }} key={paragraph._id}>
|
|
||||||
<ParagraphCard {...paragraph} key={`${paragraph._id}_card`} />
|
|
||||||
</Grid.Col>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Grid>
|
|
||||||
<Group justify="center">{pagination}</Group>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { Container, Paper, Table, Text, TextInput, Title } from "@mantine/core";
|
|
||||||
import { ReactNode, useContext, useEffect } from "react";
|
|
||||||
|
|
||||||
import { TitleContext } from "@/component/Header/Header";
|
|
||||||
import { ThemeSetting } from "@/component/Settings/Theme";
|
|
||||||
import store from "@/store";
|
|
||||||
import { useOptionsState } from "@/store/module/options";
|
|
||||||
import { setS3Url, setZincsearchUrl } from "@/store/reducer/options";
|
|
||||||
|
|
||||||
interface SettingItem {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
value: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
|
||||||
const [_, setTitle] = useContext(TitleContext);
|
|
||||||
const { state: options } = useOptionsState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTitle("Settings");
|
|
||||||
}, [setTitle]);
|
|
||||||
|
|
||||||
const settings: SettingItem[] = [
|
|
||||||
{
|
|
||||||
title: "Theme",
|
|
||||||
description: "Change the theme of your UI",
|
|
||||||
value: <ThemeSetting />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Minio URL",
|
|
||||||
description: "The URL of your Minio instance",
|
|
||||||
value: (
|
|
||||||
<TextInput
|
|
||||||
value={options.s3Url}
|
|
||||||
onChange={(e) => {
|
|
||||||
store.dispatch(setS3Url(e.currentTarget.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Zincsearch URL",
|
|
||||||
description: "The URL of your Zincsearch instance",
|
|
||||||
value: (
|
|
||||||
<TextInput
|
|
||||||
value={options.zincsearchUrl}
|
|
||||||
onChange={(e) => {
|
|
||||||
store.dispatch(setZincsearchUrl(e.currentTarget.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Made By",
|
|
||||||
description: "Yoshino-s",
|
|
||||||
value: (
|
|
||||||
<a href="https://github.com/yoshino-s">https://github.com/yoshino-s</a>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Title mt="lg" order={1}>
|
|
||||||
Settings
|
|
||||||
</Title>
|
|
||||||
Customize the look and feel of your Coder deployment.
|
|
||||||
<Paper my="xl" radius="md" withBorder style={{ overflow: "hidden" }}>
|
|
||||||
<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) => (
|
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { lazy } from "react";
|
|
||||||
import { createHashRouter } from "react-router-dom";
|
|
||||||
import { remark } from "remark";
|
|
||||||
import remarkHtml from "remark-html";
|
|
||||||
|
|
||||||
import { SearchApi } from "@/helper/api";
|
|
||||||
import MainLayout from "@/layout/MainLayout";
|
|
||||||
import SearchPage from "@/page/Search";
|
|
||||||
import store from "@/store";
|
|
||||||
|
|
||||||
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"));
|
|
||||||
|
|
||||||
const router = createHashRouter([
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <MainLayout />,
|
|
||||||
errorElement: <ErrorPage />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <SearchPage />,
|
|
||||||
loader() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/settings",
|
|
||||||
element: <SettingsPage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/tag/:tag",
|
|
||||||
element: <SearchPage />,
|
|
||||||
loader({ params: { tag } }) {
|
|
||||||
if (!tag) {
|
|
||||||
return { redirect: "/" };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
search_type: "querystring",
|
|
||||||
query: {
|
|
||||||
term: `tags:${JSON.stringify(tag)}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/author/:author",
|
|
||||||
element: <SearchPage />,
|
|
||||||
loader({ params: { author } }) {
|
|
||||||
if (!author) {
|
|
||||||
return { redirect: "/" };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
search_type: "querystring",
|
|
||||||
query: {
|
|
||||||
term: `author:${JSON.stringify(author)}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/search/:search",
|
|
||||||
element: <SearchPage />,
|
|
||||||
loader({ params: { search } }) {
|
|
||||||
if (!search) {
|
|
||||||
return { redirect: "/" };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
search_type: "querystring",
|
|
||||||
query: {
|
|
||||||
term: search,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/paragraph/:id",
|
|
||||||
element: <ParagraphPage />,
|
|
||||||
async loader({ params: { id } }) {
|
|
||||||
if (!id) {
|
|
||||||
return { redirect: "/" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const paragraph = await SearchApi.getParagraph(
|
|
||||||
store.getState().options.zincsearchUrl,
|
|
||||||
id,
|
|
||||||
).then((p) =>
|
|
||||||
SearchApi.wrapParagraph(store.getState().options.s3Url, p),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(paragraph.markdown);
|
|
||||||
if (paragraph.markdown) {
|
|
||||||
paragraph.content = (
|
|
||||||
await remark().use(remarkHtml).process(paragraph.content)
|
|
||||||
).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return paragraph;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/loading",
|
|
||||||
element: <LoadingPage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "*",
|
|
||||||
element: <NotFound />,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import * as Sentry from "@sentry/react";
|
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: "https://fee4fec58516c464f60613b40b5d3a7d@sentry.yoshino-s.xyz/2",
|
|
||||||
integrations: [
|
|
||||||
Sentry.browserProfilingIntegration(),
|
|
||||||
Sentry.browserTracingIntegration(),
|
|
||||||
Sentry.replayIntegration({
|
|
||||||
maskAllText: false,
|
|
||||||
blockAllMedia: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
// Performance Monitoring
|
|
||||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
|
||||||
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
|
|
||||||
tracePropagationTargets: ["localhost", "ds.pages.yoshino-s.xyz"],
|
|
||||||
// Session Replay
|
|
||||||
replaysSessionSampleRate: 1.0, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
|
||||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
|
||||||
|
|
||||||
profilesSampleRate: 1.0, // Capture 100% of the profiles
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { configureStore, Middleware } from "@reduxjs/toolkit";
|
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
|
||||||
|
|
||||||
import optionsReducer from "./reducer/options";
|
|
||||||
|
|
||||||
const localStorageMiddleware: Middleware = ({ getState }) => {
|
|
||||||
return (next) => (action) => {
|
|
||||||
const result = next(action);
|
|
||||||
console.log(result);
|
|
||||||
localStorage.setItem("applicationState", JSON.stringify(getState()));
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const reHydrateStore = () => {
|
|
||||||
if (localStorage.getItem("applicationState") !== null) {
|
|
||||||
return JSON.parse(localStorage.getItem("applicationState") ?? "{}"); // re-hydrate the store
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
options: optionsReducer,
|
|
||||||
},
|
|
||||||
middleware: (getDefaultMiddleware) =>
|
|
||||||
getDefaultMiddleware().concat(localStorageMiddleware),
|
|
||||||
preloadedState: reHydrateStore() as () => any,
|
|
||||||
});
|
|
||||||
|
|
||||||
type AppState = ReturnType<typeof store.getState>;
|
|
||||||
type AppDispatch = typeof store.dispatch;
|
|
||||||
|
|
||||||
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
|
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
||||||
|
|
||||||
export default store;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import store, { useAppSelector } from "..";
|
|
||||||
|
|
||||||
export const useOptionsState = () => {
|
|
||||||
const state = useAppSelector((state) => state.options);
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
getState: () => {
|
|
||||||
return store.getState().options;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
export interface OptionsState {
|
|
||||||
zincsearchUrl: string;
|
|
||||||
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: ZINCSEARCH_URL,
|
|
||||||
s3Url: MINIO_URL,
|
|
||||||
} as OptionsState,
|
|
||||||
reducers: {
|
|
||||||
setZincsearchUrl: (state, action: PayloadAction<string | undefined>) => {
|
|
||||||
state.zincsearchUrl = action.payload ?? ZINCSEARCH_URL;
|
|
||||||
},
|
|
||||||
setS3Url: (state, action: PayloadAction<string | undefined>) => {
|
|
||||||
state.s3Url = action.payload ?? MINIO_URL;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setS3Url, setZincsearchUrl } = optionsSlice.actions;
|
|
||||||
|
|
||||||
export default optionsSlice.reducer;
|
|
||||||
16
tsconfig.eslint.json
Normal file
16
tsconfig.eslint.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
".storybook/**/*",
|
||||||
|
"app",
|
||||||
|
"test-utils",
|
||||||
|
"vitest",
|
||||||
|
"*.js",
|
||||||
|
"*.cjs",
|
||||||
|
"*.mts",
|
||||||
|
"*.ts",
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -1,24 +1,30 @@
|
|||||||
{
|
{
|
||||||
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
"**/.server/**/*",
|
||||||
|
"**/.client/**/*",
|
||||||
|
".react-router/types/**/*"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
"useDefineForClassFields": true,
|
"types": ["node", "vite/client"],
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"target": "ES2022",
|
||||||
"allowJs": false,
|
"module": "ES2022",
|
||||||
"skipLibCheck": true,
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": false,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"rootDirs": [".", "./.react-router/types"],
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@test-utils": ["./test-utils"],
|
"@/*": ["./app/*"]
|
||||||
"@/*": ["./src/*"]
|
},
|
||||||
}
|
"incremental": true,
|
||||||
},
|
"experimentalDecorators": true,
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"emitDecoratorMetadata": true,
|
||||||
}
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node"
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"],
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,40 @@
|
|||||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
import { reactRouter } from "@react-router/dev/vite";
|
||||||
import { resolve } from "path";
|
import {
|
||||||
|
type SentryReactRouterBuildOptions,
|
||||||
import react from "@vitejs/plugin-react";
|
sentryReactRouter,
|
||||||
|
} from "@sentry/react-router";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import devtoolsJson from "vite-plugin-devtools-json";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
|
const sentryConfig: SentryReactRouterBuildOptions = {
|
||||||
|
telemetry: false,
|
||||||
|
project: "devsecops_ssl-op-demo-new",
|
||||||
|
sourceMapsUploadOptions: {
|
||||||
|
filesToDeleteAfterUpload: [],
|
||||||
|
},
|
||||||
|
// An auth token is required for uploading source maps.
|
||||||
|
authToken:
|
||||||
|
"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",
|
||||||
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
appType: "spa",
|
||||||
server: {
|
server: {
|
||||||
headers: {
|
cors: true,
|
||||||
"Document-Policy": "js-profiling",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
reactRouter(),
|
||||||
sentryVitePlugin({
|
devtoolsJson(),
|
||||||
org: "sentry",
|
tsconfigPaths(),
|
||||||
project: "ds-viewer",
|
sentryReactRouter(sentryConfig, {
|
||||||
url: "https://sentry.yoshino-s.xyz",
|
command: "build",
|
||||||
|
mode: "production",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@/": `${resolve(__dirname, "src")}/`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
assetsDir: "static",
|
||||||
|
manifest: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user