This commit is contained in:
Gary Kwok
2024-06-17 18:05:05 +08:00
commit 56f6d03385
105 changed files with 4350 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import React, { useState } from "react";
const Accordion = ({
title,
children,
className,
}: {
title: string;
children: React.ReactNode;
className?: string;
}) => {
const [show, setShow] = useState(false);
return (
<div className={`accordion ${show && "active"} ${className}`}>
<button className="accordion-header" onClick={() => setShow(!show)}>
{title}
<svg
className="accordion-icon"
x="0px"
y="0px"
viewBox="0 0 512 512"
xmlSpace="preserve"
>
<path
fill="currentColor"
d="M505.755,123.592c-8.341-8.341-21.824-8.341-30.165,0L256.005,343.176L36.421,123.592c-8.341-8.341-21.824-8.341-30.165,0 s-8.341,21.824,0,30.165l234.667,234.667c4.16,4.16,9.621,6.251,15.083,6.251c5.462,0,10.923-2.091,15.083-6.251l234.667-234.667 C514.096,145.416,514.096,131.933,505.755,123.592z"
></path>
</svg>
</button>
<div className="accordion-content">{children}</div>
</div>
);
};
export default Accordion;

View File

@@ -0,0 +1,30 @@
import React from "react";
const Button = ({
label,
link,
style,
rel,
}: {
label: string;
link: string;
style?: string;
rel?: string;
}) => {
return (
<a
href={link}
target="_blank"
rel={`noopener noreferrer ${
rel ? (rel === "follow" ? "" : rel) : "nofollow"
}`}
className={`btn mb-4 me-4 hover:text-white no-underline ${
style === "outline" ? "btn-outline-primary" : "btn-primary"
}`}
>
{label}
</a>
);
};
export default Button;

View File

@@ -0,0 +1,85 @@
import { humanize } from "@/lib/utils/textConverter";
import React from "react";
function Notice({
type,
children,
}: {
type: string;
children: React.ReactNode;
}) {
return (
<div className={`notice ${type}`}>
<div className="notice-head">
{type === "tip" ? (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 0C18.6274 0 24 5.37258 24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0ZM12 2.4C6.69807 2.4 2.4 6.69807 2.4 12C2.4 17.3019 6.69807 21.6 12 21.6C17.3019 21.6 21.6 17.3019 21.6 12C21.6 6.69807 17.3019 2.4 12 2.4ZM15.9515 7.55147L9.6 13.9029L8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515C5.88284 12.8201 5.88284 13.5799 6.35147 14.0485L8.75147 16.4485C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485L17.6485 9.24853C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147C17.1799 7.08284 16.4201 7.08284 15.9515 7.55147Z"
fill="currentColor"
/>
</svg>
) : type === "info" ? (
<svg
width="20"
height="20"
viewBox="0 0 18 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.16109 0.993016C9.97971 1.03952 10.6611 1.42989 11.0721 2.22339L17.7981 15.8014C18.4502 17.1739 17.4403 19.0208 15.7832 19.0474H2.23859C0.730337 19.0234 -0.507163 17.3108 0.231587 15.7864L7.08321 2.20877C7.21146 1.96502 7.26996 1.89452 7.38059 1.76664C7.82534 1.25102 8.31171 0.975016 9.16109 0.993016ZM9.05046 2.49189C8.79284 2.50464 8.55696 2.64902 8.42834 2.87327C6.06134 7.36539 3.77946 11.9036 1.56546 16.4734C1.36071 16.9328 1.71209 17.5223 2.22621 17.547C6.74871 17.6201 11.2731 17.6201 15.7956 17.547C16.2925 17.523 16.666 16.953 16.459 16.4783C14.2866 11.9093 12.0471 7.37102 9.72171 2.87814C9.58446 2.63402 9.38309 2.48739 9.05046 2.49189Z"
fill="currentColor"
/>
<path
d="M9.61323 13.2153H8.35773L8.21973 7.04688H9.75723L9.61323 13.2153ZM8.17773 15.1015C8.17773 14.8731 8.25161 14.6841 8.39973 14.5338C8.54823 14.3838 8.75036 14.3084 9.00648 14.3084C9.26298 14.3084 9.46511 14.3838 9.61323 14.5338C9.76136 14.6841 9.83561 14.8731 9.83561 15.1015C9.83561 15.3216 9.76323 15.5057 9.61923 15.6539C9.47486 15.802 9.27086 15.8762 9.00648 15.8762C8.74211 15.8762 8.53811 15.802 8.39373 15.6539C8.24973 15.5057 8.17773 15.3216 8.17773 15.1015Z"
fill="currentColor"
/>
</svg>
) : type === "warning" ? (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 0C15.522 0 20 4.478 20 10C20 15.522 15.522 20 10 20C4.478 20 0 15.522 0 10C0 4.478 4.478 0 10 0ZM10 2C5.589 2 2 5.589 2 10C2 14.411 5.589 18 10 18C14.411 18 18 14.411 18 10C18 5.589 14.411 2 10 2ZM12.293 6.293L13.707 7.707L11.414 10L13.707 12.293L12.293 13.707L10 11.414L7.707 13.707L6.293 12.293L8.586 10L6.293 7.707L7.707 6.293L10 8.586L12.293 6.293Z"
fill="currentColor"
/>
</svg>
) : (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
<p className="my-0 ml-1.5">{humanize(type)}</p>
</div>
<div className="notice-body">{children}</div>
</div>
);
}
export default Notice;

View File

@@ -0,0 +1,7 @@
import React from "react";
function Tab({ name, children }: { name: string; children: React.ReactNode }) {
return <div data-name={name}>{children}</div>;
}
export default Tab;

View File

@@ -0,0 +1,76 @@
import { marked } from "marked";
import React, { useEffect, useRef, useState } from "react";
marked.use({
mangle: false,
headerIds: false,
});
const Tabs = ({ children }: { children: React.ReactElement }) => {
const [active, setActive] = useState<number>(0);
const [defaultFocus, setDefaultFocus] = useState<boolean>(false);
const tabRefs: React.RefObject<HTMLElement[]> = useRef([]);
useEffect(() => {
if (defaultFocus) {
//@ts-ignore
tabRefs.current[active]?.focus();
} else {
setDefaultFocus(true);
}
}, [active]);
const tabLinks = Array.from(
children.props.value.matchAll(
/<div\s+data-name="([^"]+)"[^>]*>(.*?)<\/div>/gs,
),
(match: RegExpMatchArray) => ({ name: match[1], children: match[0] }),
);
const handleKeyDown = (
event: React.KeyboardEvent<EventTarget>,
index: number,
) => {
if (event.key === "Enter" || event.key === " ") {
setActive(index);
} else if (event.key === "ArrowRight") {
setActive((active + 1) % tabLinks.length);
} else if (event.key === "ArrowLeft") {
setActive((active - 1 + tabLinks.length) % tabLinks.length);
}
};
return (
<div className="tab">
<ul className="tab-nav">
{tabLinks.map(
(item: { name: string; children: string }, index: number) => (
<li
key={index}
className={`tab-nav-item ${index === active && "active"}`}
role="tab"
tabIndex={index === active ? 0 : -1}
onKeyDown={(event) => handleKeyDown(event, index)}
onClick={() => setActive(index)}
//@ts-ignore
ref={(ref) => (tabRefs.current[index] = ref)}
>
{item.name}
</li>
),
)}
</ul>
{tabLinks.map((item: { name: string; children: string }, i: number) => (
<div
className={active === i ? "tab-content block px-5" : "hidden"}
key={i}
dangerouslySetInnerHTML={{
__html: marked.parse(item.children),
}}
/>
))}
</div>
);
};
export default Tabs;

View File

@@ -0,0 +1,32 @@
import React from "react";
function Video({
title,
width = 500,
height = "auto",
src,
...rest
}: {
title: string;
width: number;
height: number | "auto";
src: string;
[key: string]: any;
}) {
return (
<video
className="overflow-hidden rounded-lg"
width={width}
height={height}
controls
{...rest}
>
<source
src={src.match(/^http/) ? src : `/videos/${src}`}
type="video/mp4"
/>
{title}
</video>
);
}
export default Video;

View File

@@ -0,0 +1,24 @@
import React from "react";
import LiteYouTubeEmbed from "react-lite-youtube-embed";
import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
const Youtube = ({
id,
title,
...rest
}: {
id: string;
title: string;
[key: string]: any;
}) => {
return (
<LiteYouTubeEmbed
wrapperClass="yt-lite rounded-lg"
id={id}
title={title}
{...rest}
/>
);
};
export default Youtube;