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) => void; const App = (_: any, state: AppState, setState: SetState) => { const setLoadingState = (newState: Partial) => { 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"));