484 lines
13 KiB
TypeScript
484 lines
13 KiB
TypeScript
|
import { ParsedRequest, Theme, FileType } from "../api/_lib/types";
|
||
|
const { H, R, copee } = window as any;
|
||
|
let timeout = -1;
|
||
|
|
||
|
interface ImagePreviewProps {
|
||
|
src: string;
|
||
|
onclick: () => void;
|
||
|
onload: () => void;
|
||
|
onerror: () => void;
|
||
|
loading: boolean;
|
||
|
}
|
||
|
|
||
|
const ImagePreview = ({
|
||
|
src,
|
||
|
onclick,
|
||
|
onload,
|
||
|
onerror,
|
||
|
loading,
|
||
|
}: ImagePreviewProps) => {
|
||
|
const style = {
|
||
|
filter: loading ? "blur(5px)" : "",
|
||
|
opacity: loading ? 0.1 : 1,
|
||
|
};
|
||
|
const title = "Click to copy image URL to clipboard";
|
||
|
return H(
|
||
|
"a",
|
||
|
{ className: "image-wrapper", href: src, onclick },
|
||
|
H("img", { src, onload, onerror, style, title })
|
||
|
);
|
||
|
};
|
||
|
|
||
|
interface DropdownOption {
|
||
|
text: string;
|
||
|
value: string;
|
||
|
}
|
||
|
|
||
|
interface DropdownProps {
|
||
|
options: DropdownOption[];
|
||
|
value: string;
|
||
|
onchange: (val: string) => void;
|
||
|
small: boolean;
|
||
|
}
|
||
|
|
||
|
const Dropdown = ({ options, value, onchange, small }: DropdownProps) => {
|
||
|
const wrapper = small ? "select-wrapper small" : "select-wrapper";
|
||
|
const arrow = small ? "select-arrow small" : "select-arrow";
|
||
|
return H(
|
||
|
"div",
|
||
|
{ className: wrapper },
|
||
|
H(
|
||
|
"select",
|
||
|
{ onchange: (e: any) => onchange(e.target.value) },
|
||
|
options.map((o) =>
|
||
|
H("option", { value: o.value, selected: value === o.value }, o.text)
|
||
|
)
|
||
|
),
|
||
|
H("div", { className: arrow }, "▼")
|
||
|
);
|
||
|
};
|
||
|
|
||
|
interface TextInputProps {
|
||
|
value: string;
|
||
|
oninput: (val: string) => void;
|
||
|
}
|
||
|
|
||
|
const TextInput = ({ value, oninput }: TextInputProps) => {
|
||
|
return H(
|
||
|
"div",
|
||
|
{ className: "input-outer-wrapper" },
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "input-inner-wrapper" },
|
||
|
H("input", {
|
||
|
type: "text",
|
||
|
value,
|
||
|
oninput: (e: any) => oninput(e.target.value),
|
||
|
})
|
||
|
)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
interface ButtonProps {
|
||
|
label: string;
|
||
|
onclick: () => void;
|
||
|
}
|
||
|
|
||
|
const Button = ({ label, onclick }: ButtonProps) => {
|
||
|
return H("button", { onclick }, label);
|
||
|
};
|
||
|
|
||
|
interface FieldProps {
|
||
|
label: string;
|
||
|
input: any;
|
||
|
}
|
||
|
|
||
|
const Field = ({ label, input }: FieldProps) => {
|
||
|
return H(
|
||
|
"div",
|
||
|
{ className: "field" },
|
||
|
H(
|
||
|
"label",
|
||
|
H("div", { className: "field-label" }, label),
|
||
|
H("div", { className: "field-value" }, input)
|
||
|
)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
interface ToastProps {
|
||
|
show: boolean;
|
||
|
message: string;
|
||
|
}
|
||
|
|
||
|
const Toast = ({ show, message }: ToastProps) => {
|
||
|
const style = { transform: show ? "translate3d(0,-0px,-0px) scale(1)" : "" };
|
||
|
return H(
|
||
|
"div",
|
||
|
{ className: "toast-area" },
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "toast-outer", style },
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "toast-inner" },
|
||
|
H("div", { className: "toast-message" }, message)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
const themeOptions: DropdownOption[] = [
|
||
|
{ text: "Light", value: "light" },
|
||
|
{ text: "Dark", value: "dark" },
|
||
|
];
|
||
|
|
||
|
const fileTypeOptions: DropdownOption[] = [
|
||
|
{ text: "PNG", value: "png" },
|
||
|
{ text: "JPEG", value: "jpeg" },
|
||
|
];
|
||
|
|
||
|
const fontSizeOptions: DropdownOption[] = Array.from({ length: 10 })
|
||
|
.map((_, i) => i * 25)
|
||
|
.filter((n) => n > 0)
|
||
|
.map((n) => ({ text: n + "px", value: n + "px" }));
|
||
|
|
||
|
const markdownOptions: DropdownOption[] = [
|
||
|
{ text: "Plain Text", value: "0" },
|
||
|
{ text: "Markdown", value: "1" },
|
||
|
];
|
||
|
|
||
|
const imageLightOptions: DropdownOption[] = [
|
||
|
{
|
||
|
text: "Manifold",
|
||
|
value: "https://manifold.markets/logo.png",
|
||
|
},
|
||
|
{
|
||
|
text: "Vercel",
|
||
|
value:
|
||
|
"https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-black.svg",
|
||
|
},
|
||
|
{
|
||
|
text: "Next.js",
|
||
|
value:
|
||
|
"https://assets.vercel.com/image/upload/front/assets/design/nextjs-black-logo.svg",
|
||
|
},
|
||
|
{
|
||
|
text: "Hyper",
|
||
|
value:
|
||
|
"https://assets.vercel.com/image/upload/front/assets/design/hyper-color-logo.svg",
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const imageDarkOptions: DropdownOption[] = [
|
||
|
{
|
||
|
text: "Manifold",
|
||
|
value: "https://manifold.markets/logo.png",
|
||
|
},
|
||
|
{
|
||
|
text: "Vercel",
|
||
|
value:
|
||
|
"https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-white.svg",
|
||
|
},
|
||
|
{
|
||
|
text: "Next.js",
|
||
|
value:
|
||
|
"https://assets.vercel.com/image/upload/front/assets/design/nextjs-white-logo.svg",
|
||
|
},
|
||
|
{
|
||
|
text: "Hyper",
|
||
|
value:
|
||
|
"https://assets.vercel.com/image/upload/front/assets/design/hyper-bw-logo.svg",
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const widthOptions = [
|
||
|
{ text: "width", value: "auto" },
|
||
|
{ text: "50", value: "50" },
|
||
|
{ text: "100", value: "100" },
|
||
|
{ text: "150", value: "150" },
|
||
|
{ text: "200", value: "200" },
|
||
|
{ text: "250", value: "250" },
|
||
|
{ text: "300", value: "300" },
|
||
|
{ text: "350", value: "350" },
|
||
|
];
|
||
|
|
||
|
const heightOptions = [
|
||
|
{ text: "height", value: "auto" },
|
||
|
{ text: "50", value: "50" },
|
||
|
{ text: "100", value: "100" },
|
||
|
{ text: "150", value: "150" },
|
||
|
{ text: "200", value: "200" },
|
||
|
{ text: "250", value: "250" },
|
||
|
{ text: "300", value: "300" },
|
||
|
{ text: "350", value: "350" },
|
||
|
];
|
||
|
|
||
|
interface AppState extends ParsedRequest {
|
||
|
loading: boolean;
|
||
|
showToast: boolean;
|
||
|
messageToast: string;
|
||
|
selectedImageIndex: number;
|
||
|
widths: string[];
|
||
|
heights: string[];
|
||
|
overrideUrl: URL | null;
|
||
|
}
|
||
|
|
||
|
type SetState = (state: Partial<AppState>) => void;
|
||
|
|
||
|
const App = (_: any, state: AppState, setState: SetState) => {
|
||
|
const setLoadingState = (newState: Partial<AppState>) => {
|
||
|
window.clearTimeout(timeout);
|
||
|
if (state.overrideUrl && state.overrideUrl !== newState.overrideUrl) {
|
||
|
newState.overrideUrl = state.overrideUrl;
|
||
|
}
|
||
|
if (newState.overrideUrl) {
|
||
|
timeout = window.setTimeout(() => setState({ overrideUrl: null }), 200);
|
||
|
}
|
||
|
|
||
|
setState({ ...newState, loading: true });
|
||
|
};
|
||
|
const {
|
||
|
fileType = "png",
|
||
|
fontSize = "100px",
|
||
|
theme = "light",
|
||
|
md = true,
|
||
|
text = "**Hello** World",
|
||
|
images = [imageLightOptions[0].value],
|
||
|
widths = [],
|
||
|
heights = [],
|
||
|
showToast = false,
|
||
|
messageToast = "",
|
||
|
loading = true,
|
||
|
selectedImageIndex = 0,
|
||
|
overrideUrl = null,
|
||
|
} = state;
|
||
|
const mdValue = md ? "1" : "0";
|
||
|
const imageOptions = theme === "light" ? imageLightOptions : imageDarkOptions;
|
||
|
const url = new URL(window.location.origin);
|
||
|
url.pathname = `${encodeURIComponent(text)}.${fileType}`;
|
||
|
url.searchParams.append("theme", theme);
|
||
|
url.searchParams.append("md", mdValue);
|
||
|
url.searchParams.append("fontSize", fontSize);
|
||
|
for (let image of images) {
|
||
|
url.searchParams.append("images", image);
|
||
|
}
|
||
|
for (let width of widths) {
|
||
|
url.searchParams.append("widths", width);
|
||
|
}
|
||
|
for (let height of heights) {
|
||
|
url.searchParams.append("heights", height);
|
||
|
}
|
||
|
|
||
|
return H(
|
||
|
"div",
|
||
|
{ className: "split" },
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "pull-left" },
|
||
|
H(
|
||
|
"div",
|
||
|
H(Field, {
|
||
|
label: "Theme",
|
||
|
input: H(Dropdown, {
|
||
|
options: themeOptions,
|
||
|
value: theme,
|
||
|
onchange: (val: Theme) => {
|
||
|
const options =
|
||
|
val === "light" ? imageLightOptions : imageDarkOptions;
|
||
|
let clone = [...images];
|
||
|
clone[0] = options[selectedImageIndex].value;
|
||
|
setLoadingState({ theme: val, images: clone });
|
||
|
},
|
||
|
}),
|
||
|
}),
|
||
|
H(Field, {
|
||
|
label: "File Type",
|
||
|
input: H(Dropdown, {
|
||
|
options: fileTypeOptions,
|
||
|
value: fileType,
|
||
|
onchange: (val: FileType) => setLoadingState({ fileType: val }),
|
||
|
}),
|
||
|
}),
|
||
|
H(Field, {
|
||
|
label: "Font Size",
|
||
|
input: H(Dropdown, {
|
||
|
options: fontSizeOptions,
|
||
|
value: fontSize,
|
||
|
onchange: (val: string) => setLoadingState({ fontSize: val }),
|
||
|
}),
|
||
|
}),
|
||
|
H(Field, {
|
||
|
label: "Text Type",
|
||
|
input: H(Dropdown, {
|
||
|
options: markdownOptions,
|
||
|
value: mdValue,
|
||
|
onchange: (val: string) => setLoadingState({ md: val === "1" }),
|
||
|
}),
|
||
|
}),
|
||
|
H(Field, {
|
||
|
label: "Text Input",
|
||
|
input: H(TextInput, {
|
||
|
value: text,
|
||
|
oninput: (val: string) => {
|
||
|
console.log("oninput " + val);
|
||
|
setLoadingState({ text: val, overrideUrl: url });
|
||
|
},
|
||
|
}),
|
||
|
}),
|
||
|
H(Field, {
|
||
|
label: "Image 1",
|
||
|
input: H(
|
||
|
"div",
|
||
|
H(Dropdown, {
|
||
|
options: imageOptions,
|
||
|
value: imageOptions[selectedImageIndex].value,
|
||
|
onchange: (val: string) => {
|
||
|
let clone = [...images];
|
||
|
clone[0] = val;
|
||
|
const selected = imageOptions.map((o) => o.value).indexOf(val);
|
||
|
setLoadingState({
|
||
|
images: clone,
|
||
|
selectedImageIndex: selected,
|
||
|
});
|
||
|
},
|
||
|
}),
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "field-flex" },
|
||
|
H(Dropdown, {
|
||
|
options: widthOptions,
|
||
|
value: widths[0],
|
||
|
small: true,
|
||
|
onchange: (val: string) => {
|
||
|
let clone = [...widths];
|
||
|
clone[0] = val;
|
||
|
setLoadingState({ widths: clone });
|
||
|
},
|
||
|
}),
|
||
|
H(Dropdown, {
|
||
|
options: heightOptions,
|
||
|
value: heights[0],
|
||
|
small: true,
|
||
|
onchange: (val: string) => {
|
||
|
let clone = [...heights];
|
||
|
clone[0] = val;
|
||
|
setLoadingState({ heights: clone });
|
||
|
},
|
||
|
})
|
||
|
)
|
||
|
),
|
||
|
}),
|
||
|
...images.slice(1).map((image, i) =>
|
||
|
H(Field, {
|
||
|
label: `Image ${i + 2}`,
|
||
|
input: H(
|
||
|
"div",
|
||
|
H(TextInput, {
|
||
|
value: image,
|
||
|
oninput: (val: string) => {
|
||
|
let clone = [...images];
|
||
|
clone[i + 1] = val;
|
||
|
setLoadingState({ images: clone, overrideUrl: url });
|
||
|
},
|
||
|
}),
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "field-flex" },
|
||
|
H(Dropdown, {
|
||
|
options: widthOptions,
|
||
|
value: widths[i + 1],
|
||
|
small: true,
|
||
|
onchange: (val: string) => {
|
||
|
let clone = [...widths];
|
||
|
clone[i + 1] = val;
|
||
|
setLoadingState({ widths: clone });
|
||
|
},
|
||
|
}),
|
||
|
H(Dropdown, {
|
||
|
options: heightOptions,
|
||
|
value: heights[i + 1],
|
||
|
small: true,
|
||
|
onchange: (val: string) => {
|
||
|
let clone = [...heights];
|
||
|
clone[i + 1] = val;
|
||
|
setLoadingState({ heights: clone });
|
||
|
},
|
||
|
})
|
||
|
),
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "field-flex" },
|
||
|
H(Button, {
|
||
|
label: `Remove Image ${i + 2}`,
|
||
|
onclick: (e: MouseEvent) => {
|
||
|
e.preventDefault();
|
||
|
const filter = (arr: any[]) =>
|
||
|
[...arr].filter((_, n) => n !== i + 1);
|
||
|
const imagesClone = filter(images);
|
||
|
const widthsClone = filter(widths);
|
||
|
const heightsClone = filter(heights);
|
||
|
setLoadingState({
|
||
|
images: imagesClone,
|
||
|
widths: widthsClone,
|
||
|
heights: heightsClone,
|
||
|
});
|
||
|
},
|
||
|
})
|
||
|
)
|
||
|
),
|
||
|
})
|
||
|
),
|
||
|
H(Field, {
|
||
|
label: `Image ${images.length + 1}`,
|
||
|
input: H(Button, {
|
||
|
label: `Add Image ${images.length + 1}`,
|
||
|
onclick: () => {
|
||
|
const nextImage =
|
||
|
images.length === 1
|
||
|
? "https://cdn.jsdelivr.net/gh/remojansen/logo.ts@master/ts.svg"
|
||
|
: "";
|
||
|
setLoadingState({ images: [...images, nextImage] });
|
||
|
},
|
||
|
}),
|
||
|
})
|
||
|
)
|
||
|
),
|
||
|
H(
|
||
|
"div",
|
||
|
{ className: "pull-right" },
|
||
|
H(ImagePreview, {
|
||
|
src: overrideUrl ? overrideUrl.href : url.href,
|
||
|
loading: loading,
|
||
|
onload: () => setState({ loading: false }),
|
||
|
onerror: () => {
|
||
|
setState({
|
||
|
showToast: true,
|
||
|
messageToast: "Oops, an error occurred",
|
||
|
});
|
||
|
setTimeout(() => setState({ showToast: false }), 2000);
|
||
|
},
|
||
|
onclick: (e: Event) => {
|
||
|
e.preventDefault();
|
||
|
const success = copee.toClipboard(url.href);
|
||
|
if (success) {
|
||
|
setState({
|
||
|
showToast: true,
|
||
|
messageToast: "Copied image URL to clipboard",
|
||
|
});
|
||
|
setTimeout(() => setState({ showToast: false }), 3000);
|
||
|
} else {
|
||
|
window.open(url.href, "_blank");
|
||
|
}
|
||
|
return false;
|
||
|
},
|
||
|
})
|
||
|
),
|
||
|
H(Toast, {
|
||
|
message: messageToast,
|
||
|
show: showToast,
|
||
|
})
|
||
|
);
|
||
|
};
|
||
|
|
||
|
R(H(App), document.getElementById("app"));
|