test-template

This commit is contained in:
saml33 2021-10-22 12:49:46 +11:00
parent 8eff1329db
commit 3556a1fc60
5 changed files with 566 additions and 500 deletions

View File

@ -1,65 +1,92 @@
import { IncomingMessage } from 'http';
import { parse } from 'url';
import { ParsedRequest, Theme } from './types';
import { IncomingMessage } from "http";
import { parse } from "url";
import { ParsedRequest, Theme } from "./types";
export function parseRequest(req: IncomingMessage) {
console.log('HTTP ' + req.url);
const { pathname, query } = parse(req.url || '/', true);
const { fontSize, images, widths, heights, theme, md } = (query || {});
console.log("HTTP " + req.url);
const { pathname, query } = parse(req.url || "/", true);
const {
fontSize,
images,
market,
pnl,
avgEntry,
markPrice,
widths,
heights,
theme,
md,
} = query || {};
if (Array.isArray(fontSize)) {
throw new Error('Expected a single fontSize');
}
if (Array.isArray(theme)) {
throw new Error('Expected a single theme');
}
const arr = (pathname || '/').slice(1).split('.');
let extension = '';
let text = '';
if (arr.length === 0) {
text = '';
} else if (arr.length === 1) {
text = arr[0];
} else {
extension = arr.pop() as string;
text = arr.join('.');
}
if (Array.isArray(fontSize)) {
throw new Error("Expected a single fontSize");
}
if (Array.isArray(theme)) {
throw new Error("Expected a single theme");
}
const parsedRequest: ParsedRequest = {
fileType: extension === 'jpeg' ? extension : 'png',
text: decodeURIComponent(text),
theme: theme === 'dark' ? 'dark' : 'light',
md: md === '1' || md === 'true',
fontSize: fontSize || '96px',
images: getArray(images),
widths: getArray(widths),
heights: getArray(heights),
};
parsedRequest.images = getDefaultImages(parsedRequest.images, parsedRequest.theme);
return parsedRequest;
const arr = (pathname || "/").slice(1).split(".");
let extension = "";
let side = "";
if (arr.length === 0) {
side = "";
} else if (arr.length === 1) {
side = arr[0];
} else {
extension = arr.pop() as string;
side = arr.join(".");
}
const parsedMarket = Array.isArray(market) ? market[0] : market;
const parsedPnl = Array.isArray(pnl) ? pnl[0] : pnl;
const parsedAvgEntry = Array.isArray(avgEntry) ? avgEntry[0] : avgEntry;
const parsedMarkPrice = Array.isArray(markPrice) ? markPrice[0] : markPrice;
const parsedRequest: ParsedRequest = {
fileType: extension === "jpeg" ? extension : "png",
market: decodeURIComponent(parsedMarket!),
pnl: decodeURIComponent(parsedPnl!),
avgEntry: decodeURIComponent(parsedAvgEntry!),
markPrice: decodeURIComponent(parsedMarkPrice!),
side: decodeURIComponent(side),
theme: theme === "dark" ? "dark" : "light",
md: md === "1" || md === "true",
fontSize: fontSize || "96px",
images: getArray(images),
widths: getArray(widths),
heights: getArray(heights),
};
parsedRequest.images = getDefaultImages(
parsedRequest.images,
parsedRequest.theme
);
return parsedRequest;
}
function getArray(stringOrArray: string[] | string | undefined): string[] {
if (typeof stringOrArray === 'undefined') {
return [];
} else if (Array.isArray(stringOrArray)) {
return stringOrArray;
} else {
return [stringOrArray];
}
if (typeof stringOrArray === "undefined") {
return [];
} else if (Array.isArray(stringOrArray)) {
return stringOrArray;
} else {
return [stringOrArray];
}
}
function getDefaultImages(images: string[], theme: Theme): string[] {
const defaultImage = theme === 'light'
? 'https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-black.svg'
: 'https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-white.svg';
const defaultImage =
theme === "light"
? "https://trade.mango.markets/assets/icons/logo.svg"
: "https://trade.mango.markets/assets/icons/logo.svg";
if (!images || !images[0]) {
return [defaultImage];
}
if (!images[0].startsWith('https://assets.vercel.com/') && !images[0].startsWith('https://assets.zeit.co/')) {
images[0] = defaultImage;
}
return images;
if (!images || !images[0]) {
return [defaultImage];
}
if (
!images[0].startsWith("https://assets.vercel.com/") &&
!images[0].startsWith("https://assets.zeit.co/")
) {
images[0] = defaultImage;
}
return images;
}

View File

@ -1,62 +1,22 @@
import marked from "marked";
import { sanitizeHtml } from "./sanitizer";
import { ParsedRequest } from "./types";
import { readFileSync } from 'fs';
import marked from 'marked';
import { sanitizeHtml } from './sanitizer';
import { ParsedRequest } from './types';
const twemoji = require('twemoji');
const twOptions = { folder: 'svg', ext: '.svg' };
const emojify = (text: string) => twemoji.parse(text, twOptions);
const rglr = readFileSync(`${__dirname}/../_fonts/Inter-Regular.woff2`).toString('base64');
const bold = readFileSync(`${__dirname}/../_fonts/Inter-Bold.woff2`).toString('base64');
const mono = readFileSync(`${__dirname}/../_fonts/Vera-Mono.woff2`).toString('base64');
function getCss(theme: string, fontSize: string) {
let background = 'white';
let foreground = 'black';
let radial = 'lightgray';
if (theme === 'dark') {
background = 'black';
foreground = 'white';
radial = 'dimgray';
}
return `
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: normal;
src: url(data:font/woff2;charset=utf-8;base64,${rglr}) format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: bold;
src: url(data:font/woff2;charset=utf-8;base64,${bold}) format('woff2');
}
@font-face {
font-family: 'Vera';
font-style: normal;
font-weight: normal;
src: url(data:font/woff2;charset=utf-8;base64,${mono}) format("woff2");
}
function getCss(side: string, pnl: string) {
const pnlColor = parseFloat(pnl) >= 0 ? "#AFD803" : "#E54033";
const sideColor = side.toLowerCase() === "long" ? "#AFD803" : "#E54033";
return `
body {
background: ${background};
background-image: radial-gradient(circle at 25px 25px, ${radial} 2%, transparent 0%), radial-gradient(circle at 75px 75px, ${radial} 2%, transparent 0%);
background: #141026;
background-size: 100px 100px;
color: white;
font-family: 'Lato', sans-serif;
height: 100vh;
display: flex;
text-align: center;
align-items: center;
justify-content: center;
padding: 120px 256px;
}
code {
color: #D400FF;
font-family: 'Vera';
white-space: pre-wrap;
letter-spacing: -5px;
}
@ -87,60 +47,142 @@ function getCss(theme: string, fontSize: string) {
margin: 150px;
}
.emoji {
height: 1em;
width: 1em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
.side-market {
display: flex;
font-size: 40px;
align-items: center;
justify-content: center;
padding: 40px 40px 24px;
}
.side {
color: ${sideColor};
padding: 0px 24px;
}
.divider {
color: rgba(255,255,255,0.5);
margin-bottom: 8px;
}
.market {
padding: 0px 24px;
}
.pnl {
border: 4px ${pnlColor} solid;
border-radius: 24px;
color: ${pnlColor};
display: flex;
font-size: 140px;
font-weight: bold;
height: 240px;
align-items: center;
justify-content: center;
}
.heading {
font-family: 'Inter', sans-serif;
font-size: ${sanitizeHtml(fontSize)};
font-style: normal;
color: ${foreground};
line-height: 1.8;
.trade-details-wrapper {
display: flex;
justify-content: space-between;
padding: 0px 200px;
}
.trade-details {
font-size: 56px;
font-weight: bold;
line-height: 0.5;
padding: 100px;
}
.label {
color: rgba(255,255,255,0.5);
font-size: 40px;
font-weight: normal;
}`;
}
export function getHtml(parsedReq: ParsedRequest) {
const { text, theme, md, fontSize, images, widths, heights } = parsedReq;
return `<!DOCTYPE html>
const {
market,
side,
pnl,
avgEntry,
markPrice,
md,
images,
widths,
heights,
} = parsedReq;
return `<!DOCTYPE html>
<html>
<meta charset="utf-8">
<title>Generated Image</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;900&display=swap" rel="stylesheet">
<style>
${getCss(theme, fontSize)}
${getCss(side, pnl)}
</style>
<body>
<div>
<div class="spacer">
<div class="logo-wrapper">
${images.map((img, i) =>
getPlusSign(i) + getImage(img, widths[i], heights[i])
).join('')}
${images
.map(
(img, i) =>
getPlusSign(i) + getImage(img, widths[i], heights[i])
)
.join("")}
</div>
<div class="spacer">
<div class="heading">${emojify(
md ? marked(text) : sanitizeHtml(text)
)}
<div class="side-market"><span class="side">${
md ? marked(side.toUpperCase()) : sanitizeHtml(side.toUpperCase())
}</span>
<span class="divider">|</span>
<span class="market">${
md ? marked(market) : sanitizeHtml(market)
}</span>
</div>
<div class="pnl">${md ? marked(`${pnl}%`) : sanitizeHtml(`${pnl}%`)}
</div>
<div class="trade-details-wrapper">
<div class="trade-details">
<div class="label">Avg Entry Price</div>
<div>${
md
? marked(`$${parseFloat(avgEntry).toLocaleString()}`)
: sanitizeHtml(
`$${parseFloat(avgEntry).toLocaleString()}`
)
}
</div>
</div>
<div class="trade-details">
<div class="label">Mark Price</div>
<div>${
md
? marked(`$${parseFloat(markPrice).toLocaleString()}`)
: sanitizeHtml(
`$${parseFloat(markPrice).toLocaleString()}`
)
}
</div>
</div>
</div>
</div>
</body>
</html>`;
}
function getImage(src: string, width ='auto', height = '225') {
return `<img
function getImage(src: string, width = "auto", height = "204") {
return `<img
class="logo"
alt="Generated Image"
src="${sanitizeHtml(src)}"
width="${sanitizeHtml(width)}"
height="${sanitizeHtml(height)}"
/>`
/>`;
}
function getPlusSign(i: number) {
return i === 0 ? '' : '<div class="plus">+</div>';
return i === 0 ? "" : '<div class="plus">+</div>';
}

View File

@ -1,13 +1,17 @@
export type FileType = 'png' | 'jpeg';
export type Theme = 'light' | 'dark';
export type FileType = "png" | "jpeg";
export type Theme = "light" | "dark";
export interface ParsedRequest {
fileType: FileType;
text: string;
theme: Theme;
md: boolean;
fontSize: string;
images: string[];
widths: string[];
heights: string[];
fileType: FileType;
side: string;
market: string;
pnl: string;
avgEntry: string;
markPrice: string;
theme: Theme;
md: boolean;
fontSize: string;
images: string[];
widths: string[];
heights: string[];
}

View File

@ -1,7 +1,7 @@
{
"private": true,
"engines": {
"node": "14.x"
"node": "16.x"
},
"scripts": {
"build": "tsc -p api/tsconfig.json && tsc -p web/tsconfig.json"

View File

@ -1,419 +1,412 @@
import { ParsedRequest, Theme, FileType } from '../api/_lib/types';
const { H, R, copee } = (window as any);
import { ParsedRequest, 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;
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 }
)
);
}
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;
text: string;
value: string;
}
interface DropdownProps {
options: DropdownOption[];
value: string;
onchange: (val: string) => void;
small: boolean;
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 },
'▼'
)
);
}
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;
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);
}
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 FieldProps {
label: string;
input: any;
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),
),
);
}
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;
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 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 fileTypeOptions: DropdownOption[] = [
{ text: 'PNG', value: 'png' },
{ text: 'JPEG', value: 'jpeg' },
{ 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' },
{ text: "Plain Text", value: "0" },
{ text: "Markdown", value: "1" },
];
const imageLightOptions: DropdownOption[] = [
{ 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' },
{
text: "Mango",
value: "https://trade.mango.markets/assets/icons/logo.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: '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' },
{
text: "Mango",
value: "https://trade.mango.markets/assets/icons/logo.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' },
{ 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' },
{ 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;
loading: boolean;
showToast: boolean;
messageToast: string;
selectedImageIndex: number;
widths: string[];
heights: string[];
overrideUrl: URL | null;
side: string;
market: string;
pnl: string;
avgEntry: string;
markPrice: string;
}
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);
const setLoadingState = (newState: Partial<AppState>) => {
window.clearTimeout(timeout);
if (state.overrideUrl && state.overrideUrl !== newState.overrideUrl) {
newState.overrideUrl = state.overrideUrl;
}
for (let width of widths) {
url.searchParams.append('widths', width);
}
for (let height of heights) {
url.searchParams.append('heights', height);
if (newState.overrideUrl) {
timeout = window.setTimeout(() => setState({ overrideUrl: null }), 200);
}
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);
setState({ ...newState, loading: true });
};
const {
fileType = "png",
fontSize = "100px",
theme = "light",
md = true,
side = "Long",
market = "BTC-PERP",
pnl = "0.00",
avgEntry = "0.00",
markPrice = "0.00",
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(side)}.${fileType}`;
url.searchParams.append("theme", theme);
url.searchParams.append("market", market);
url.searchParams.append("pnl", pnl);
url.searchParams.append("avgEntry", avgEntry);
url.searchParams.append("markPrice", markPrice);
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: "File Type",
input: H(Dropdown, {
options: fileTypeOptions,
value: fileType,
onchange: (val: FileType) => setLoadingState({ fileType: val }),
}),
}),
H(Field, {
label: "Text Type",
input: H(Dropdown, {
options: markdownOptions,
value: mdValue,
onchange: (val: string) => setLoadingState({ md: val === "1" }),
}),
}),
H(Field, {
label: "Side",
input: H(TextInput, {
value: side,
oninput: (val: string) => {
console.log("oninput " + val);
setLoadingState({ side: val, overrideUrl: url });
},
}),
}),
H(Field, {
label: "Market",
input: H(TextInput, {
value: market,
oninput: (val: string) => {
console.log("oninput " + val);
setLoadingState({ market: val });
},
}),
}),
H(Field, {
label: "PnL",
input: H(TextInput, {
value: pnl,
oninput: (val: string) => {
console.log("oninput " + val);
setLoadingState({ pnl: val });
},
}),
}),
H(Field, {
label: "Avg Entry Price",
input: H(TextInput, {
value: avgEntry,
oninput: (val: string) => {
console.log("oninput " + val);
setLoadingState({ avgEntry: val });
},
}),
}),
H(Field, {
label: "Mark Price",
input: H(TextInput, {
value: markPrice,
oninput: (val: string) => {
console.log("oninput " + val);
setLoadingState({ markPrice: val });
},
}),
}),
H(Field, {
label: "Image",
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 });
},
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,
}),
H(Dropdown, {
options: heightOptions,
value: heights[0],
small: true,
onchange: (val: string) => {
let clone = [...heights];
clone[0] = val;
setLoadingState({ heights: clone });
},
})
)
),
})
);
)
),
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'));
R(H(App), document.getElementById("app"));