openpanel/documentation/src/refine-theme/landing-walkthrough-video.tsx
2024-02-05 10:23:04 +01:00

529 lines
16 KiB
TypeScript

import React from "react";
import clsx from "clsx";
import { ArrowRightIcon } from "./icons/arrow-right";
import { RedoIcon } from "./icons/redo";
import { AnimatePresence, motion, useInView } from "framer-motion";
import Highlight, { defaultProps } from "prism-react-renderer";
import { usePrismTheme } from "@docusaurus/theme-common";
const WalkthroughCodeWindow = ({ code }: { code: string }) => {
const theme = usePrismTheme();
return (
<div
className={clsx(
"w-full",
"min-w-[430px]",
"max-w-[430px]",
"rounded-lg",
"ring-4",
"ring-refine-bg",
"bg-gray-700",
"flex flex-col",
)}
>
<div
className={clsx(
"p-3",
"border-b",
"border-b-gray-800",
"flex items-center justify-start",
"gap-1",
)}
>
{["#FF5F5A", "#FFBE2E", "#2ACA44"].map((c) => (
<div
key={c}
className={clsx("rounded-full", "w-2", "h-2")}
style={{
backgroundColor: c,
}}
/>
))}
</div>
<div
className={clsx(
"px-4",
"pb-12",
"pt-4",
"w-full",
"flex-1",
"font-mono",
"text-[12px] leading-[16px]",
"text-gray-0",
)}
>
<Highlight
{...defaultProps}
theme={theme}
code={`${code ?? ""}`.trim()}
language="tsx"
>
{({ tokens, getLineProps, getTokenProps }) => (
<>
{tokens.map((line, i) => (
<div
{...getLineProps({ line })}
key={`${code}-${i}`}
>
{line.map((token, key) => {
const content =
`${token.content}`.replace(
"___",
"&nbsp;&nbsp;&nbsp;&nbsp;",
);
const {
children: _children,
...tokenProps
} = getTokenProps({
token,
});
return (
<span
{...tokenProps}
key={`${token.content}-${key}`}
dangerouslySetInnerHTML={{
__html: content,
}}
/>
);
})}
</div>
))}
</>
)}
</Highlight>
</div>
</div>
);
};
const WalkthroughNextButton = ({
onClick,
children,
}: {
onClick: () => void;
children: React.ReactNode;
}) => {
return (
<motion.button
type="button"
initial={{ opacity: 0, translateY: 30, rotateX: -90 }}
animate={{
opacity: 1,
rotateX: 0,
translateY: 0,
}}
exit={{ opacity: 0, rotateX: -90, translateY: 30 }}
transition={{
duration: 0.3,
delay: 0.4,
}}
className={clsx(
"-mt-6",
"ml-4",
"overflow-hidden",
"p-px",
"rounded-[8px]",
"relative",
"w-auto",
"flex",
"ring-4",
"ring-refine-bg",
"group",
"focus:outline-none",
"z-[1]",
)}
onClick={onClick}
>
<div
className={clsx(
"bg-walkthrough-button-shadow",
"w-[300px]",
"aspect-square",
"h-auto",
"absolute",
"-left-[calc(150px-50%)]",
"-top-[calc(150px-calc(48px/2))]",
"animate-spin-slow",
)}
/>
<div
className={clsx(
"py-3",
"px-5",
"bg-refine-walkthrough-button-bg",
"flex-shrink-0",
"flex",
"z-[1]",
"rounded-[7px]",
"text-gray-0",
"group-hover:brightness-110",
"transition-[filter]",
"duration-200",
"ease-in-out",
)}
>
{children}
</div>
</motion.button>
);
};
const WalkthroughRestartButton = ({ onClick }: { onClick: () => void }) => {
return (
<motion.button
type="button"
className={clsx(
"-mt-6",
"ml-4",
"overflow-hidden",
"p-px",
"rounded-[8px]",
"relative",
"w-auto",
"flex",
"ring-4",
"ring-refine-bg",
"group",
"focus:outline-none",
"z-[1]",
)}
initial={{ opacity: 0, translateY: 30, rotateX: -90 }}
animate={{
opacity: 1,
rotateX: 0,
translateY: 0,
}}
exit={{ opacity: 0, rotateX: -90, translateY: 30 }}
transition={{
duration: 0.3,
delay: 0.4,
}}
onClick={onClick}
>
<div
className={clsx(
"bg-walkthrough-button-alt-shadow",
"w-[300px]",
"aspect-square",
"h-auto",
"absolute",
"-left-[calc(150px-50%)]",
"-top-[calc(150px-calc(48px/2))]",
"animate-spin-slow",
)}
/>
<div
className={clsx(
"py-3",
"px-5",
"bg-refine-walkthrough-button-alt-bg",
"flex-shrink-0",
"flex",
"z-[1]",
"rounded-[7px]",
"text-gray-0",
"group-hover:brightness-110",
"transition-[filter]",
"duration-200",
"ease-in-out",
)}
>
<span
className={clsx(
"whitespace-nowrap",
"flex items-center justify-center",
"gap-2",
"text-base",
"text-gray-0",
)}
>
<span>
<RedoIcon />
</span>
<span>
<span className="font-semibold">Start Over</span>
</span>
</span>
</div>
</motion.button>
);
};
const WalkthroughNextButtonLabel = ({ text }: { text: string }) => {
return (
<span
className={clsx(
"whitespace-nowrap",
"flex items-center justify-center",
"gap-3",
"text-base",
"text-gray-0",
)}
>
<span>
<span className="font-normal">Next: </span>
<span className="font-semibold">{text}</span>
</span>
<span>
<ArrowRightIcon />
</span>
</span>
);
};
const WalkthroughPhaseWrapper = ({
onClick,
text,
code,
end,
}: {
onClick: () => void;
code: string;
text?: string;
end?: boolean;
}) => {
return (
<motion.div
initial={{ opacity: 0, y: 80, scale: 0.9 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
}}
exit={{ opacity: 0, y: -80, scale: 0.9 }}
transition={{
duration: 0.23,
}}
className={clsx("absolute", "left-0", "bottom-0")}
>
<AnimatePresence>
<WalkthroughCodeWindow code={code} />
{end && <WalkthroughRestartButton onClick={onClick} />}
{text && (
<WalkthroughNextButton onClick={onClick}>
<WalkthroughNextButtonLabel text={text} />
</WalkthroughNextButton>
)}
</AnimatePresence>
</motion.div>
);
};
const walkthroughPhases = [
"initial",
"add-your-backend",
"hook-the-table",
"add-your-ui-framework",
"add-more-crud-functions",
"add-more-routes",
"secure-your-app",
"restart",
] as const;
type WalkthroughPhase = (typeof walkthroughPhases)[number];
const phaseCheckpoints: Record<(typeof walkthroughPhases)[number], number> = {
initial: 0.0,
"add-your-backend": 0.8,
"hook-the-table": 1.7,
"add-your-ui-framework": 8,
"add-more-crud-functions": 16,
"add-more-routes": 17,
"secure-your-app": 17.9,
restart: 23.8,
};
const phaseData: Record<
WalkthroughPhase,
{
code?: string;
text?: string;
end?: boolean;
}
> = {
initial: {},
"add-your-backend": {
code: `
import { Refine } from "@refinedev/core";
/* ... */
<Route path="/orders" element={<MyCustomTable />} />
`,
text: "Add your Backend",
},
"hook-the-table": {
code: `
import dataProvider from "@refinedev/nestjsx-crud";
`,
text: "Hook the Table",
},
"add-your-ui-framework": {
code: `
import { useTable } from "@refinedev/core";
/* ... */
const tableValues = useTable();
/* ... */
<table>{...}</table>
`,
text: "Add your UI Framework",
},
"add-more-crud-functions": {
code: `
import { Table } from "antd";
import { Layout, List } from "@refinedev/antd";
import { useTable } from "@refinedev/antd";
`,
text: "Add more CRUD functions",
},
"add-more-routes": {
code: `
{
___name: "orders",
___list: "/orders",
___show: "/orders/show/:id",
___create: "/orders/create",
___edit: "/orders/edit:id",
}
`,
text: "Add more Routes",
},
"secure-your-app": {
code: `
<Route path="/orders" element={<OrdersPage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/customers" element={<CustomersPage />} />
`,
text: "Secure your App",
},
restart: {
code: `
import { AuthPage } from "@refinedev/antd";
import { Auth0Provider } from "@auth0/auth0-react";
`,
end: true,
},
};
const getNextPhase = (phase: WalkthroughPhase) => {
const currentIndex = walkthroughPhases.indexOf(phase);
if (currentIndex === -1) {
return "initial";
}
if (currentIndex === walkthroughPhases.length - 1) {
return "initial";
}
return walkthroughPhases[currentIndex + 1];
};
const isAtCheckpoint = (currentTime: number, phase: WalkthroughPhase) => {
// we can consider the checkpoint to be reached if we're at exact time or past 0.25s
const lowerBound = phaseCheckpoints[phase];
const upperBound = phaseCheckpoints[phase] + 0.125;
return currentTime >= lowerBound && currentTime <= upperBound;
};
export const LandingWalkthroughVideo = () => {
const videoRef = React.useRef<HTMLVideoElement>(null);
const [phase, setPhase] = React.useState<WalkthroughPhase>("initial");
const [isPlaying, setIsPlaying] = React.useState(false);
const inView = useInView(videoRef, {
amount: "all",
once: true,
});
const onTime = React.useCallback(
(currentTime: number) => {
const nextPhase = getNextPhase(phase);
const atNext = isAtCheckpoint(currentTime, nextPhase);
if (atNext) {
videoRef?.current?.pause();
setPhase(nextPhase);
}
},
[phase],
);
React.useEffect(() => {
if (isPlaying) {
const interval = setInterval(() => {
onTime(videoRef.current?.currentTime);
}, 10);
return () => {
clearInterval(interval);
};
}
}, [isPlaying]);
const onPhaseClick = React.useCallback(() => {
videoRef.current?.play();
}, []);
React.useEffect(() => {
if (inView) {
videoRef.current?.play();
} else {
videoRef.current?.pause();
}
}, [inView]);
return (
<div
className={clsx(
"w-full",
"flex",
"items-center",
"justify-center",
"relative",
)}
>
<video
ref={videoRef}
muted={true}
playsInline={true}
controls={false}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onEnded={() => {
if (videoRef.current) {
videoRef.current.currentTime = 0;
videoRef.current.play();
}
setPhase("initial");
}}
className={clsx(
"w-full h-full opacity-[0.99]",
"max-w-screen-walkthrough",
)}
>
<source
src="https://refine.ams3.cdn.digitaloceanspaces.com/website/static/assets/refine-walkthrough.mov"
type='video/mp4; codecs="hvc1"'
/>
<source
src="https://refine.ams3.cdn.digitaloceanspaces.com/website/static/assets/refine-walkthrough.webm"
type="video/webm;"
/>
</video>
<div className={clsx("absolute", "left-0", "bottom-11")}>
<AnimatePresence>
{phase !== "initial" && !isPlaying && (
<WalkthroughPhaseWrapper
key={phase}
code={phaseData[phase].code ?? ""}
text={phaseData[phase].text}
end={phaseData[phase].end}
onClick={onPhaseClick}
/>
)}
</AnimatePresence>
</div>
</div>
);
};