@bbki.ng/site 0.0.17

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.
Files changed (117) hide show
  1. package/.github/workflows/deploy.yml +44 -0
  2. package/.husky/pre-commit +4 -0
  3. package/.prettierignore +1 -0
  4. package/.prettierrc.json +1 -0
  5. package/.rush/temp/shrinkwrap-deps.json +908 -0
  6. package/CODE_OF_CONDUCT.md +45 -0
  7. package/CONTRIBUTING.md +11 -0
  8. package/LICENSE +21 -0
  9. package/README.md +21 -0
  10. package/index.html +33 -0
  11. package/jest.config.js +15 -0
  12. package/package.json +69 -0
  13. package/postcss.config.cjs +6 -0
  14. package/public/Logo.svg +9 -0
  15. package/public/apple-touch-icon.png +0 -0
  16. package/public/favicon-16x16.png +0 -0
  17. package/public/favicon-32x32.png +0 -0
  18. package/public/favicon.ico +0 -0
  19. package/public/favicon.svg +9 -0
  20. package/public/pwa-192x192.png +0 -0
  21. package/public/pwa-512x512.png +0 -0
  22. package/public/robots.txt +2 -0
  23. package/src/__test__/utils/index.test.ts +90 -0
  24. package/src/app.tsx +97 -0
  25. package/src/articles/anti-logic.mdx +61 -0
  26. package/src/articles/bbking-manual.mdx +88 -0
  27. package/src/articles/black-river.mdx +8 -0
  28. package/src/articles/cooldown.mdx +12 -0
  29. package/src/articles/fall.mdx +8 -0
  30. package/src/articles/img.mdx +104 -0
  31. package/src/articles/index.ts +35 -0
  32. package/src/articles/loading.mdx +129 -0
  33. package/src/articles/major-cold.mdx +14 -0
  34. package/src/articles/men-without-women.mdx +19 -0
  35. package/src/articles/movie-day.mdx +15 -0
  36. package/src/articles/now.mdx +15 -0
  37. package/src/articles/projects.mdx +9 -0
  38. package/src/articles/quote.mdx +27 -0
  39. package/src/articles/spring-cooldown.mdx +7 -0
  40. package/src/articles/spring-rain.mdx +9 -0
  41. package/src/articles/travel.mdx +21 -0
  42. package/src/articles/warming-up.mdx +10 -0
  43. package/src/articles/web-burnning.mdx +10 -0
  44. package/src/auth_required.tsx +17 -0
  45. package/src/components/Spinner.tsx +33 -0
  46. package/src/components/article/index.tsx +31 -0
  47. package/src/components/aspect_ratio_box/index.tsx +29 -0
  48. package/src/components/blur_cover/index.tsx +28 -0
  49. package/src/components/book_list/index.tsx +50 -0
  50. package/src/components/comment/index.tsx +70 -0
  51. package/src/components/comment/use_cusdis_event.ts +37 -0
  52. package/src/components/corner_prompt_box/index.tsx +63 -0
  53. package/src/components/disabled_text/index.tsx +23 -0
  54. package/src/components/fade_out_cover/index.tsx +37 -0
  55. package/src/components/footer/footer_links.ts +13 -0
  56. package/src/components/footer/index.tsx +21 -0
  57. package/src/components/hotkey_nav/index.tsx +50 -0
  58. package/src/components/img_list/index.tsx +43 -0
  59. package/src/components/index.tsx +27 -0
  60. package/src/components/movie_list/index.tsx +50 -0
  61. package/src/components/my_suspense.tsx +14 -0
  62. package/src/components/progress_bar/index.tsx +31 -0
  63. package/src/components/reload_prompt/index.tsx +36 -0
  64. package/src/components/stickers/index.tsx +46 -0
  65. package/src/components/table_skeleton/index.tsx +40 -0
  66. package/src/components/tags/index.tsx +52 -0
  67. package/src/components/video_player/index.tsx +81 -0
  68. package/src/components/with_wrapper/index.tsx +13 -0
  69. package/src/constants/cusdis.ts +6 -0
  70. package/src/constants/index.ts +16 -0
  71. package/src/constants/photo_projects.ts +54 -0
  72. package/src/constants/photos.ts +270 -0
  73. package/src/constants/routes.ts +24 -0
  74. package/src/constants/video_logs.ts +16 -0
  75. package/src/demo/DemoBox.tsx +15 -0
  76. package/src/demo/ImgDemo.tsx +34 -0
  77. package/src/demo/SpinnerDemo.tsx +17 -0
  78. package/src/global/mdx.d.ts +8 -0
  79. package/src/global_loading_state_provider.tsx +27 -0
  80. package/src/hooks/index.ts +15 -0
  81. package/src/hooks/useScrollToTop.ts +24 -0
  82. package/src/hooks/useTransitionCls.ts +36 -0
  83. package/src/hooks/use_img_loading.ts +16 -0
  84. package/src/hooks/use_pathname.ts +6 -0
  85. package/src/hooks/use_paths.ts +30 -0
  86. package/src/hooks/use_projects.ts +56 -0
  87. package/src/hooks/use_route_name.ts +7 -0
  88. package/src/hooks/use_supa_session.ts +30 -0
  89. package/src/hooks/use_uploader.ts +34 -0
  90. package/src/hooks/use_video_controls.ts +71 -0
  91. package/src/main.css +156 -0
  92. package/src/main.tsx +19 -0
  93. package/src/pages/cover/index.tsx +10 -0
  94. package/src/pages/extensions/png/consts.ts +9 -0
  95. package/src/pages/extensions/png/index.tsx +41 -0
  96. package/src/pages/extensions/png/png_projects.tsx +63 -0
  97. package/src/pages/extensions/txt/article.tsx +26 -0
  98. package/src/pages/extensions/txt/consts.ts +8 -0
  99. package/src/pages/extensions/txt/index.tsx +21 -0
  100. package/src/pages/index.tsx +14 -0
  101. package/src/pages/login/index.tsx +33 -0
  102. package/src/pages/now/index.tsx +7 -0
  103. package/src/pages/tags/index.tsx +28 -0
  104. package/src/pages/tags/tag_result.tsx +19 -0
  105. package/src/swr.tsx +18 -0
  106. package/src/types/articles.ts +6 -0
  107. package/src/types/color.ts +21 -0
  108. package/src/types/cusdis.ts +4 -0
  109. package/src/types/oss.ts +15 -0
  110. package/src/types/path.ts +11 -0
  111. package/src/types/photo.ts +17 -0
  112. package/src/types/supabase.ts +9 -0
  113. package/src/utils/index.ts +143 -0
  114. package/src/utils/tags.ts +21 -0
  115. package/tailwind.config.cjs +10 -0
  116. package/tsconfig.json +24 -0
  117. package/vite.config.ts +108 -0
@@ -0,0 +1,70 @@
1
+ import React, { useState } from "react";
2
+ import classnames from "classnames";
3
+ import { ReactCusdis } from "react-cusdis";
4
+ import { BgColors } from "@/types/color";
5
+ import { CUSDIS_ATTRS, CUSDIS_OFFICIAL_SITE_ADDRESS } from "@/constants/cusdis";
6
+ import { useCusidsEvent } from "@/components/comment/use_cusdis_event";
7
+ import { CornerPromptBox } from "@/components";
8
+ import { Link, LinkColor } from "@bbki.ng/components";
9
+
10
+ type CommentProps = {
11
+ title: string;
12
+ };
13
+
14
+ export const Comment = (props: CommentProps) => {
15
+ const path = location.href;
16
+ const cusdisAttrs = Object.assign({}, CUSDIS_ATTRS, {
17
+ pageTitle: props.title,
18
+ pageUrl: path,
19
+ pageId: path,
20
+ });
21
+
22
+ const [ready, setReady] = useState(false);
23
+ const [showBox, setShowBox] = useState(false);
24
+
25
+ useCusidsEvent({
26
+ handleCommentLoaded: () => {
27
+ setReady(true);
28
+ },
29
+ handleCommentSent: () => {
30
+ setShowBox(true);
31
+ },
32
+ });
33
+
34
+ return (
35
+ <div
36
+ className={classnames(
37
+ "overflow-hidden",
38
+ "transition-all",
39
+ BgColors.WHITE_GRAY,
40
+ {
41
+ "max-h-0": !ready,
42
+ "p-5": ready,
43
+ "mb-8": ready,
44
+ }
45
+ )}
46
+ >
47
+ <ReactCusdis attrs={cusdisAttrs} />
48
+ <CornerPromptBox
49
+ autoCancelAfter={3000}
50
+ content="评论已发送,等待 18+ 内容审核。"
51
+ showBox={showBox}
52
+ cancelLabel="关闭"
53
+ onCancel={() => {
54
+ console.log("cancel");
55
+ setShowBox(false);
56
+ }}
57
+ />
58
+ <div className="text-center">
59
+ <Link
60
+ to={CUSDIS_OFFICIAL_SITE_ADDRESS}
61
+ external
62
+ color={LinkColor.GRAY}
63
+ className="px-1"
64
+ >
65
+ powered by discus
66
+ </Link>
67
+ </div>
68
+ </div>
69
+ );
70
+ };
@@ -0,0 +1,37 @@
1
+ import { useEffect } from "react";
2
+ import { cusdisEvent } from "../../types/cusdis";
3
+
4
+ type cusdisEventHandlers = {
5
+ handleCommentLoaded?: () => void;
6
+ handleCommentSent?: () => void;
7
+ };
8
+
9
+ const noop = () => null;
10
+
11
+ export const useCusidsEvent = (handlers: cusdisEventHandlers) => {
12
+ const { handleCommentLoaded = noop, handleCommentSent = noop } = handlers;
13
+ const onMessage = (e: MessageEvent) => {
14
+ try {
15
+ const msg = JSON.parse(e.data);
16
+ if (msg.from === "cusdis") {
17
+ switch (msg.event) {
18
+ case cusdisEvent.COMMENTS_LOADED:
19
+ handleCommentLoaded();
20
+ break;
21
+ case cusdisEvent.COMMENT_SENT:
22
+ handleCommentSent();
23
+ break;
24
+ default:
25
+ return;
26
+ }
27
+ }
28
+ } catch (e) {}
29
+ };
30
+
31
+ useEffect(() => {
32
+ window.addEventListener("message", onMessage);
33
+ return () => {
34
+ window.removeEventListener("message", onMessage);
35
+ };
36
+ }, []);
37
+ };
@@ -0,0 +1,63 @@
1
+ import React, { EventHandler, useEffect } from "react";
2
+ import { PopConfirm } from "@bbki.ng/components";
3
+ import classNames from "classnames";
4
+
5
+ type cornerPromptBoxProps = {
6
+ okLabel?: string;
7
+ onOk?: (() => void) | null;
8
+ onCancel?: (() => void) | null;
9
+ cancelLabel?: string;
10
+ autoCancelAfter?: number;
11
+ content: string;
12
+ showBox: boolean;
13
+ className?: string;
14
+ };
15
+
16
+ export const CornerPromptBox = (props: cornerPromptBoxProps) => {
17
+ const { onOk, onCancel, content, showBox, autoCancelAfter, className } =
18
+ props;
19
+
20
+ useEffect(() => {
21
+ if (!autoCancelAfter || !onCancel || !showBox) {
22
+ return;
23
+ }
24
+ const timerId = setTimeout(() => {
25
+ onCancel();
26
+ }, autoCancelAfter);
27
+
28
+ return () => {
29
+ clearTimeout(timerId);
30
+ };
31
+ }, [showBox]);
32
+
33
+ const handleOk = async () => {
34
+ if (!onOk) {
35
+ return;
36
+ }
37
+ return onOk();
38
+ };
39
+
40
+ if (!showBox) {
41
+ return null;
42
+ }
43
+
44
+ return (
45
+ <div className="p-0 m-0 w-0 h-0">
46
+ <div
47
+ className={classNames(
48
+ "left-0 bottom-0 m-32 z-10 bg-white fixed",
49
+ className
50
+ )}
51
+ >
52
+ <PopConfirm
53
+ onOk={handleOk}
54
+ onCancel={
55
+ onCancel as EventHandler<React.MouseEvent<HTMLButtonElement>>
56
+ }
57
+ >
58
+ {content}
59
+ </PopConfirm>
60
+ </div>
61
+ </div>
62
+ );
63
+ };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import classnames from "classnames";
3
+
4
+ type disabledTextProps = {
5
+ children: any;
6
+ className?: string;
7
+ };
8
+
9
+ export const DisabledText = (props: disabledTextProps) => {
10
+ return (
11
+ <span className={classnames("text-gray-400", props.className)}>
12
+ {props.children}
13
+ </span>
14
+ );
15
+ };
16
+
17
+ export const SmallDisabledText = (props: disabledTextProps) => {
18
+ return (
19
+ <small>
20
+ <DisabledText>{props.children}</DisabledText>
21
+ </small>
22
+ );
23
+ };
@@ -0,0 +1,37 @@
1
+ import React, { FunctionComponent, useEffect, useState } from "react";
2
+ import classnames from "classnames";
3
+ import { DEFAULT_DELAY } from "@/constants";
4
+
5
+ type FadeOutCoverProps = {
6
+ duration: number; // ms
7
+ coverColor: string;
8
+ };
9
+
10
+ export const FadeOutCover: FunctionComponent<FadeOutCoverProps> = (props) => {
11
+ const [hidden, setHidden] = useState(false);
12
+ useEffect(() => {
13
+ const id = setTimeout(() => {
14
+ setHidden(true);
15
+ }, props.duration || DEFAULT_DELAY);
16
+ return () => {
17
+ clearTimeout(id);
18
+ };
19
+ }, []);
20
+ return (
21
+ <div
22
+ style={{
23
+ backgroundColor: props.coverColor || "#fff",
24
+ }}
25
+ className={classnames(
26
+ "transition-opacity",
27
+ "h-full",
28
+ "w-full",
29
+ "absolute",
30
+ "opacity-100",
31
+ {
32
+ "opacity-0": hidden,
33
+ }
34
+ )}
35
+ />
36
+ );
37
+ };
@@ -0,0 +1,13 @@
1
+ import { pathObj } from "@/types/path";
2
+ import { GITHUB_REPO_ADDRESS, ROUTES } from "@/constants";
3
+
4
+ export const FooterLinks: pathObj[] = [
5
+ {
6
+ name: "blog",
7
+ path: ROUTES.BLOG,
8
+ },
9
+ // {
10
+ // text: "contributing",
11
+ // link: "https://github.com/bbbottle/bbki.ng/blob/main/CONTRIBUTING.md",
12
+ // },
13
+ ];
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { pathObj } from "@/types/path";
3
+ import { TextColors } from "@/types/color";
4
+ // import { List } from "../list";
5
+ import { FooterLinks } from "./footer_links";
6
+ import { Link, LinkColor } from "@bbki.ng/components";
7
+
8
+ export const Footer = () => {
9
+ const renderFooterLink = (l: pathObj) => {
10
+ return (
11
+ <Link to={l.path as string} color={LinkColor.GRAY}>
12
+ {l.name}
13
+ </Link>
14
+ );
15
+ };
16
+
17
+ return renderFooterLink(FooterLinks[0]);
18
+ // return (
19
+ // <List items={FooterLinks} itemRenderer={renderFooterLink} horizontal />
20
+ // );
21
+ };
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { useHotkeys } from "react-hotkeys-hook";
4
+ import { GITHUB_REPO_ADDRESS, ROUTES } from "@/constants";
5
+
6
+ enum HotKeys {
7
+ i = "i",
8
+ e = "e",
9
+ c = "c",
10
+ t = "t",
11
+ p = "p",
12
+ a = "a",
13
+ f = "f",
14
+ b = "b",
15
+ h = "h",
16
+ s = "s",
17
+ T = "shift+t",
18
+ }
19
+
20
+ const KEY_ROUTES = [
21
+ { key: HotKeys.i, route: ROUTES.INDEX },
22
+ { key: HotKeys.c, route: ROUTES.CONTENT },
23
+ { key: HotKeys.h, route: ROUTES.HELP },
24
+ { key: HotKeys.T, route: ROUTES.TAGS },
25
+ ];
26
+
27
+ export const HotKeyNav = (props: any) => {
28
+ const nav = useNavigate();
29
+ const goto = (path: string) => {
30
+ nav(path);
31
+ };
32
+
33
+ useHotkeys(HotKeys.b, () => {
34
+ nav(-1);
35
+ });
36
+ useHotkeys(HotKeys.f, () => {
37
+ nav(1);
38
+ });
39
+ useHotkeys(HotKeys.s, () => {
40
+ window.open(GITHUB_REPO_ADDRESS);
41
+ });
42
+
43
+ KEY_ROUTES.map(({ key, route }) => {
44
+ useHotkeys(key, () => {
45
+ goto(route);
46
+ });
47
+ });
48
+
49
+ return props.children;
50
+ };
@@ -0,0 +1,43 @@
1
+ import React, { FunctionComponent, ReactElement, ReactNode } from "react";
2
+ import cls from "classnames";
3
+ import { Photo } from "@/types/photo";
4
+ import { Article, Img } from "@bbki.ng/components";
5
+
6
+ interface imgListProps {
7
+ className: string;
8
+ imgList: Photo[];
9
+ description?: any;
10
+ beforeListRenderer?: () => ReactNode;
11
+ }
12
+
13
+ const BaseImgList: FunctionComponent<imgListProps> = (props: imgListProps) => {
14
+ const { imgList, className, beforeListRenderer } = props;
15
+
16
+ return (
17
+ <div className={cls("max-h-full no-scrollbar overflow-auto", className)}>
18
+ {beforeListRenderer && <>{beforeListRenderer()}</>}
19
+ {imgList.map((img, index) => {
20
+ const isLast = index === imgList.length - 1;
21
+ return (
22
+ <div key={img.src}>
23
+ <Img {...img} className={cls({ "mb-256": !isLast })} />
24
+ </div>
25
+ );
26
+ })}
27
+ </div>
28
+ );
29
+ };
30
+
31
+ interface TitledImageListProps extends imgListProps {
32
+ title: string | ReactElement;
33
+ }
34
+
35
+ export const ImgList = (props: TitledImageListProps) => {
36
+ const { title, description, ...rest } = props;
37
+
38
+ return (
39
+ <Article title={title} description={description}>
40
+ <BaseImgList {...rest} />
41
+ </Article>
42
+ );
43
+ };
@@ -0,0 +1,27 @@
1
+ export { Footer } from "./footer";
2
+
3
+ export { DisabledText, SmallDisabledText } from "./disabled_text";
4
+
5
+ export { ImgList } from "./img_list";
6
+
7
+ export { withArticleWrapper } from "./with_wrapper";
8
+
9
+ export { HotKeyNav } from "./hotkey_nav";
10
+
11
+ export { VideoPlayer } from "./video_player";
12
+
13
+ export { ProgressBar } from "./progress_bar";
14
+
15
+ export { BlurCover } from "./blur_cover";
16
+
17
+ export { ReloadPrompt } from "./reload_prompt";
18
+
19
+ export { Stickers } from "./stickers";
20
+
21
+ export { CornerPromptBox } from "./corner_prompt_box";
22
+
23
+ export { Tags } from "./tags";
24
+
25
+ export { Comment } from "./comment";
26
+
27
+ export { MySuspense } from "./my_suspense";
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { Table, Link, Error } from "@bbki.ng/components";
3
+ import { TableSkeleton } from "@/components/table_skeleton";
4
+ import { useMovies } from "@/hooks";
5
+
6
+ const CELL_STYLE = {
7
+ width: 100,
8
+ maxWidth: 100,
9
+ };
10
+
11
+ export const MovieList = () => {
12
+ const { movies, isLoading, isError } = useMovies();
13
+ if (isError) {
14
+ return <Error error={isError} />;
15
+ }
16
+
17
+ if (isLoading) {
18
+ return <TableSkeleton />;
19
+ }
20
+
21
+ const renderHeader = () => {
22
+ return (
23
+ <>
24
+ <Table.HCell style={CELL_STYLE}>名字</Table.HCell>
25
+ <Table.HCell style={CELL_STYLE}>状态</Table.HCell>
26
+ </>
27
+ );
28
+ };
29
+
30
+ const renderRow = (index: number) => {
31
+ const { name, link, status } = movies[index];
32
+ return (
33
+ <>
34
+ <Table.Cell style={CELL_STYLE}>
35
+ <Link to={link} external>
36
+ {name}
37
+ </Link>
38
+ </Table.Cell>
39
+ <Table.Cell style={CELL_STYLE}>{status}</Table.Cell>
40
+ </>
41
+ );
42
+ };
43
+ return (
44
+ <Table
45
+ rowCount={movies.length}
46
+ rowRenderer={renderRow}
47
+ headerRenderer={renderHeader}
48
+ />
49
+ );
50
+ };
@@ -0,0 +1,14 @@
1
+ import React, { ReactElement, ReactNode, Suspense } from "react";
2
+ import { Spinner } from "./Spinner";
3
+ import { ErrorBoundary } from "@bbki.ng/components";
4
+
5
+ export const MySuspense = (props: {
6
+ children: ReactNode;
7
+ fallback?: ReactElement;
8
+ }) => {
9
+ return (
10
+ <ErrorBoundary>
11
+ <Suspense fallback={<Spinner />}>{props.children}</Suspense>
12
+ </ErrorBoundary>
13
+ );
14
+ };
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import classnames from "classnames";
3
+ import { BgColors } from "@/types/color";
4
+ import { floatNumberToPercentageString } from "@/utils";
5
+
6
+ type progressBarProps = {
7
+ bgColor?: BgColors;
8
+ fgColor?: BgColors;
9
+ height?: number;
10
+ progress: number;
11
+ };
12
+
13
+ export const ProgressBar = (props: progressBarProps) => {
14
+ const {
15
+ height = 2,
16
+ fgColor = BgColors.BLUE,
17
+ bgColor = BgColors.GRAY,
18
+ progress,
19
+ } = props;
20
+
21
+ const wrapperCls = classnames("w-full", "text-left", bgColor);
22
+ const innerBarCls = classnames("h-full", fgColor);
23
+ return (
24
+ <div className={wrapperCls} style={{ height }}>
25
+ <div
26
+ className={innerBarCls}
27
+ style={{ width: floatNumberToPercentageString(progress) }}
28
+ />
29
+ </div>
30
+ );
31
+ };
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+
3
+ // @ts-ignore
4
+ import { useRegisterSW } from "virtual:pwa-register/react";
5
+ import { CornerPromptBox } from "@/components";
6
+
7
+ export const ReloadPrompt = () => {
8
+ const {
9
+ needRefresh: [needRefresh, setNeedRefresh],
10
+ updateServiceWorker,
11
+ } = useRegisterSW({
12
+ onRegisterError(error: any) {
13
+ console.log("SW registration error", error);
14
+ },
15
+ });
16
+
17
+ const close = () => {
18
+ // setOfflineReady(false);
19
+ setNeedRefresh(false);
20
+ };
21
+
22
+ // @ts-ignore
23
+ const appVer = GLOBAL_BBKING_VERSION;
24
+ console.log("appVer: ", appVer);
25
+
26
+ return (
27
+ <CornerPromptBox
28
+ className="z-[1010]"
29
+ content={`🚀 发现新版本(当前 v${appVer})。获取更新?`}
30
+ showBox={needRefresh}
31
+ onCancel={close}
32
+ cancelLabel="关闭"
33
+ onOk={needRefresh ? () => updateServiceWorker(true) : close}
34
+ />
35
+ );
36
+ };
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { Img } from "@bbki.ng/components";
3
+ import { NavLink, useLocation } from "react-router-dom";
4
+
5
+ export const Stickers = () => {
6
+ const { pathname } = useLocation();
7
+
8
+ if (pathname !== "/") {
9
+ return null;
10
+ }
11
+
12
+ return (
13
+ <div className="fixed bottom-128 md:bottom-256 right-16 z-10 md:right-64">
14
+ <NavLink to="projects">
15
+ <Img
16
+ size="large"
17
+ src="https://zjh-im-res.oss-cn-shenzhen.aliyuncs.com/image/stickers/sticker-water-delivery.jpg"
18
+ className="-rotate-[32.95deg] relative top-0 md:-top-64 left-64 md:left-[unset]"
19
+ width={126}
20
+ height={59}
21
+ renderedWidth={126}
22
+ />
23
+ </NavLink>
24
+ <NavLink to="404">
25
+ <Img
26
+ size="large"
27
+ src="https://zjh-im-res.oss-cn-shenzhen.aliyuncs.com/image/stickers/sticker-lock.jpg"
28
+ className="rotate-[10.77deg] relative left-64 md:left-[unset]"
29
+ width={93}
30
+ height={50}
31
+ renderedWidth={93}
32
+ />
33
+ </NavLink>
34
+ <NavLink to="blog">
35
+ <Img
36
+ size="large"
37
+ src="https://zjh-im-res.oss-cn-shenzhen.aliyuncs.com/image/stickers/bar-code.jpg"
38
+ className="-rotate-[13.37deg] relative top-64 left-64 md:left-[unset] md:right-256 md:top-128"
39
+ width={176}
40
+ height={60}
41
+ renderedWidth={176}
42
+ />
43
+ </NavLink>
44
+ </div>
45
+ );
46
+ };
@@ -0,0 +1,40 @@
1
+ import { Skeleton, SkeletonColor, Table } from "@bbki.ng/components";
2
+ import React from "react";
3
+
4
+ const CELL_STYLE = {
5
+ width: 100,
6
+ maxWidth: 100,
7
+ };
8
+
9
+ export const TableSkeleton = (props: {
10
+ headers?: string[];
11
+ cellStyle?: { width: number; maxWidth: number };
12
+ }) => {
13
+ const { headers, cellStyle = CELL_STYLE } = props;
14
+ const [name = "名字", status = "状态"] = headers || [];
15
+ const renderHeader = () => {
16
+ return (
17
+ <>
18
+ <Table.HCell style={cellStyle}>{name}</Table.HCell>
19
+ <Table.HCell style={cellStyle}>{status}</Table.HCell>
20
+ </>
21
+ );
22
+ };
23
+
24
+ const renderRow = () => {
25
+ return (
26
+ <>
27
+ <Table.Cell style={cellStyle}>
28
+ <Skeleton width={84} height={16} bgColor={SkeletonColor.BLUE} />
29
+ </Table.Cell>
30
+ <Table.Cell style={cellStyle}>
31
+ <Skeleton width={32} height={16} bgColor={SkeletonColor.GRAY} />
32
+ </Table.Cell>
33
+ </>
34
+ );
35
+ };
36
+
37
+ return (
38
+ <Table rowCount={3} rowRenderer={renderRow} headerRenderer={renderHeader} />
39
+ );
40
+ };
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import classnames from "classnames";
3
+ import { List, Tag, Link, TitledList } from "@bbki.ng/components";
4
+ import { ROUTES } from "@/constants";
5
+
6
+ type MyTag =
7
+ | {
8
+ path: string;
9
+ name: string;
10
+ }
11
+ | string;
12
+
13
+ type Tags = {
14
+ tags: MyTag[];
15
+ className?: string;
16
+ inline?: boolean;
17
+ };
18
+
19
+ export const Tags = (props: Tags) => {
20
+ const { inline, className, tags } = props;
21
+
22
+ const renderTag = (tag: MyTag) => {
23
+ const TagComp = inline ? Tag : Link;
24
+ if (typeof tag === "string") {
25
+ return <TagComp to={`${ROUTES.TAGS}/${tag}`}>{tag}</TagComp>;
26
+ }
27
+
28
+ return <TagComp to={tag.path}>{tag.name}</TagComp>;
29
+ };
30
+
31
+ if (inline) {
32
+ return (
33
+ <>
34
+ <List
35
+ items={tags}
36
+ itemRenderer={renderTag}
37
+ horizontal={inline}
38
+ className={classnames(className, "inline-flex", "flex-wrap")}
39
+ />
40
+ </>
41
+ );
42
+ }
43
+ return (
44
+ <TitledList
45
+ title={"标签"}
46
+ items={tags}
47
+ itemRenderer={renderTag}
48
+ horizontal={inline}
49
+ className={className}
50
+ />
51
+ );
52
+ };