mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
fork refine
This commit is contained in:
357
documentation/src/components/tutorial-toc/index.tsx
Normal file
357
documentation/src/components/tutorial-toc/index.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user