mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
358 lines
12 KiB
TypeScript
358 lines
12 KiB
TypeScript
import React, { HTMLAttributes, useState } from "react";
|
|
import snarkdown from "snarkdown";
|
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
|
// @ts-expect-error no types
|
|
import { useDoc } from "@docusaurus/theme-common/internal";
|
|
import { useHistory, useLocation } from "@docusaurus/router";
|
|
import clsx from "clsx";
|
|
|
|
import { useCurrentTutorial } from "../../hooks/use-current-tutorial";
|
|
import { UnitCircle } from "../unit-circle";
|
|
import { TutorialCircle } from "../tutorial-circle";
|
|
import { useTutorialUIPackage } from "../../hooks/use-tutorial-ui-package";
|
|
import { PreferredUIPackage } from "../../context/TutorialUIPackageContext";
|
|
|
|
const uiNames: Record<PreferredUIPackage, string> = {
|
|
headless: "Headless",
|
|
antd: "Ant Design",
|
|
mui: "Material UI",
|
|
mantine: "Mantine",
|
|
"chakra-ui": "Chakra UI",
|
|
};
|
|
const baseIconUrl =
|
|
"https://refine.ams3.cdn.digitaloceanspaces.com/website/static/icons/colored/ui-framework-";
|
|
|
|
type LinkWithIdProps = HTMLAttributes<HTMLAnchorElement> & {
|
|
id: string;
|
|
isCurrent?: boolean;
|
|
dangerouslySetInnerHTML?: { __html: string };
|
|
};
|
|
|
|
const LinkWithId = ({
|
|
id,
|
|
isCurrent,
|
|
className,
|
|
dangerouslySetInnerHTML,
|
|
...rest
|
|
}: LinkWithIdProps) => {
|
|
const toUrl = useBaseUrl(`/docs/${id}`, { forcePrependBaseUrl: true });
|
|
|
|
return (
|
|
<a
|
|
{...rest}
|
|
href={toUrl}
|
|
className={clsx(
|
|
className,
|
|
isCurrent
|
|
? "text-gray-800 dark:text-gray-0"
|
|
: " hover:!text-refine-link-light active:!text-refine-link-light dark:hover:!text-refine-link-dark dark:active:!text-refine-link-dark",
|
|
)}
|
|
dangerouslySetInnerHTML={dangerouslySetInnerHTML}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const markdownConverter = (text: string) => {
|
|
const numericStartRegexp = /^\d+\.\s?/g;
|
|
// use this to get the numeric start
|
|
const _numericStart = text.match(numericStartRegexp)?.[0] || "";
|
|
const numericStartIgnore = text.replace(numericStartRegexp, "");
|
|
|
|
const marked = snarkdown(numericStartIgnore);
|
|
|
|
return `${marked}`;
|
|
};
|
|
|
|
const TutorialUIStatus = () => {
|
|
const { preferred: preferredUIPackage } = useTutorialUIPackage();
|
|
|
|
return (
|
|
<div className="mt-4">
|
|
<div
|
|
className={clsx(
|
|
"rounded",
|
|
" bg-gray-50 dark:bg-gray-800",
|
|
"p-4",
|
|
)}
|
|
>
|
|
<div className={clsx("font-semibold text-sm leading-6")}>
|
|
Current Framework
|
|
</div>
|
|
<div
|
|
className={clsx(
|
|
"flex items-center justify-between",
|
|
"pt-3",
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<img
|
|
src={`${baseIconUrl}${preferredUIPackage}.svg`}
|
|
className="h-auto w-6"
|
|
/>
|
|
<span className="text-sm leading-6">
|
|
{uiNames[preferredUIPackage]}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<LinkWithId
|
|
id="tutorial/introduction/select-framework"
|
|
className={clsx(
|
|
"underline",
|
|
"text-sm",
|
|
"text-refine-link-light",
|
|
"dark:text-refine-link-dark",
|
|
)}
|
|
>
|
|
Change
|
|
</LinkWithId>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type TocLinkProps = {
|
|
item: any;
|
|
activeId?: string;
|
|
setActiveId?: React.Dispatch<React.SetStateAction<string>>;
|
|
};
|
|
|
|
const TocLink: React.FC<TocLinkProps> = ({ item, activeId, setActiveId }) => {
|
|
const location = useLocation();
|
|
const history = useHistory();
|
|
const { hash: locationHash } = location;
|
|
|
|
React.useEffect(() => {
|
|
const targetElement = document.getElementById(item.id);
|
|
|
|
if (targetElement) {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
const hash = `#${item.id}`;
|
|
if (hash !== locationHash) {
|
|
setActiveId(item.id);
|
|
window.history.replaceState({}, "", hash);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{
|
|
rootMargin: "0px 0px -80% 0px",
|
|
},
|
|
);
|
|
|
|
observer.observe(targetElement);
|
|
|
|
return () => {
|
|
observer.unobserve(targetElement);
|
|
};
|
|
}
|
|
}, [item.id]);
|
|
|
|
return (
|
|
<a
|
|
href={`#${item.id}`}
|
|
dangerouslySetInnerHTML={{ __html: item.value }}
|
|
className={clsx(
|
|
"text-gray-800 dark:text-gray-0 hover:!text-refine-link active:!text-refine-link dark:hover:!text-refine-link-dark dark:active:!text-refine-link-dark",
|
|
activeId === item.id &&
|
|
"!text-refine-link-light dark:!text-refine-link-dark",
|
|
)}
|
|
></a>
|
|
);
|
|
};
|
|
|
|
export const TutorialTOC = () => {
|
|
const {
|
|
toc,
|
|
metadata: { id: currentDocId },
|
|
} = useDoc();
|
|
const currentTutorial = useCurrentTutorial();
|
|
const { hash } = useLocation();
|
|
|
|
const baseActiveId = `${hash}`.replace("#", "");
|
|
|
|
const [selectedUnit, setSelectedUnit] = useState(currentTutorial.unit);
|
|
const [activeId, setActiveId] = React.useState<string | undefined>(
|
|
baseActiveId,
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
setActiveId(baseActiveId);
|
|
}, [baseActiveId]);
|
|
|
|
const renderTocItem = (item: (typeof toc)[number]) => {
|
|
return (
|
|
<li
|
|
key={item.id}
|
|
style={{
|
|
marginLeft: (item.level - 2) * 12,
|
|
}}
|
|
className="mb-1"
|
|
>
|
|
<TocLink
|
|
item={item}
|
|
activeId={activeId}
|
|
setActiveId={setActiveId}
|
|
/>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const renderTOC = () => {
|
|
if (toc.length === 0) return null;
|
|
return (
|
|
<ul className="mt-1 list-none pl-0">{toc.map(renderTocItem)}</ul>
|
|
);
|
|
};
|
|
|
|
const renderUnitItem = (
|
|
doc: NonNullable<
|
|
typeof currentTutorial
|
|
>["units"][number]["docs"][number],
|
|
) => {
|
|
const formattedTitle = markdownConverter(doc.title);
|
|
const unitNo = doc.title.split(".")[0];
|
|
|
|
return (
|
|
<li
|
|
key={doc.id}
|
|
className={clsx("flex flex-row items-start gap-2", "pb-4")}
|
|
>
|
|
<div className={clsx("mt-0.5", "h-8 w-8", "flex-shrink-0")}>
|
|
<TutorialCircle
|
|
id={doc.id}
|
|
width="100%"
|
|
height="100%"
|
|
unitNo={isNaN(Number(unitNo)) ? undefined : unitNo}
|
|
isCurrent={doc.current}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-2 pt-[6px]">
|
|
<LinkWithId
|
|
id={doc.id}
|
|
isCurrent={doc.current}
|
|
className={clsx("leading-[22px]", "no-underline", {
|
|
"text-gray-900 dark:text-white":
|
|
currentDocId === doc.id,
|
|
"hover:cursor-default hover:no-underline hover:text-gray-900":
|
|
doc.current,
|
|
})}
|
|
dangerouslySetInnerHTML={{ __html: formattedTitle }}
|
|
/>
|
|
|
|
{doc.current && renderTOC()}
|
|
</div>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const renderUnitDocs = (
|
|
unit?: NonNullable<typeof currentTutorial>["units"][number],
|
|
) => {
|
|
return (
|
|
<div>
|
|
<ul className={clsx("list-none", "pl-0")}>
|
|
{unit?.docs
|
|
.sort((a, b) =>
|
|
`${a.title}`?.localeCompare(`${b.title}`),
|
|
)
|
|
.map(renderUnitItem)}
|
|
</ul>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderUnitTab = (unit: (typeof currentTutorial)["units"][number]) => {
|
|
return (
|
|
<button
|
|
key={unit.no}
|
|
type="button"
|
|
data-unit-title={unit.title}
|
|
data-unit-no={unit.no}
|
|
data-unit-current={
|
|
unit.unit === selectedUnit ? "true" : undefined
|
|
}
|
|
onClick={() => setSelectedUnit(unit.unit)}
|
|
className={clsx(
|
|
"tutorial-unit",
|
|
unit.unit === selectedUnit &&
|
|
"bg-gradient-to-t from-gray-50 dark:from-gray-700 from-40% to-gray-200 dark:to-gray-600",
|
|
"-mb-1",
|
|
"flex items-center justify-center",
|
|
"w-[40px] h-[44px]",
|
|
"cursor-pointer",
|
|
"rounded-tl-[24px] rounded-tr-[24px]",
|
|
"border-none",
|
|
"font-semibold",
|
|
)}
|
|
>
|
|
<UnitCircle
|
|
unit={unit.unit}
|
|
width="32px"
|
|
height="32px"
|
|
isSelected={unit.unit === selectedUnit}
|
|
/>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
const currentUnit:
|
|
| NonNullable<typeof currentTutorial>["units"][number]
|
|
| undefined = currentTutorial?.units.find(
|
|
(unit) => unit.unit === selectedUnit,
|
|
);
|
|
|
|
const isFirstUnit =
|
|
currentTutorial?.units?.find((el) => el?.unit === currentTutorial?.unit)
|
|
?.no === 1;
|
|
|
|
return (
|
|
<div className={clsx("tutorial-tracker", "max-h-[calc(100vh-6rem]")}>
|
|
<div
|
|
className={clsx(
|
|
"tutorial-units",
|
|
"mb-1 px-2",
|
|
"flex items-center gap-0.5",
|
|
)}
|
|
>
|
|
{currentTutorial?.units.map(renderUnitTab)}
|
|
</div>
|
|
<div
|
|
className={clsx(
|
|
"rounded",
|
|
"pt-6 px-3 pb-2",
|
|
"bg-gray-50 dark:bg-gray-700",
|
|
)}
|
|
>
|
|
{/* <div className={clsx("mb-2", "text-sm font-bold")}>
|
|
{currentUnit?.title ?? currentUnit?.unit ?? "-"}
|
|
</div> */}
|
|
<div className={clsx("text-sm")}>
|
|
{renderUnitDocs(currentUnit)}
|
|
</div>
|
|
</div>
|
|
{!isFirstUnit &&
|
|
currentDocId !== "tutorial/introduction/select-framework" && (
|
|
<TutorialUIStatus />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const useDocTOCwithTutorial = () => {
|
|
const tutorial = useCurrentTutorial();
|
|
const { frontMatter, toc } = useDoc();
|
|
const hidden = frontMatter.hide_table_of_contents;
|
|
const canRender = (!hidden && toc.length > 0) || tutorial?.isTutorial;
|
|
const tutorialTOC = canRender ? <TutorialTOC /> : undefined;
|
|
|
|
return {
|
|
hidden,
|
|
tutorialTOC,
|
|
};
|
|
};
|