2022-03-18 13:51:48 +01:00

370 lines
12 KiB
TypeScript

import HorizontalLine from "components/HorizontalLine";
import Img, { getAssetURL, ImageQuality } from "components/Img";
import InsetBox from "components/InsetBox";
import LightBox from "components/LightBox";
import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import Markdown from "markdown-to-jsx";
import { slugify } from "queries/helpers";
import React, { useState } from "react";
type ScenBreakProps = {
className?: string;
text: string;
};
export default function Markdawn(props: ScenBreakProps): JSX.Element {
const appLayout = useAppLayout();
const text = preprocessMarkDawn(props.text);
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
if (text) {
return (
<>
<LightBox
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
<Markdown
className={`formatted ${props.className}`}
options={{
slugify: slugify,
overrides: {
h1: {
component: (props: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => {
return (
<div className="flex flex-row place-items-center place-content-center gap-3">
<h1 id={props.id} style={props.style}>
{props.children}
</h1>
<HeaderToolTip id={props.id} />
</div>
);
},
},
h2: {
component: (props: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => {
return (
<div className="flex flex-row place-items-center place-content-center gap-3">
<h2 id={props.id} style={props.style}>
{props.children}
</h2>
<HeaderToolTip id={props.id} />
</div>
);
},
},
h3: {
component: (props: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => {
return (
<div className="flex flex-row place-items-center place-content-center gap-3">
<h3 id={props.id} style={props.style}>
{props.children}
</h3>
<HeaderToolTip id={props.id} />
</div>
);
},
},
h4: {
component: (props: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => {
return (
<div className="flex flex-row place-items-center place-content-center gap-3">
<h4 id={props.id} style={props.style}>
{props.children}
</h4>
<HeaderToolTip id={props.id} />
</div>
);
},
},
h5: {
component: (props: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => {
return (
<div className="flex flex-row place-items-center place-content-center gap-3">
<h5 id={props.id} style={props.style}>
{props.children}
</h5>
<HeaderToolTip id={props.id} />
</div>
);
},
},
h6: {
component: (props: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => {
return (
<div className="flex flex-row place-items-center place-content-center gap-3">
<h6 id={props.id} style={props.style}>
{props.children}
</h6>
<HeaderToolTip id={props.id} />
</div>
);
},
},
Sep: {
component: () => {
return <div className="my-24"></div>;
},
},
SceneBreak: {
component: (props: { id: string }) => {
return (
<div
id={props.id}
className={
"h-0 text-center text-3xl text-dark mt-16 mb-20"
}
>
* * *
</div>
);
},
},
player: {
component: () => {
return (
<span className="text-dark opacity-70">
{appLayout.playerName ? appLayout.playerName : "<player>"}
</span>
);
},
},
Transcript: {
component: (props) => {
return (
<div className="grid grid-cols-[auto_1fr] mobile:grid-cols-1 gap-x-6 gap-y-2">
{props.children}
</div>
);
},
},
Line: {
component: (props) => {
return (
<>
<strong className="text-dark opacity-60 mobile:!-mb-4">
{props.name}
</strong>
<p className="whitespace-pre-line">{props.children}</p>
</>
);
},
},
InsetBox: {
component: (props) => {
return (
<InsetBox className="my-12">{props.children}</InsetBox>
);
},
},
li: {
component: (props: { children: React.ReactNode }) => {
return (
<li
className={
props.children &&
props.children?.toString().length > 100
? "my-4"
: ""
}
>
{props.children}
</li>
);
},
},
Highlight: {
component: (props: { children: React.ReactNode }) => {
return <mark>{props.children}</mark>;
},
},
footer: {
component: (props: { children: React.ReactNode }) => {
return (
<>
<HorizontalLine />
<div>{props.children}</div>
</>
);
},
},
img: {
component: (props: {
alt: string;
src: string;
width?: number;
height?: number;
caption?: string;
name?: string;
}) => {
return (
<div
className="my-8 cursor-pointer"
onClick={() => {
setLightboxOpen(true);
setLightboxImages([
props.src.startsWith("/uploads/")
? getAssetURL(props.src, ImageQuality.Large)
: props.src,
]);
setLightboxIndex(0);
}}
>
{props.src.startsWith("/uploads/") ? (
<div className="relative w-full aspect-video">
<Img
image={{
__typename: "UploadFile",
alternativeText: props.alt,
url: props.src,
width: props.width || 1500,
height: props.height || 1000,
caption: props.caption || "",
name: props.name || "",
}}
layout="fill"
objectFit="contain"
quality={ImageQuality.Medium}
></Img>
</div>
) : (
<div className="grid place-content-center">
<img {...props} className="max-h-[50vh] " />
</div>
)}
</div>
);
},
},
},
}}
>
{text}
</Markdown>
</>
);
}
return <></>;
}
function HeaderToolTip(props: { id: string }) {
return (
<ToolTip content={"Copy anchor link"} trigger="mouseenter">
<ToolTip content={"Copied! 👍"} trigger="click">
<span
className="material-icons transition-color hover:text-dark cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(
process.env.NEXT_PUBLIC_URL_SELF +
window.location.pathname +
"#" +
props.id
);
}}
>
link
</span>
</ToolTip>
</ToolTip>
);
}
export function preprocessMarkDawn(text: string): string {
let scenebreakIndex = 0;
const visitedSlugs: string[] = [];
const result = text.split("\n").map((line) => {
if (line === "* * *" || line === "---") {
scenebreakIndex++;
return `<SceneBreak id="scene-break-${scenebreakIndex}">`;
}
if (line.startsWith("# ")) {
return markdawnHeadersParser(headerLevels.h1, line, visitedSlugs);
}
if (line.startsWith("## ")) {
return markdawnHeadersParser(headerLevels.h2, line, visitedSlugs);
}
if (line.startsWith("### ")) {
return markdawnHeadersParser(headerLevels.h3, line, visitedSlugs);
}
if (line.startsWith("#### ")) {
return markdawnHeadersParser(headerLevels.h4, line, visitedSlugs);
}
if (line.startsWith("##### ")) {
return markdawnHeadersParser(headerLevels.h5, line, visitedSlugs);
}
if (line.startsWith("###### ")) {
return markdawnHeadersParser(headerLevels.h6, line, visitedSlugs);
}
return line;
});
return result.join("\n");
}
enum headerLevels {
h1 = 1,
h2 = 2,
h3 = 3,
h4 = 4,
h5 = 5,
h6 = 6,
}
function markdawnHeadersParser(
headerLevel: headerLevels,
line: string,
visitedSlugs: string[]
): string {
const lineText = line.slice(headerLevel + 1);
let slug = slugify(lineText);
let newSlug = slug;
let index = 2;
while (visitedSlugs.includes(newSlug)) {
newSlug = `${slug}-${index}`;
index++;
}
visitedSlugs.push(newSlug);
return `<${headerLevels[headerLevel]} id="${newSlug}">${lineText}</${headerLevels[headerLevel]}>`;
}
function getAssetUrl(): React.SetStateAction<string[]> {
throw new Error("Function not implemented.");
}