@bbki.ng/site 5.5.19 → 5.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/blog/app.tsx +18 -54
- package/src/blog/components/BaseLayout.tsx +55 -0
- package/src/blog/components/effect-layer/EffectContextProvider.tsx +3 -8
- package/src/blog/components/index.tsx +2 -4
- package/src/blog/context/global_loading_state_provider.tsx +3 -9
- package/src/blog/hooks/use_loading.ts +1 -1
- package/src/blog/hooks/use_plugin_entries.ts +48 -0
- package/src/blog/hooks/use_posts.ts +16 -9
- package/src/blog/index.tsx +1 -0
- package/src/blog/pages/cover/index.tsx +33 -24
- package/src/blog/pages/extensions/txt/article.tsx +6 -3
- package/src/blog/pages/extensions/txt/index.tsx +29 -25
- package/src/blog/swr.tsx +3 -2
- package/src/blog/types/path.ts +0 -7
- package/src/blog/utils/index.ts +0 -21
- package/src/core/context/createPluginCtx.tsx +12 -7
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/useMiddlewareTransData.ts +32 -46
- package/src/core/hooks/useSlotComp.ts +7 -3
- package/src/core/hooks/use_plugins.ts +16 -5
- package/src/core/pluginManager.ts +1 -1
- package/src/core/registry.ts +20 -0
- package/src/plugins/extra-entry/components/page.tsx +14 -0
- package/src/plugins/extra-entry/index.ts +32 -0
- package/src/plugins/manifest.ts +6 -0
- package/src/plugins/xwy/const/index.ts +1 -1
- package/src/types/plugin.ts +11 -1
- package/src/types/posts.ts +9 -0
- package/src/types/slots.ts +8 -2
- package/src/utils/index.tsx +52 -0
- package/src/blog/components/Auth.tsx +0 -13
- package/src/blog/components/DelayFadeIn/DelayFadeIn.tsx +0 -28
- package/src/blog/components/Spinner.tsx +0 -10
- package/src/blog/components/my_suspense.tsx +0 -11
- package/src/blog/components/share/share-btn.tsx +0 -28
- package/src/blog/components/share/share-icon.tsx +0 -19
- package/src/blog/hooks/use_font_loading.ts +0 -41
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/blog/app.tsx
CHANGED
|
@@ -1,75 +1,32 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { Outlet, Route, Routes
|
|
3
|
-
import {
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Outlet, Route, Routes } from 'react-router-dom';
|
|
3
|
+
import { NotFound } from '@bbki.ng/ui';
|
|
4
4
|
|
|
5
|
-
import { usePaths } from '@/hooks';
|
|
6
5
|
import ArticlePage from '@/pages/extensions/txt/article';
|
|
7
6
|
import Txt from '@/pages/extensions/txt';
|
|
8
|
-
import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
9
7
|
import { BotRedirect } from '@/pages/bot';
|
|
10
8
|
import { BBContext } from '@/context/bbcontext';
|
|
11
9
|
import { Slot } from '#/core/components/SlotComp';
|
|
12
10
|
import { usePlugins } from '#/core/hooks/use_plugins';
|
|
13
11
|
import { SWR } from '@/swr';
|
|
12
|
+
import type { PluginID } from '#/types/plugin';
|
|
14
13
|
|
|
15
14
|
import { Cover, Streaming } from './pages';
|
|
15
|
+
import { usePluginEntries } from './hooks/use_plugin_entries';
|
|
16
|
+
import { BaseLayout } from './components/BaseLayout';
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
const paths = usePaths();
|
|
19
|
-
const { isLoading } = useContext(GlobalLoadingContext);
|
|
20
|
-
|
|
21
|
-
const nav = useNavigate();
|
|
22
|
-
|
|
23
|
-
const defaultLogo = (
|
|
24
|
-
<Logo className="mr-2 cursor-pointer hover:opacity-80" onClick={() => nav('/')} />
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<Page
|
|
29
|
-
nav={
|
|
30
|
-
<Nav
|
|
31
|
-
paths={paths}
|
|
32
|
-
className="gradient-blur-cover select-none"
|
|
33
|
-
loading={isLoading}
|
|
34
|
-
customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
|
|
35
|
-
style={{
|
|
36
|
-
paddingTop: 'var(--safe-top)',
|
|
37
|
-
transition: 'all .2s ease-in-out',
|
|
38
|
-
}}
|
|
39
|
-
/>
|
|
40
|
-
}
|
|
41
|
-
main={
|
|
42
|
-
<Grid
|
|
43
|
-
leftAside={
|
|
44
|
-
<div className="py-32 px-6">
|
|
45
|
-
<Slot name="leftCol" data={paths} />
|
|
46
|
-
</div>
|
|
47
|
-
}
|
|
48
|
-
rightAside={
|
|
49
|
-
<div className="py-32 px-6">
|
|
50
|
-
<Slot name="rightCol" data={paths} />
|
|
51
|
-
</div>
|
|
52
|
-
}
|
|
53
|
-
>
|
|
54
|
-
<Container className="py-32">
|
|
55
|
-
<ErrorBoundary>
|
|
56
|
-
<Outlet />
|
|
57
|
-
</ErrorBoundary>
|
|
58
|
-
</Container>
|
|
59
|
-
</Grid>
|
|
60
|
-
}
|
|
61
|
-
/>
|
|
62
|
-
);
|
|
63
|
-
};
|
|
18
|
+
const APP_PLUGIN_IDS: Array<PluginID> = ['sticker', 'xwy', 'extra-cd' /*'extra-entry'*/];
|
|
64
19
|
|
|
65
20
|
export const App = () => {
|
|
66
|
-
usePlugins(
|
|
21
|
+
usePlugins(APP_PLUGIN_IDS);
|
|
22
|
+
|
|
23
|
+
const pluginEntries = usePluginEntries();
|
|
67
24
|
|
|
68
25
|
return (
|
|
69
26
|
<SWR>
|
|
70
27
|
<BBContext>
|
|
71
28
|
<Routes>
|
|
72
|
-
<Route path="/" element={<
|
|
29
|
+
<Route path="/" element={<BaseLayout />}>
|
|
73
30
|
<Route index element={<Cover />} />
|
|
74
31
|
|
|
75
32
|
<Route path="blog" element={<Outlet />}>
|
|
@@ -79,6 +36,13 @@ export const App = () => {
|
|
|
79
36
|
|
|
80
37
|
<Route path="bot" element={<BotRedirect />} />
|
|
81
38
|
<Route path="now" element={<Streaming />} />
|
|
39
|
+
{pluginEntries?.map(route => (
|
|
40
|
+
<Route
|
|
41
|
+
key={route.path}
|
|
42
|
+
path={route.path}
|
|
43
|
+
element={<Slot name="route" data={route} />}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
82
46
|
</Route>
|
|
83
47
|
<Route path="*" element={<NotFound />} />
|
|
84
48
|
</Routes>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { useNavigate, Outlet } from 'react-router-dom';
|
|
3
|
+
import { Logo, Nav, Page, Grid, ErrorBoundary, Container } from '@bbki.ng/ui';
|
|
4
|
+
|
|
5
|
+
import { Slot } from '#/core/components/SlotComp';
|
|
6
|
+
import { usePaths } from '@/hooks';
|
|
7
|
+
import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
8
|
+
|
|
9
|
+
export const BaseLayout = () => {
|
|
10
|
+
const paths = usePaths();
|
|
11
|
+
const { isLoading } = useContext(GlobalLoadingContext);
|
|
12
|
+
|
|
13
|
+
const nav = useNavigate();
|
|
14
|
+
|
|
15
|
+
const defaultLogo = (
|
|
16
|
+
<Logo className="mr-2 cursor-pointer hover:opacity-80" onClick={() => nav('/')} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Page
|
|
21
|
+
nav={
|
|
22
|
+
<Nav
|
|
23
|
+
paths={paths}
|
|
24
|
+
className="gradient-blur-cover select-none"
|
|
25
|
+
loading={isLoading}
|
|
26
|
+
customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
|
|
27
|
+
style={{
|
|
28
|
+
paddingTop: 'var(--safe-top)',
|
|
29
|
+
transition: 'all .2s ease-in-out',
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
}
|
|
33
|
+
main={
|
|
34
|
+
<Grid
|
|
35
|
+
leftAside={
|
|
36
|
+
<div className="py-32 px-6">
|
|
37
|
+
<Slot name="leftCol" data={paths} />
|
|
38
|
+
</div>
|
|
39
|
+
}
|
|
40
|
+
rightAside={
|
|
41
|
+
<div className="py-32 px-6">
|
|
42
|
+
<Slot name="rightCol" data={paths} />
|
|
43
|
+
</div>
|
|
44
|
+
}
|
|
45
|
+
>
|
|
46
|
+
<Container className="py-32">
|
|
47
|
+
<ErrorBoundary>
|
|
48
|
+
<Outlet />
|
|
49
|
+
</ErrorBoundary>
|
|
50
|
+
</Container>
|
|
51
|
+
</Grid>
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -7,7 +7,7 @@ import { useFingerprint } from '@/hooks/use_fingerprint';
|
|
|
7
7
|
const hashStr: string = typeof GLOBAL_COMMIT_HASH === 'string' ? GLOBAL_COMMIT_HASH : '0000000';
|
|
8
8
|
|
|
9
9
|
export const EffectContextProvider = (props: { children: ReactNode }) => {
|
|
10
|
-
const { isLoading
|
|
10
|
+
const { isLoading } = useContext(GlobalLoadingContext);
|
|
11
11
|
const { deviceId } = useFingerprint();
|
|
12
12
|
|
|
13
13
|
const effects: Effect[] = useMemo(() => {
|
|
@@ -15,13 +15,8 @@ export const EffectContextProvider = (props: { children: ReactNode }) => {
|
|
|
15
15
|
if (deviceId) wmLines.push(deviceId);
|
|
16
16
|
wmLines.push('hello world');
|
|
17
17
|
|
|
18
|
-
return [
|
|
19
|
-
|
|
20
|
-
paper(),
|
|
21
|
-
spiral({ active: isLoading || isFontLoading }),
|
|
22
|
-
watermark({ lines: wmLines }),
|
|
23
|
-
];
|
|
24
|
-
}, [isLoading, isFontLoading, deviceId]);
|
|
18
|
+
return [grain(), paper(), spiral({ active: isLoading }), watermark({ lines: wmLines })];
|
|
19
|
+
}, [isLoading, deviceId]);
|
|
25
20
|
|
|
26
21
|
return (
|
|
27
22
|
<>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { LinkList } from '@bbki.ng/ui';
|
|
2
|
+
import { LinkList, LinkListProps } from '@bbki.ng/ui';
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
export const CenterLinkList = (props: any) => {
|
|
4
|
+
export const CenterLinkList = (props: LinkListProps) => {
|
|
7
5
|
return (
|
|
8
6
|
<div className="flex justify-center relative h-full">
|
|
9
7
|
<LinkList {...props} />
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import React, { createContext, ReactNode, useState, useCallback, useMemo } from 'react';
|
|
2
2
|
|
|
3
|
-
import { useFontLoading } from '@/hooks/use_font_loading';
|
|
4
|
-
|
|
5
3
|
type LoadingStates = Map<string, boolean>;
|
|
6
4
|
|
|
7
5
|
type LoadingContext = {
|
|
8
6
|
isLoading: boolean;
|
|
9
|
-
isFontLoading: boolean;
|
|
10
7
|
register: (id: string) => void;
|
|
11
8
|
setLoading: (id: string, loading: boolean) => void;
|
|
12
9
|
unregister: (id: string) => void;
|
|
13
10
|
};
|
|
14
11
|
|
|
15
12
|
export const GlobalLoadingContext = createContext<LoadingContext>({
|
|
16
|
-
isFontLoading: false,
|
|
17
13
|
isLoading: false,
|
|
18
14
|
register: () => {},
|
|
19
15
|
setLoading: () => {},
|
|
@@ -22,7 +18,6 @@ export const GlobalLoadingContext = createContext<LoadingContext>({
|
|
|
22
18
|
|
|
23
19
|
export const GlobalLoadingStateProvider = (props: { children: ReactNode }) => {
|
|
24
20
|
const [loadingStates, setLoadingStates] = useState<LoadingStates>(new Map());
|
|
25
|
-
const isFontLoading = useFontLoading();
|
|
26
21
|
|
|
27
22
|
const register = useCallback((id: string) => {
|
|
28
23
|
setLoadingStates(prev => {
|
|
@@ -49,18 +44,17 @@ export const GlobalLoadingStateProvider = (props: { children: ReactNode }) => {
|
|
|
49
44
|
}, []);
|
|
50
45
|
|
|
51
46
|
const isLoading = useMemo(() => {
|
|
52
|
-
return Array.from(loadingStates.values()).some(Boolean)
|
|
53
|
-
}, [loadingStates
|
|
47
|
+
return Array.from(loadingStates.values()).some(Boolean);
|
|
48
|
+
}, [loadingStates]);
|
|
54
49
|
|
|
55
50
|
const contextValue = useMemo(
|
|
56
51
|
() => ({
|
|
57
52
|
isLoading,
|
|
58
|
-
isFontLoading,
|
|
59
53
|
register,
|
|
60
54
|
setLoading,
|
|
61
55
|
unregister,
|
|
62
56
|
}),
|
|
63
|
-
[isLoading,
|
|
57
|
+
[isLoading, register, setLoading, unregister]
|
|
64
58
|
);
|
|
65
59
|
|
|
66
60
|
return (
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { type PathRouteProps } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
import { useMiddlewareRunner } from '#/core/hooks';
|
|
5
|
+
|
|
6
|
+
const protectedRoutesSet = new Set<string>([
|
|
7
|
+
'/bot',
|
|
8
|
+
'/now',
|
|
9
|
+
'/',
|
|
10
|
+
'/blog',
|
|
11
|
+
'/blog/:title',
|
|
12
|
+
'default',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
type PluginRoute = Omit<PathRouteProps, 'element'>;
|
|
16
|
+
|
|
17
|
+
export const usePluginEntries = () => {
|
|
18
|
+
const [fullRoutes, setFullRoutes] = useState<Array<PluginRoute>>([]);
|
|
19
|
+
const runRef = useRef<(input: Array<PluginRoute>) => Promise<Array<PluginRoute>>>(() =>
|
|
20
|
+
Promise.resolve([])
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const onMiddlewareChange = useCallback(() => {
|
|
24
|
+
runRef.current([]).then(setFullRoutes);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const { run } = useMiddlewareRunner<Array<PluginRoute>>({
|
|
28
|
+
hookPoint: 'extendedRoutes',
|
|
29
|
+
onMiddlewareChange,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
runRef.current = run;
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
run([]).then(setFullRoutes);
|
|
36
|
+
}, [run]);
|
|
37
|
+
|
|
38
|
+
const safeRoutes = fullRoutes?.filter(route => {
|
|
39
|
+
if ('path' in route) {
|
|
40
|
+
return !protectedRoutesSet.has(route.path);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}) as Array<PluginRoute>;
|
|
44
|
+
|
|
45
|
+
console.log('safe plugin routes:', safeRoutes);
|
|
46
|
+
|
|
47
|
+
return safeRoutes ?? [];
|
|
48
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
|
-
import { useMemo } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import { useGlobalLoading } from '@/hooks';
|
|
5
5
|
import { baseFetcher } from '@/utils';
|
|
6
6
|
import { API_ENDPOINT } from '@/constants/routes';
|
|
7
|
-
import {
|
|
7
|
+
import { IPost } from '#/types/posts';
|
|
8
|
+
import { useMiddlewareRunner } from '#/core/hooks/useMiddlewareTransData';
|
|
8
9
|
|
|
9
10
|
const isProd = true;
|
|
10
11
|
const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
|
|
@@ -28,34 +29,40 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
|
28
29
|
const baseTitleList: TitleListItem[] = useMemo(() => {
|
|
29
30
|
if (!data || swrError) return [];
|
|
30
31
|
|
|
31
|
-
return data.map((p:
|
|
32
|
+
return data.map((p: IPost) => ({
|
|
32
33
|
name: p.title,
|
|
33
34
|
to: p.title,
|
|
34
35
|
children: p.title,
|
|
35
36
|
}));
|
|
36
37
|
}, [data, swrError]);
|
|
37
38
|
|
|
39
|
+
const [fullTitleList, setFullTitleList] = useState<TitleListItem[]>(baseTitleList);
|
|
40
|
+
|
|
38
41
|
// Use middleware hook to transform title list
|
|
39
42
|
const {
|
|
40
|
-
data: titleList,
|
|
41
43
|
loading: isTransforming,
|
|
42
44
|
error: transformError,
|
|
43
|
-
|
|
45
|
+
run,
|
|
46
|
+
} = useMiddlewareRunner<TitleListItem[]>({
|
|
44
47
|
hookPoint: 'transformTitleList',
|
|
45
|
-
baseData: baseTitleList,
|
|
46
|
-
immediate: baseTitleList.length > 0,
|
|
47
48
|
});
|
|
48
49
|
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (baseTitleList.length > 0) {
|
|
52
|
+
run(baseTitleList).then(setFullTitleList);
|
|
53
|
+
}
|
|
54
|
+
}, [baseTitleList, run]);
|
|
55
|
+
|
|
49
56
|
const gLoading = useGlobalLoading('posts', isDataLoading || isTransforming);
|
|
50
57
|
|
|
51
58
|
const posts =
|
|
52
59
|
isDataLoading || name === '' || swrError || !data
|
|
53
60
|
? data
|
|
54
|
-
: data.find((p:
|
|
61
|
+
: data.find((p: IPost) => p.title === name);
|
|
55
62
|
|
|
56
63
|
return {
|
|
57
64
|
posts,
|
|
58
|
-
titleList:
|
|
65
|
+
titleList: fullTitleList ?? [],
|
|
59
66
|
isError: swrError || transformError,
|
|
60
67
|
isLoading: gLoading,
|
|
61
68
|
};
|
package/src/blog/index.tsx
CHANGED
|
@@ -1,31 +1,40 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { LinkProps } from '@bbki.ng/ui';
|
|
3
|
+
|
|
2
4
|
import { CenterLinkList } from '@/components';
|
|
3
|
-
import {
|
|
5
|
+
import { useMiddlewareRunner } from '#/core/hooks';
|
|
6
|
+
|
|
7
|
+
export const Cover = (_: { className?: string }) => {
|
|
8
|
+
const baseEntries: Array<LinkProps> = useMemo(
|
|
9
|
+
() => [
|
|
10
|
+
{
|
|
11
|
+
to: '/blog',
|
|
12
|
+
children: 'cd ./blog',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
to: '/now',
|
|
16
|
+
children: 'cd ./now',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
[]
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const [entries, setEntries] = React.useState<Array<LinkProps>>(baseEntries);
|
|
23
|
+
|
|
24
|
+
const { run } = useMiddlewareRunner<Array<LinkProps>>({
|
|
25
|
+
hookPoint: 'transformCoverEntry',
|
|
26
|
+
onMiddlewareChange: () => {
|
|
27
|
+
run(baseEntries).then(setEntries);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
run(baseEntries).then(setEntries);
|
|
33
|
+
}, [baseEntries, run]);
|
|
4
34
|
|
|
5
|
-
export const Cover = (props: { className?: string }) => {
|
|
6
|
-
const globalRouteCtx = useContext(GlobalRoutesContext);
|
|
7
|
-
const routes = globalRouteCtx.globalRoutes;
|
|
8
|
-
const pluginEntry = routes.length > 0 ? [{ to: '/plugins', name: 'cd ./plugins' }] : [];
|
|
9
35
|
return (
|
|
10
36
|
<>
|
|
11
|
-
<CenterLinkList
|
|
12
|
-
className="select-none"
|
|
13
|
-
links={[
|
|
14
|
-
{
|
|
15
|
-
to: '/blog',
|
|
16
|
-
name: 'cd ./blog',
|
|
17
|
-
children: 'cd ./blog',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
to: '/now',
|
|
21
|
-
name: 'cd ./now',
|
|
22
|
-
children: 'cd ./now',
|
|
23
|
-
},
|
|
24
|
-
...pluginEntry,
|
|
25
|
-
]}
|
|
26
|
-
title=""
|
|
27
|
-
// footer={<Version className="" />}
|
|
28
|
-
/>
|
|
37
|
+
<CenterLinkList className="select-none" links={entries} />
|
|
29
38
|
</>
|
|
30
39
|
);
|
|
31
40
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { NotFound } from '@bbki.ng/ui';
|
|
3
3
|
import { useParams } from 'react-router-dom';
|
|
4
|
+
|
|
4
5
|
import { usePosts } from '@/hooks/use_posts';
|
|
5
6
|
import { ArticlePage } from '@/components/article';
|
|
6
7
|
import { useBlogScrollReset } from '@/hooks/use_blog_scroll_pos_restoration';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
function TxtArticle() {
|
|
9
10
|
const { title } = useParams();
|
|
10
11
|
const { posts, isError, isLoading } = usePosts(title);
|
|
11
12
|
|
|
@@ -19,7 +20,7 @@ export default () => {
|
|
|
19
20
|
return <NotFound />;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
if (isLoading) {
|
|
23
|
+
if (isLoading || !posts) {
|
|
23
24
|
return null;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -30,4 +31,6 @@ export default () => {
|
|
|
30
31
|
<div dangerouslySetInnerHTML={{ __html: posts.content }} />
|
|
31
32
|
</ArticlePage>
|
|
32
33
|
);
|
|
33
|
-
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default TxtArticle;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { LinkProps, Button } from '@bbki.ng/ui';
|
|
3
|
+
|
|
3
4
|
import { usePosts } from '@/hooks/use_posts';
|
|
4
5
|
import { CenterLinkList } from '@/components';
|
|
5
6
|
import { useBlogScroll, useBlogScrollRestoration } from '@/hooks/use_blog_scroll_pos_restoration';
|
|
@@ -9,43 +10,46 @@ type TxtProps = {
|
|
|
9
10
|
articleList?: LinkProps[];
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
const { titleList,
|
|
13
|
+
const Txt = (props: TxtProps) => {
|
|
14
|
+
const { titleList, isError } = usePosts();
|
|
14
15
|
|
|
15
16
|
useBlogScrollRestoration();
|
|
16
17
|
|
|
17
18
|
const { gotoTop } = useBlogScroll();
|
|
18
19
|
|
|
19
|
-
if (isLoading) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
if (isError) {
|
|
24
|
-
return <CenterLinkList links={props.articleList} />;
|
|
21
|
+
return <CenterLinkList links={props.articleList ?? titleList} />;
|
|
25
22
|
}
|
|
26
23
|
|
|
24
|
+
const links = props.articleList ?? titleList;
|
|
25
|
+
|
|
27
26
|
return (
|
|
28
27
|
<CenterLinkList
|
|
29
|
-
links={
|
|
30
|
-
loading={isLoading}
|
|
28
|
+
links={links}
|
|
31
29
|
footer={
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
links.length > 0 ? (
|
|
31
|
+
<Button onClick={gotoTop} className="mt-32">
|
|
32
|
+
<svg
|
|
33
|
+
data-testid="geist-icon"
|
|
34
|
+
height="16"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
viewBox="0 0 16 16"
|
|
37
|
+
width="16"
|
|
38
|
+
>
|
|
39
|
+
<path
|
|
40
|
+
fillRule="evenodd"
|
|
41
|
+
clipRule="evenodd"
|
|
42
|
+
d="M8.70711 1.39644C8.31659 1.00592 7.68342 1.00592 7.2929 1.39644L2.21968 6.46966L1.68935 6.99999L2.75001 8.06065L3.28034 7.53032L7.25001 3.56065V14.25V15H8.75001V14.25V3.56065L12.7197 7.53032L13.25 8.06065L14.3107 6.99999L13.7803 6.46966L8.70711 1.39644Z"
|
|
43
|
+
fill="currentColor"
|
|
44
|
+
></path>
|
|
45
|
+
</svg>
|
|
46
|
+
</Button>
|
|
47
|
+
) : null
|
|
48
48
|
}
|
|
49
49
|
/>
|
|
50
50
|
);
|
|
51
51
|
};
|
|
52
|
+
|
|
53
|
+
Txt.displayName = 'Txt';
|
|
54
|
+
|
|
55
|
+
export default Txt;
|
package/src/blog/swr.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
2
|
import { SWRConfig } from 'swr';
|
|
3
|
+
|
|
3
4
|
import { cfApiFetcher } from '@/utils';
|
|
4
5
|
|
|
5
|
-
export const SWR = (props: { children:
|
|
6
|
+
export const SWR = (props: { children: ReactNode }) => {
|
|
6
7
|
return (
|
|
7
8
|
<SWRConfig
|
|
8
9
|
value={{
|
package/src/blog/types/path.ts
CHANGED
package/src/blog/utils/index.ts
CHANGED
|
@@ -32,24 +32,3 @@ export const withBBApi =
|
|
|
32
32
|
fetcher(`${apiEndPoint}/${resource}`, { ...init, mode: 'cors' });
|
|
33
33
|
|
|
34
34
|
export const cfApiFetcher = withBBApi(baseFetcher)(API_ENDPOINT);
|
|
35
|
-
|
|
36
|
-
export const changeFont = (type: FontType) => {
|
|
37
|
-
const rootDiv = document.getElementById('root');
|
|
38
|
-
if (rootDiv == null) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (rootDiv.classList.contains(type)) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// remove all font type class
|
|
47
|
-
for (const fontType in FontType) {
|
|
48
|
-
rootDiv.classList.remove(fontType);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
rootDiv.classList.add(type);
|
|
52
|
-
|
|
53
|
-
// save font type to local storage
|
|
54
|
-
localStorage.setItem('font', type);
|
|
55
|
-
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { createContext, useContext, ComponentType } from 'react';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 创建插件专属的 Context 工具集
|
|
@@ -33,15 +34,19 @@ export function createPluginCtx<T>() {
|
|
|
33
34
|
* const Enhanced = withCtx(picker, BaseComp)(ctx);
|
|
34
35
|
* ctx.api.registerSlot('slotName', Enhanced, pluginId);
|
|
35
36
|
*/
|
|
36
|
-
const withCtx = <P extends
|
|
37
|
+
const withCtx = <P extends IComPropsRegisteredToSlot>(
|
|
37
38
|
ctx: T,
|
|
38
39
|
Component: React.ComponentType<P>
|
|
39
40
|
): ComponentType<P> => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const WrappedComponent: React.FC<P> = props => {
|
|
42
|
+
return (
|
|
43
|
+
<Ctx.Provider value={ctx}>
|
|
44
|
+
<Component {...props} />
|
|
45
|
+
</Ctx.Provider>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return WrappedComponent;
|
|
45
50
|
};
|
|
46
51
|
|
|
47
52
|
return {
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -1,68 +1,54 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
import { registry } from '#/core/registry';
|
|
4
4
|
import type { HookPoint } from '#/types/slots';
|
|
5
5
|
|
|
6
|
-
export interface UseMiddlewareTransDataOptions
|
|
6
|
+
export interface UseMiddlewareTransDataOptions {
|
|
7
7
|
hookPoint: HookPoint;
|
|
8
|
-
|
|
9
|
-
immediate?: boolean;
|
|
8
|
+
onMiddlewareChange?: () => void; // 通知外部,由外部决定是否 run
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export interface UseMiddlewareTransDataResult<T> {
|
|
13
|
-
data: T | undefined;
|
|
14
12
|
loading: boolean;
|
|
15
13
|
error: Error | null;
|
|
16
|
-
run: () => Promise<T>;
|
|
14
|
+
run: (inputData: T) => Promise<T>;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
export function
|
|
17
|
+
export function useMiddlewareRunner<T>({
|
|
20
18
|
hookPoint,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}: UseMiddlewareTransDataOptions<T>): UseMiddlewareTransDataResult<T> {
|
|
24
|
-
const [data, setData] = useState<T | undefined>(undefined);
|
|
19
|
+
onMiddlewareChange,
|
|
20
|
+
}: UseMiddlewareTransDataOptions): UseMiddlewareTransDataResult<T> {
|
|
25
21
|
const [loading, setLoading] = useState(false);
|
|
26
22
|
const [error, setError] = useState<Error | null>(null);
|
|
27
23
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
24
|
+
const run = useCallback(
|
|
25
|
+
async (inputData: T) => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const result = await registry.runMiddleware(hookPoint, inputData);
|
|
30
|
+
return result;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
33
|
+
setError(error);
|
|
34
|
+
throw error;
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[hookPoint]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// 仅通知,不执行
|
|
48
43
|
useEffect(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
run()
|
|
54
|
-
.then(result => {
|
|
55
|
-
if (cancelled) return;
|
|
56
|
-
setData(result);
|
|
57
|
-
})
|
|
58
|
-
.catch(() => {
|
|
59
|
-
// 错误已在 run 中设置到 state
|
|
60
|
-
});
|
|
44
|
+
const unsubscribe = registry.subscribeMiddleware(hookPoint, () => {
|
|
45
|
+
onMiddlewareChange?.();
|
|
46
|
+
});
|
|
61
47
|
|
|
62
48
|
return () => {
|
|
63
|
-
|
|
49
|
+
unsubscribe();
|
|
64
50
|
};
|
|
65
|
-
}, [hookPoint,
|
|
51
|
+
}, [hookPoint, onMiddlewareChange]);
|
|
66
52
|
|
|
67
|
-
return {
|
|
53
|
+
return { loading, error, run };
|
|
68
54
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import { SlotName } from '#/types/slots';
|
|
3
|
+
import { IComPropsRegisteredToSlot, SlotName } from '#/types/slots';
|
|
4
4
|
|
|
5
5
|
import { registry } from '../registry';
|
|
6
6
|
|
|
7
7
|
export const useSlotComp = (slotName: SlotName) => {
|
|
8
|
-
const [components, setComponents] = useState<React.ComponentType<
|
|
8
|
+
const [components, setComponents] = useState<React.ComponentType<IComPropsRegisteredToSlot>[]>(
|
|
9
|
+
() => registry.getComponents(slotName)
|
|
10
|
+
);
|
|
9
11
|
|
|
10
12
|
useEffect(() => {
|
|
13
|
+
setComponents(registry.getComponents(slotName));
|
|
14
|
+
|
|
11
15
|
const unsubscribe = registry.subscribe(() => {
|
|
12
16
|
const comps = registry.getComponents(slotName);
|
|
13
17
|
setComponents(comps);
|
|
@@ -16,7 +20,7 @@ export const useSlotComp = (slotName: SlotName) => {
|
|
|
16
20
|
return () => {
|
|
17
21
|
unsubscribe();
|
|
18
22
|
};
|
|
19
|
-
}, []);
|
|
23
|
+
}, [slotName]);
|
|
20
24
|
|
|
21
25
|
return components;
|
|
22
26
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { pluginManager } from '#/core/pluginManager';
|
|
4
4
|
import { PluginID } from '#/types/plugin';
|
|
@@ -13,11 +13,20 @@ import { PluginID } from '#/types/plugin';
|
|
|
13
13
|
* usePlugins(['sticker', 'fontstyler']);
|
|
14
14
|
*/
|
|
15
15
|
export const usePlugins = (pluginIds: Array<PluginID>) => {
|
|
16
|
+
const [done, setDone] = useState(false);
|
|
17
|
+
|
|
18
|
+
const loadAllPlugins = useCallback(async () => {
|
|
19
|
+
try {
|
|
20
|
+
await Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)));
|
|
21
|
+
setDone(true);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error loading plugins:', error);
|
|
24
|
+
}
|
|
25
|
+
}, [pluginIds]);
|
|
26
|
+
|
|
16
27
|
useEffect(() => {
|
|
17
28
|
// 加载指定的插件
|
|
18
|
-
|
|
19
|
-
pluginManager.loadPlugin(id);
|
|
20
|
-
});
|
|
29
|
+
loadAllPlugins();
|
|
21
30
|
|
|
22
31
|
return () => {
|
|
23
32
|
// 卸载所有指定的插件
|
|
@@ -25,5 +34,7 @@ export const usePlugins = (pluginIds: Array<PluginID>) => {
|
|
|
25
34
|
pluginManager.disablePlugin(id);
|
|
26
35
|
});
|
|
27
36
|
};
|
|
28
|
-
}, [pluginIds]);
|
|
37
|
+
}, [loadAllPlugins, pluginIds]);
|
|
38
|
+
|
|
39
|
+
return done;
|
|
29
40
|
};
|
package/src/core/registry.ts
CHANGED
|
@@ -20,6 +20,7 @@ export class Registry {
|
|
|
20
20
|
private slots = new Map<SlotName, ISlotEntry[]>();
|
|
21
21
|
|
|
22
22
|
private listeners = new Set<() => void>();
|
|
23
|
+
private middlewareListenerMap = new Map<HookPoint, Set<() => void>>();
|
|
23
24
|
|
|
24
25
|
private middlewares = new Map<HookPoint, IMiddlewareEntry<unknown>[]>();
|
|
25
26
|
|
|
@@ -27,6 +28,22 @@ export class Registry {
|
|
|
27
28
|
this.listeners.forEach(listener => listener());
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
private broadcastMiddlewareChange(point: HookPoint) {
|
|
32
|
+
const listeners = this.middlewareListenerMap.get(point);
|
|
33
|
+
if (listeners) {
|
|
34
|
+
listeners.forEach(listener => listener());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
subscribeMiddleware(hookPoint: HookPoint, listener: () => void) {
|
|
39
|
+
if (!this.middlewareListenerMap.has(hookPoint)) {
|
|
40
|
+
this.middlewareListenerMap.set(hookPoint, new Set());
|
|
41
|
+
}
|
|
42
|
+
const listeners = this.middlewareListenerMap.get(hookPoint)!;
|
|
43
|
+
listeners.add(listener);
|
|
44
|
+
return () => listeners.delete(listener);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
subscribe(listener: () => void) {
|
|
31
48
|
this.listeners.add(listener);
|
|
32
49
|
return () => this.listeners.delete(listener);
|
|
@@ -91,7 +108,10 @@ export class Registry {
|
|
|
91
108
|
const newList = [...existing, newEntry as IMiddlewareEntry<unknown>].sort(
|
|
92
109
|
(a, b) => (b.weight || 0) - (a.weight || 0)
|
|
93
110
|
);
|
|
111
|
+
|
|
94
112
|
this.middlewares.set(hookPoint, newList);
|
|
113
|
+
|
|
114
|
+
this.broadcastMiddlewareChange(hookPoint);
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
getComponents(slotName: SlotName): React.ComponentType<IComPropsRegisteredToSlot>[] {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PathRouteProps } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
|
+
|
|
6
|
+
export const ExtendedRoutesPage = ({ data }: IComPropsRegisteredToSlot) => {
|
|
7
|
+
const route = data as Omit<PathRouteProps, 'element'>;
|
|
8
|
+
|
|
9
|
+
if (route.path === '/hello') {
|
|
10
|
+
return <div className="p-4 text-center">Hello from Extra Entry Plugin!</div>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <></>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { IHostContext } from '#/types/hostApi';
|
|
2
|
+
import { IPlugin } from '#/types/plugin';
|
|
3
|
+
import { buildEntryCreator } from '#/utils';
|
|
4
|
+
|
|
5
|
+
import { ExtendedRoutesPage } from './components/page';
|
|
6
|
+
|
|
7
|
+
export class ExtraEntryPlugin implements IPlugin {
|
|
8
|
+
id: string = 'extra-entry';
|
|
9
|
+
name: string = 'Extra Entry';
|
|
10
|
+
description?: string | undefined;
|
|
11
|
+
version: string = '1.0.0';
|
|
12
|
+
author?: string | undefined = 'bbki.ng';
|
|
13
|
+
|
|
14
|
+
onInstall?: ((ctx: IHostContext) => void | Promise<void>) | undefined = async (
|
|
15
|
+
ctx: IHostContext
|
|
16
|
+
) => {
|
|
17
|
+
const entryCreator = buildEntryCreator(ctx);
|
|
18
|
+
entryCreator(
|
|
19
|
+
{
|
|
20
|
+
pageComponent: ExtendedRoutesPage,
|
|
21
|
+
path: '/hello',
|
|
22
|
+
label: 'cd ./hello',
|
|
23
|
+
},
|
|
24
|
+
this.id
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
onDisable?: (() => Promise<void> | void) | undefined;
|
|
29
|
+
onDestroy?: (() => void) | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default new ExtraEntryPlugin();
|
package/src/plugins/manifest.ts
CHANGED
|
@@ -12,6 +12,12 @@ export const PLUGIN_MANIFEST = [
|
|
|
12
12
|
description:
|
|
13
13
|
'Provides additional change directory functionalities for enhanced user experience.',
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
name: 'extra-entry',
|
|
17
|
+
id: 'extra-entry',
|
|
18
|
+
version: '0.1.0',
|
|
19
|
+
description: 'Provides extra entries for cover and extended routes.',
|
|
20
|
+
},
|
|
15
21
|
{
|
|
16
22
|
name: 'xwy',
|
|
17
23
|
id: 'xwy',
|
package/src/types/plugin.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
4
|
+
|
|
1
5
|
import { IHostContext } from './hostApi';
|
|
2
6
|
|
|
3
7
|
export interface IPlugin {
|
|
@@ -12,4 +16,10 @@ export interface IPlugin {
|
|
|
12
16
|
onDestroy?: () => void;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export type PluginID = 'sticker' | 'xwy' | 'extra-cd';
|
|
19
|
+
export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry';
|
|
20
|
+
|
|
21
|
+
export interface IPluginEntry {
|
|
22
|
+
path: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
pageComponent: React.ComponentType<IComPropsRegisteredToSlot>;
|
|
25
|
+
}
|
package/src/types/posts.ts
CHANGED
package/src/types/slots.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow' | 'logo';
|
|
2
|
-
|
|
1
|
+
export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow' | 'logo' | 'route';
|
|
2
|
+
|
|
3
|
+
export type HookPoint =
|
|
4
|
+
| 'filterPosts'
|
|
5
|
+
| 'transformPostContent'
|
|
6
|
+
| 'transformTitleList'
|
|
7
|
+
| 'extendedRoutes'
|
|
8
|
+
| 'transformCoverEntry';
|
|
3
9
|
|
|
4
10
|
export interface IComPropsRegisteredToSlot {
|
|
5
11
|
data: unknown;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { LinkProps, PathRouteProps } from 'react-router-dom';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { IHostContext } from '#/types/hostApi';
|
|
5
|
+
import { IPluginEntry } from '#/types/plugin';
|
|
6
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
7
|
+
|
|
8
|
+
export const buildEntryCreator = (ctx: IHostContext) => (entry: IPluginEntry, id: string) => {
|
|
9
|
+
ctx.api.registerMiddleware(
|
|
10
|
+
'extendedRoutes',
|
|
11
|
+
(routes: Array<Omit<PathRouteProps, 'element'>>) => {
|
|
12
|
+
return [
|
|
13
|
+
...routes,
|
|
14
|
+
{
|
|
15
|
+
path: entry.path,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
},
|
|
19
|
+
id,
|
|
20
|
+
10
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
ctx.api.registerSlot(
|
|
24
|
+
'route',
|
|
25
|
+
(props: IComPropsRegisteredToSlot) => {
|
|
26
|
+
const route = props.data as Omit<PathRouteProps, 'element'>;
|
|
27
|
+
if (route.path === entry.path) {
|
|
28
|
+
const Com = entry.pageComponent;
|
|
29
|
+
return <Com data={props.data} />;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
},
|
|
33
|
+
id
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!entry.label) return;
|
|
37
|
+
|
|
38
|
+
ctx.api.registerMiddleware(
|
|
39
|
+
'transformCoverEntry',
|
|
40
|
+
(entries: Array<LinkProps>) => {
|
|
41
|
+
return [
|
|
42
|
+
...entries,
|
|
43
|
+
{
|
|
44
|
+
to: entry.path,
|
|
45
|
+
children: entry.label,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
id,
|
|
50
|
+
10
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Role, useRole } from '@/hooks/use_role';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Navigate } from 'react-router-dom';
|
|
4
|
-
|
|
5
|
-
export const Auth = (props: { children: any; shouldRedirect?: boolean; role?: Role[] }) => {
|
|
6
|
-
const myRole = useRole();
|
|
7
|
-
|
|
8
|
-
if (props.role && !props.role.includes(myRole)) {
|
|
9
|
-
return props.shouldRedirect ? <Navigate to={'/login'} /> : null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return props.children;
|
|
13
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import cls from 'classnames';
|
|
3
|
-
|
|
4
|
-
export type DelayFadeInProps = {
|
|
5
|
-
children: any;
|
|
6
|
-
delay: number;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const DelayFadeIn = (props: DelayFadeInProps) => {
|
|
10
|
-
const [show, setShow] = React.useState(false);
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const id = setTimeout(() => {
|
|
14
|
-
setShow(true);
|
|
15
|
-
}, props.delay);
|
|
16
|
-
|
|
17
|
-
return () => {
|
|
18
|
-
clearTimeout(id);
|
|
19
|
-
};
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
22
|
-
const className = cls('transition-opacity', {
|
|
23
|
-
'opacity-100': show,
|
|
24
|
-
'opacity-0': !show,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
return <div className={className}>{props.children}</div>;
|
|
28
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useGlobalLoading } from '@/hooks';
|
|
3
|
-
|
|
4
|
-
export const Spinner = (props: { disableDotIndicator?: boolean }) => {
|
|
5
|
-
const { disableDotIndicator } = props;
|
|
6
|
-
|
|
7
|
-
useGlobalLoading('spinner', !disableDotIndicator);
|
|
8
|
-
|
|
9
|
-
return <div className="h-full w-full grid place-items-center"></div>;
|
|
10
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import React, { ReactElement, ReactNode, Suspense } from 'react';
|
|
2
|
-
import { ErrorBoundary } from '@bbki.ng/ui';
|
|
3
|
-
import { Spinner } from './Spinner';
|
|
4
|
-
|
|
5
|
-
export const MySuspense = (props: { children: ReactNode; fallback?: ReactElement }) => {
|
|
6
|
-
return (
|
|
7
|
-
<ErrorBoundary>
|
|
8
|
-
<Suspense fallback={<Spinner />}>{props.children}</Suspense>
|
|
9
|
-
</ErrorBoundary>
|
|
10
|
-
);
|
|
11
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Button } from '@bbki.ng/ui';
|
|
3
|
-
import { ShareIcon } from './share-icon';
|
|
4
|
-
|
|
5
|
-
export const ShareBtn = ({ shareInfo }: { shareInfo: ShareData }) => {
|
|
6
|
-
const handleShare = async () => {
|
|
7
|
-
try {
|
|
8
|
-
await navigator.share(shareInfo);
|
|
9
|
-
} catch (error) {
|
|
10
|
-
const isAbortError = (error as Error).name === 'AbortError';
|
|
11
|
-
if (isAbortError) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
console.error('Share failed:', (error as Error).message);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<Button
|
|
20
|
-
size="sm"
|
|
21
|
-
className="text-gray-400 hover:text-gray-600 transition-colors ease-in duration-200"
|
|
22
|
-
variant="ghost"
|
|
23
|
-
onClick={handleShare}
|
|
24
|
-
>
|
|
25
|
-
<ShareIcon />
|
|
26
|
-
</Button>
|
|
27
|
-
);
|
|
28
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export const ShareIcon = () => (
|
|
4
|
-
<svg
|
|
5
|
-
data-testid="geist-icon"
|
|
6
|
-
height="12"
|
|
7
|
-
stroke-linejoin="round"
|
|
8
|
-
// style="color:currentColor"
|
|
9
|
-
viewBox="0 0 16 16"
|
|
10
|
-
width="12"
|
|
11
|
-
>
|
|
12
|
-
<path
|
|
13
|
-
fill-rule="evenodd"
|
|
14
|
-
clip-rule="evenodd"
|
|
15
|
-
d="M7.29289 1.39644C7.68342 1.00592 8.31658 1.00592 8.70711 1.39644L11.7803 4.46966L12.3107 4.99999L11.25 6.06065L10.7197 5.53032L8.75 3.56065V10.25V11H7.25V10.25V3.56065L5.28033 5.53032L4.75 6.06065L3.68934 4.99999L4.21967 4.46966L7.29289 1.39644ZM13.5 9.24999V13.5H2.5V9.24999V8.49999H1V9.24999V14C1 14.5523 1.44771 15 2 15H14C14.5523 15 15 14.5523 15 14V9.24999V8.49999H13.5V9.24999Z"
|
|
16
|
-
fill="currentColor"
|
|
17
|
-
></path>
|
|
18
|
-
</svg>
|
|
19
|
-
);
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { changeFont } from '@/utils';
|
|
3
|
-
import { FontType } from '@/types/font';
|
|
4
|
-
|
|
5
|
-
export const useFontLoading = () => {
|
|
6
|
-
const [isFontLoading, setIsFontLoading] = useState(false);
|
|
7
|
-
|
|
8
|
-
const handleFontLoading = () => {
|
|
9
|
-
setIsFontLoading(true);
|
|
10
|
-
document.fonts.ready.then(() => {
|
|
11
|
-
handleFontLoadingDone();
|
|
12
|
-
});
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const handleFontLoadingDone = () => {
|
|
16
|
-
setIsFontLoading(false);
|
|
17
|
-
// setTimeout(() => {
|
|
18
|
-
// }, 500);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const handleFontLoadingError = () => {
|
|
22
|
-
setIsFontLoading(false);
|
|
23
|
-
changeFont(FontType.Mono);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
document.fonts.onloadingerror = handleFontLoadingError;
|
|
28
|
-
document.fonts.onloading = handleFontLoading;
|
|
29
|
-
|
|
30
|
-
document.fonts.ready.then(() => {
|
|
31
|
-
handleFontLoadingDone();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
return () => {
|
|
35
|
-
document.fonts.onloadingerror = null;
|
|
36
|
-
document.fonts.onloading = null;
|
|
37
|
-
};
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
return isFontLoading;
|
|
41
|
-
};
|