@bbki.ng/site 5.5.15 → 5.5.16

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @bbki.ng/site
2
2
 
3
+ ## 5.5.16
4
+
5
+ ### Patch Changes
6
+
7
+ - 22df0c4: fix global loading
8
+
3
9
  ## 5.5.15
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.5.15",
3
+ "version": "5.5.16",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/blog/app.tsx CHANGED
@@ -11,9 +11,9 @@ import { SWR } from '@/swr';
11
11
  import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
12
12
  import { BotRedirect } from '@/pages/bot';
13
13
  import { BBContext } from '@/context/bbcontext';
14
+ import { Slot } from '#/core/components/SlotComp';
15
+ import { usePlugins } from '#/core/hooks/use_plugins';
14
16
  import { useDynamicLogo } from './hooks/use_dynamic_logo';
15
- import { Slot } from '../core/components/SlotComp';
16
- import { usePlugins } from './hooks/use_plugins';
17
17
 
18
18
  const Layout = () => {
19
19
  const paths = usePaths();
@@ -1,20 +1,10 @@
1
- import React, { useContext, useEffect } from 'react';
2
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
1
+ import React from 'react';
2
+ import { useLoading } from '@/hooks';
3
3
 
4
4
  export const Spinner = (props: { disableDotIndicator?: boolean }) => {
5
5
  const { disableDotIndicator } = props;
6
6
 
7
- const { setIsLoading } = useContext(GlobalLoadingContext);
8
-
9
- useEffect(() => {
10
- if (disableDotIndicator) {
11
- return;
12
- }
13
- setIsLoading(true);
14
- return () => {
15
- setIsLoading(false);
16
- };
17
- }, []);
7
+ useLoading('spinner', !disableDotIndicator);
18
8
 
19
9
  return <div className="h-full w-full grid place-items-center"></div>;
20
10
  };
@@ -1,29 +1,69 @@
1
- import React, { createContext, ReactNode, useState, Dispatch, SetStateAction } from 'react';
1
+ import React, { createContext, ReactNode, useState, useCallback, useMemo } from 'react';
2
2
  import { useFontLoading } from '@/hooks/use_font_loading';
3
3
 
4
+ type LoadingStates = Map<string, boolean>;
5
+
4
6
  type LoadingContext = {
5
7
  isLoading: boolean;
6
8
  isFontLoading: boolean;
7
- setIsLoading: Dispatch<SetStateAction<boolean>>;
9
+ register: (id: string) => void;
10
+ setLoading: (id: string, loading: boolean) => void;
11
+ unregister: (id: string) => void;
8
12
  };
9
13
 
10
14
  export const GlobalLoadingContext = createContext<LoadingContext>({
11
15
  isFontLoading: false,
12
16
  isLoading: false,
13
- setIsLoading: () => false,
17
+ register: () => {},
18
+ setLoading: () => {},
19
+ unregister: () => {},
14
20
  });
15
21
 
16
22
  export const GlobalLoadingStateProvider = (props: { children: ReactNode }) => {
17
- const [isLoading, setIsLoading] = useState(false);
23
+ const [loadingStates, setLoadingStates] = useState<LoadingStates>(new Map());
18
24
  const isFontLoading = useFontLoading();
25
+
26
+ const register = useCallback((id: string) => {
27
+ setLoadingStates(prev => {
28
+ const next = new Map(prev);
29
+ next.set(id, false);
30
+ return next;
31
+ });
32
+ }, []);
33
+
34
+ const setLoading = useCallback((id: string, loading: boolean) => {
35
+ setLoadingStates(prev => {
36
+ const next = new Map(prev);
37
+ next.set(id, loading);
38
+ return next;
39
+ });
40
+ }, []);
41
+
42
+ const unregister = useCallback((id: string) => {
43
+ setLoadingStates(prev => {
44
+ const next = new Map(prev);
45
+ next.delete(id);
46
+ return next;
47
+ });
48
+ }, []);
49
+
50
+ const isLoading = useMemo(() => {
51
+ return Array.from(loadingStates.values()).some(Boolean);
52
+ }, [loadingStates]);
53
+
54
+ const contextValue = useMemo(
55
+ () => ({
56
+ isLoading,
57
+ isFontLoading,
58
+ register,
59
+ setLoading,
60
+ unregister,
61
+ }),
62
+ [isLoading, isFontLoading, register, setLoading, unregister]
63
+ );
64
+
19
65
  return (
20
- <GlobalLoadingContext.Provider
21
- value={{
22
- isLoading,
23
- setIsLoading,
24
- isFontLoading,
25
- }}
26
- >
66
+ <GlobalLoadingContext.Provider value={contextValue}>
27
67
  {props.children}
28
68
  </GlobalLoadingContext.Provider>
29
69
  );
@@ -1,3 +1,4 @@
1
1
  export { usePaths } from './use_paths';
2
2
  export { useStreaming } from './use_streaming';
3
3
  export type { StreamingItem } from './use_streaming';
4
+ export { useLoading } from './use_loading';
@@ -0,0 +1,29 @@
1
+ import { useContext, useEffect } from 'react';
2
+ import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
3
+
4
+ /**
5
+ * Hook to register a loading state with the global loading context.
6
+ * The global loading state is true if ANY registered loading state is true.
7
+ *
8
+ * @param id - Unique identifier for this loading state
9
+ * @param loading - Whether this specific loading state is active
10
+ *
11
+ * @example
12
+ * // In a data fetching hook
13
+ * useLoading('posts', isDataLoading);
14
+ *
15
+ * @example
16
+ * // In a component
17
+ * useLoading('spinner', true);
18
+ */
19
+ export function useLoading(id: string, loading: boolean) {
20
+ const { register, setLoading, unregister } = useContext(GlobalLoadingContext);
21
+
22
+ useEffect(() => {
23
+ register(id);
24
+ setLoading(id, loading);
25
+ return () => {
26
+ unregister(id);
27
+ };
28
+ }, [id, loading, register, setLoading, unregister]);
29
+ }
@@ -1,18 +1,15 @@
1
1
  import useSWR from 'swr';
2
- import { useContext, useEffect, useState } from 'react';
3
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
2
+ import { useMemo } from 'react';
3
+ import { useLoading } from '@/hooks';
4
4
  import { baseFetcher } from '@/utils';
5
5
  import { API_ENDPOINT } from '@/constants/routes';
6
- import { registry } from '#/core/registry';
6
+ import { useMiddlewareTransData } from '#/core/hooks';
7
7
 
8
8
  // In dev, use /api prefix to leverage Vite proxy to localhost:8787
9
9
  const isProd = true;
10
10
  // const isProd = typeof window !== 'undefined' && /^https:\/\/bbki\.ng/.test(window.location.href);
11
11
  const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
12
12
 
13
- // Use baseFetcher for full URLs, cfApiFetcher is for relative paths
14
- const postsFetcher = (resource: string) => baseFetcher(resource);
15
-
16
13
  export interface TitleListItem {
17
14
  name: string;
18
15
  to: string;
@@ -21,58 +18,54 @@ export interface TitleListItem {
21
18
  }
22
19
 
23
20
  export const usePosts = (name: string = '', suspense?: boolean) => {
24
- const { data: response, error } = useSWR(POSTS_API, postsFetcher, {
21
+ const { data: response, error: swrError } = useSWR(POSTS_API, baseFetcher, {
25
22
  revalidateOnFocus: false,
26
23
  suspense,
27
24
  });
28
25
 
29
26
  // Extract posts array from API response { status: "success", data: [...] }
30
27
  const data = response?.data;
28
+ const isDataLoading = !data && !swrError;
31
29
 
32
- const [titleList, setTitleList] = useState<TitleListItem[]>([]);
33
- let isLoading = !data && !error;
34
- const { setIsLoading } = useContext(GlobalLoadingContext);
35
-
36
- useEffect(() => {
37
- setIsLoading(isLoading);
38
- }, [isLoading, setIsLoading]);
39
-
40
- // Apply middleware to transform titleList
41
- useEffect(() => {
42
- const transformTitleList = async () => {
43
- if (isLoading || error || !data) {
44
- setTitleList([]);
45
- return;
46
- }
30
+ // Build base title list from posts data
31
+ const baseTitleList: TitleListItem[] = useMemo(() => {
32
+ if (!data || swrError) return [];
33
+ return [
34
+ ...data.map((p: any) => ({
35
+ name: p.title,
36
+ to: p.title,
37
+ children: p.title,
38
+ })),
39
+ {
40
+ name: 'cd ~',
41
+ to: '/',
42
+ children: 'cd ~',
43
+ },
44
+ ];
45
+ }, [data, swrError]);
47
46
 
48
- const baseTitleList: TitleListItem[] = [
49
- ...data.map((p: any) => ({
50
- name: p.title,
51
- to: p.title,
52
- children: p.title,
53
- })),
54
- {
55
- name: 'cd ~',
56
- to: '/',
57
- children: 'cd ~',
58
- },
59
- ];
60
-
61
- // Run middleware to allow plugins to transform the title list
62
- const transformedList = await registry.runMiddleware('transformTitleList', baseTitleList);
63
- setTitleList(transformedList);
64
- };
47
+ // Use middleware hook to transform title list
48
+ const {
49
+ data: titleList,
50
+ loading: isTransforming,
51
+ error: transformError,
52
+ } = useMiddlewareTransData({
53
+ hookPoint: 'transformTitleList',
54
+ baseData: baseTitleList,
55
+ immediate: baseTitleList.length > 0,
56
+ });
65
57
 
66
- transformTitleList();
67
- }, [data, isLoading, error]);
58
+ useLoading('posts', isDataLoading || isTransforming);
68
59
 
69
60
  const posts =
70
- isLoading || name == '' || error || !data ? data : data.find((p: any) => p.title == name);
61
+ isDataLoading || name === '' || swrError || !data
62
+ ? data
63
+ : data.find((p: any) => p.title === name);
71
64
 
72
65
  return {
73
- posts: posts,
74
- titleList,
75
- isError: error,
76
- isLoading: !data && !error,
66
+ posts,
67
+ titleList: titleList ?? [],
68
+ isError: swrError || transformError,
69
+ isLoading: isDataLoading || (baseTitleList.length > 0 && isTransforming),
77
70
  };
78
71
  };
@@ -2,8 +2,8 @@ import useSWR from 'swr';
2
2
  import useSWRInfinite from 'swr/infinite';
3
3
  import { baseFetcher, withBBApi } from '@/utils';
4
4
  import { API_ENDPOINT } from '@/constants/routes';
5
- import { useContext, useEffect, useState, useCallback } from 'react';
6
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
5
+ import { useEffect, useState, useCallback } from 'react';
6
+ import { useLoading } from '@/hooks';
7
7
 
8
8
  // In dev, use /api prefix to leverage Vite proxy to localhost:8787
9
9
  // const isProd = typeof window !== 'undefined' && /^https:\/\/bbki.ng/.test(window.location.href);
@@ -101,11 +101,7 @@ export function useStreaming(options: UseStreamingOptions = {}) {
101
101
  });
102
102
  }, []);
103
103
 
104
- const { setIsLoading } = useContext(GlobalLoadingContext);
105
-
106
- useEffect(() => {
107
- setIsLoading(isLoading);
108
- }, [isLoading, setIsLoading]);
104
+ useLoading('streaming', isLoading);
109
105
 
110
106
  return {
111
107
  streaming: data?.data ?? [],
@@ -4,7 +4,7 @@ import { useSlotComp } from '../hooks/useSlotComp';
4
4
 
5
5
  export interface ISlotProps {
6
6
  name: SlotName;
7
- data?: any;
7
+ data?: unknown;
8
8
  }
9
9
 
10
10
  export const Slot: React.FC<ISlotProps> = ({ name, data }) => {
@@ -0,0 +1,6 @@
1
+ export { useSlotComp } from './useSlotComp';
2
+ export { useMiddlewareTransData } from './useMiddlewareTransData';
3
+ export type {
4
+ UseMiddlewareTransDataOptions,
5
+ UseMiddlewareTransDataResult,
6
+ } from './useMiddlewareTransData';
@@ -0,0 +1,68 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+
3
+ import { registry } from '#/core/registry';
4
+ import type { HookPoint } from '#/types/slots';
5
+
6
+ export interface UseMiddlewareTransDataOptions<T> {
7
+ hookPoint: HookPoint;
8
+ baseData: T;
9
+ immediate?: boolean;
10
+ }
11
+
12
+ export interface UseMiddlewareTransDataResult<T> {
13
+ data: T | undefined;
14
+ loading: boolean;
15
+ error: Error | null;
16
+ run: () => Promise<T>;
17
+ }
18
+
19
+ export function useMiddlewareTransData<T>({
20
+ hookPoint,
21
+ baseData,
22
+ immediate = true,
23
+ }: UseMiddlewareTransDataOptions<T>): UseMiddlewareTransDataResult<T> {
24
+ const [data, setData] = useState<T | undefined>(undefined);
25
+ const [loading, setLoading] = useState(false);
26
+ const [error, setError] = useState<Error | null>(null);
27
+
28
+ const immediateRef = useRef(immediate);
29
+ immediateRef.current = immediate;
30
+
31
+ const run = useCallback(async (): Promise<T> => {
32
+ setLoading(true);
33
+ setError(null);
34
+
35
+ try {
36
+ const result = await registry.runMiddleware(hookPoint, baseData);
37
+ setData(result);
38
+ return result;
39
+ } catch (err) {
40
+ const error = err instanceof Error ? err : new Error(String(err));
41
+ setError(error);
42
+ throw error;
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, [hookPoint, baseData]);
47
+
48
+ useEffect(() => {
49
+ if (!immediateRef.current) return;
50
+
51
+ let cancelled = false;
52
+
53
+ run()
54
+ .then(result => {
55
+ if (cancelled) return;
56
+ setData(result);
57
+ })
58
+ .catch(() => {
59
+ // 错误已在 run 中设置到 state
60
+ });
61
+
62
+ return () => {
63
+ cancelled = true;
64
+ };
65
+ }, [hookPoint, baseData, run]);
66
+
67
+ return { data, loading, error, run };
68
+ }
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from 'react';
2
+
2
3
  import { pluginManager } from '#/core/pluginManager';
3
4
 
4
5
  /**
@@ -1,3 +1,5 @@
1
+ import React from 'react';
2
+
1
3
  import { IHostContext } from '#/types/hostApi';
2
4
  import { registry } from './registry';
3
5
  import type { SlotName, HookPoint } from '#/types/slots';
@@ -1,12 +1,19 @@
1
+ import React from 'react';
2
+
1
3
  import { HookPoint, SlotName } from './slots';
2
4
  import { type FingerprintData } from '@/utils/fingerprints';
3
5
 
4
6
  export interface IHostApi {
5
7
  getDeviceId: () => Promise<{ id: string; fp: FingerprintData }>;
6
- registerMiddleware: (point: HookPoint, fn: Function, pluginId: string, weight?: number) => void;
7
- registerSlot: (
8
+ registerMiddleware: <T>(
9
+ point: HookPoint,
10
+ fn: (baseData: T) => T,
11
+ pluginId: string,
12
+ weight?: number
13
+ ) => void;
14
+ registerSlot: <T>(
8
15
  slotName: SlotName,
9
- component: React.ComponentType<any>,
16
+ component: React.ComponentType<T>,
10
17
  pluginId: string,
11
18
  weight?: number
12
19
  ) => void;