test-template
This commit is contained in:
parent
8eff1329db
commit
3556a1fc60
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>';
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
711
web/index.ts
711
web/index.ts
|
@ -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"));
|
||||
|
|
Loading…
Reference in New Issue