256 lines
7.1 KiB
TypeScript
256 lines
7.1 KiB
TypeScript
import React, { useRef, useEffect, CSSProperties } from "react";
|
|
import { useRecoilValue, useSetRecoilState } from "recoil";
|
|
import { useHotkeys } from "react-hotkeys-hook";
|
|
import { motion } from "framer-motion";
|
|
import { Svg2Png } from "svg2png-converter";
|
|
import { saveAs } from "file-saver";
|
|
import { Copy, X, CheckCircle, Download } from "phosphor-react";
|
|
import ReactGA from "react-ga";
|
|
|
|
import {
|
|
iconWeightAtom,
|
|
iconSizeAtom,
|
|
iconColorAtom,
|
|
iconPreviewOpenAtom,
|
|
} from "@/state/atoms";
|
|
import useTransientState from "@/hooks/useTransientState";
|
|
import { IconEntry, SnippetType } from "@/lib";
|
|
import { getCodeSnippets, supportsWeight } from "@/utils";
|
|
|
|
import TagCloud from "./TagCloud";
|
|
|
|
const panelVariants = {
|
|
open: {
|
|
opacity: 1,
|
|
height: "100%",
|
|
marginTop: "4px",
|
|
marginBottom: "4px",
|
|
transition: { type: "tween", duration: 0.1 },
|
|
},
|
|
collapsed: {
|
|
opacity: 0,
|
|
height: "0px",
|
|
marginTop: "0px",
|
|
marginBottom: "0px",
|
|
transition: { type: "tween", duration: 0.1 },
|
|
},
|
|
};
|
|
|
|
const contentVariants = {
|
|
open: { opacity: 1, transition: { duration: 0.2, delay: 0.1 } },
|
|
collapsed: { opacity: 0, transition: { duration: 0 } },
|
|
};
|
|
|
|
const buttonColor = "#35313D";
|
|
const successColor = "#1FA647";
|
|
const disabledColor = "#B7B7B7";
|
|
|
|
interface InfoPanelProps {
|
|
index: number;
|
|
spans: number;
|
|
isDark: boolean;
|
|
entry: IconEntry;
|
|
}
|
|
|
|
const renderedSnippets = [
|
|
SnippetType.REACT,
|
|
SnippetType.VUE,
|
|
SnippetType.HTML,
|
|
SnippetType.FLUTTER,
|
|
];
|
|
|
|
const DetailsPanel = (props: InfoPanelProps) => {
|
|
const { index, spans, isDark, entry } = props;
|
|
const { name, Icon, categories, tags } = entry;
|
|
const weight = useRecoilValue(iconWeightAtom);
|
|
const size = useRecoilValue(iconSizeAtom);
|
|
const color = useRecoilValue(iconColorAtom);
|
|
const setOpen = useSetRecoilState(iconPreviewOpenAtom);
|
|
const [copied, setCopied] = useTransientState<SnippetType | "SVG" | false>(
|
|
false,
|
|
2000
|
|
);
|
|
const ref = useRef<SVGSVGElement>(null);
|
|
|
|
useHotkeys("esc", () => setOpen(false));
|
|
|
|
useEffect(
|
|
() => ReactGA.event({ category: "Grid", action: "Details", label: name }),
|
|
[name]
|
|
);
|
|
|
|
const buttonBarStyle: CSSProperties = {
|
|
color: isDark ? "white" : buttonColor,
|
|
};
|
|
const snippetButtonStyle: CSSProperties =
|
|
weight === "duotone"
|
|
? { color: disabledColor, userSelect: "none" }
|
|
: { color: buttonColor };
|
|
|
|
const snippets = getCodeSnippets({
|
|
displayName: Icon.displayName!,
|
|
name,
|
|
weight,
|
|
size,
|
|
color,
|
|
});
|
|
|
|
const handleCopySnippet = (
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
|
type: SnippetType
|
|
) => {
|
|
event.currentTarget.blur();
|
|
setCopied(type);
|
|
const data = snippets[type];
|
|
data && void navigator.clipboard?.writeText(data);
|
|
};
|
|
|
|
const handleCopySVG = (
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
) => {
|
|
event.currentTarget.blur();
|
|
setCopied("SVG");
|
|
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
|
|
};
|
|
|
|
const handleDownloadSVG = (
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
) => {
|
|
event.currentTarget.blur();
|
|
if (!ref.current?.outerHTML) return;
|
|
const blob = new Blob([ref.current.outerHTML]);
|
|
saveAs(blob, `${name}${weight === "regular" ? "" : `-${weight}`}.svg`);
|
|
};
|
|
|
|
const handleDownloadPNG = async (
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
) => {
|
|
event.currentTarget.blur();
|
|
if (!ref.current?.outerHTML) return;
|
|
Svg2Png.save(
|
|
ref.current,
|
|
`${name}${weight === "regular" ? "" : `-${weight}`}.png`,
|
|
{ scaleX: 2.667, scaleY: 2.667 }
|
|
);
|
|
};
|
|
|
|
return (
|
|
<motion.section
|
|
className="info-box"
|
|
animate="open"
|
|
exit="collapsed"
|
|
variants={panelVariants}
|
|
style={{
|
|
order: index + (spans - (index % spans)),
|
|
color: isDark ? "white" : "black",
|
|
}}
|
|
>
|
|
<motion.div
|
|
initial="collapsed"
|
|
animate="open"
|
|
exit="collapsed"
|
|
variants={contentVariants}
|
|
className="icon-preview"
|
|
>
|
|
<Icon ref={ref} color={color} weight={weight} size={192} />
|
|
<p>{name}</p>
|
|
<TagCloud
|
|
name={name}
|
|
tags={Array.from(
|
|
new Set<string>([...categories, ...name.split("-"), ...tags])
|
|
)}
|
|
isDark={isDark}
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial="collapsed"
|
|
animate="open"
|
|
exit="collapsed"
|
|
variants={contentVariants}
|
|
className="icon-usage"
|
|
>
|
|
{renderedSnippets.map((type) => {
|
|
const isWeightSupported = supportsWeight({ type, weight });
|
|
|
|
return (
|
|
<div className="snippet" key={type}>
|
|
{type}
|
|
<pre
|
|
tabIndex={0}
|
|
style={isWeightSupported ? undefined : snippetButtonStyle}
|
|
>
|
|
<span>
|
|
{isWeightSupported
|
|
? snippets[type]
|
|
: "This weight is not yet supported"}
|
|
</span>
|
|
<button
|
|
title="Copy snippet"
|
|
onClick={(e) => handleCopySnippet(e, type)}
|
|
disabled={!isWeightSupported}
|
|
style={isWeightSupported ? undefined : snippetButtonStyle}
|
|
>
|
|
{copied === type ? (
|
|
<CheckCircle size={24} color={successColor} weight="fill" />
|
|
) : (
|
|
<Copy
|
|
size={24}
|
|
color={
|
|
isWeightSupported
|
|
? buttonColor
|
|
: snippetButtonStyle.color
|
|
}
|
|
weight="fill"
|
|
/>
|
|
)}
|
|
</button>
|
|
</pre>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
<div className="button-row">
|
|
<button style={buttonBarStyle} onClick={handleDownloadPNG}>
|
|
<Download size={32} color="currentColor" weight="fill" /> Download
|
|
PNG
|
|
</button>
|
|
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
|
|
<Download size={32} color="currentColor" weight="fill" /> Download
|
|
SVG
|
|
</button>
|
|
<button style={buttonBarStyle} onClick={handleCopySVG}>
|
|
{copied === "SVG" ? (
|
|
<CheckCircle size={32} color={successColor} weight="fill" />
|
|
) : (
|
|
<Copy size={32} color="currentColor" weight="fill" />
|
|
)}
|
|
{copied === "SVG" ? "Copied!" : "Copy SVG"}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
<motion.span
|
|
initial="collapsed"
|
|
animate="open"
|
|
exit="collapsed"
|
|
variants={contentVariants}
|
|
title="Close"
|
|
>
|
|
<X
|
|
className="close-icon"
|
|
tabIndex={0}
|
|
color={buttonBarStyle.color}
|
|
size={32}
|
|
weight="fill"
|
|
onClick={() => setOpen(false)}
|
|
onKeyDown={(e) => {
|
|
e.key === "Enter" && setOpen(false);
|
|
}}
|
|
/>
|
|
</motion.span>
|
|
</motion.section>
|
|
);
|
|
};
|
|
|
|
export default DetailsPanel;
|