Convert to typescript

This commit is contained in:
Luck
2022-11-06 22:46:13 +00:00
parent e995261d87
commit 042bace2c6
18 changed files with 593 additions and 321 deletions

2
.gitignore vendored
View File

@@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea

View File

@@ -1,9 +1,9 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"quoteProps": "consistent"
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"quoteProps": "consistent"
}

View File

@@ -3,26 +3,40 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@monaco-editor/react": "^4.3.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0",
"content-type-parser": "^1.0.2",
"@monaco-editor/react": "4.4.6",
"copy-to-clipboard": "^3.3.1",
"history": "^5.0.0",
"local-storage": "^2.0.0",
"pako": "^2.0.3",
"react": "^18.1.0",
"react": "^18.2.0",
"react-device-detect": "^2.1.2",
"react-dom": "^18.1.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"styled-components": "^5.2.1"
"styled-components": "^5.2.1",
"typescript": "^4.8.4",
"whatwg-mimetype": "^3.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0",
"@types/jest": "^29.2.2",
"@types/node": "^18.11.9",
"@types/pako": "^2.0.0",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.8",
"@types/styled-components": "^5.1.26",
"@types/whatwg-mimetype": "^3.0.0",
"monaco-editor": "^0.34.1",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"format": "prettier --write '**/*.ts' '**/*.tsx' '**/*.css'"
},
"eslintConfig": {
"extends": [

View File

@@ -1,62 +1,27 @@
import { useCallback, useEffect, useState } from 'react';
import Editor from './components/Editor';
import parseContentType from 'content-type-parser';
import { languageIds } from './util/highlighting';
import { bytebinUrl } from './util/constants';
function getPasteIdFromUrl() {
const path = window.location.pathname;
if (path && /^\/[a-zA-Z0-9]+$/.test(path)) {
return path.substring(1);
} else {
return undefined;
}
}
async function loadFromBytebin(id) {
try {
const resp = await fetch(bytebinUrl + id);
if (resp.ok) {
const content = await resp.text();
const type = parseLanguageFromContentType(
resp.headers.get('content-type')
);
document.title = 'pastes | ' + id;
return { ok: true, content, type };
} else {
return { ok: false };
}
} catch (e) {
return { ok: false };
}
}
function parseLanguageFromContentType(contentType) {
const { type, subtype: subType } = parseContentType(contentType);
if (type === 'application' && subType === 'json') {
return 'json';
}
if (type === 'text' && languageIds.includes(subType.toLowerCase())) {
return subType.toLowerCase();
}
}
import { loadFromBytebin } from './util/storage';
const INITIAL = Symbol();
const LOADING = Symbol();
const LOADED = Symbol();
export default function App() {
const [pasteId] = useState(getPasteIdFromUrl);
const [state, setState] = useState(INITIAL);
const [forcedContent, setForcedContent] = useState('');
const [actualContent, setActualContent] = useState('');
const [contentType, setContentType] = useState();
type LoadingState = typeof INITIAL | typeof LOADING | typeof LOADED;
const setContent = useCallback((content) => {
setActualContent(content);
setForcedContent(content);
}, [setActualContent, setForcedContent]);
export default function App() {
const [pasteId] = useState<string | undefined>(getPasteIdFromUrl);
const [state, setState] = useState<LoadingState>(INITIAL);
const [forcedContent, setForcedContent] = useState<string>('');
const [actualContent, setActualContent] = useState<string>('');
const [contentType, setContentType] = useState<string>();
const setContent = useCallback(
(content: string) => {
setActualContent(content);
setForcedContent(content);
},
[setActualContent, setForcedContent]
);
useEffect(() => {
if (pasteId && state === INITIAL) {
@@ -88,7 +53,7 @@ export default function App() {
);
}
function get404Message(pasteId) {
function get404Message(pasteId: string) {
return `
@@ -101,3 +66,12 @@ function get404Message(pasteId) {
maybe the paste expired?
`;
}
function getPasteIdFromUrl() {
const path = window.location.pathname;
if (path && /^\/[a-zA-Z0-9]+$/.test(path)) {
return path.substring(1);
} else {
return undefined;
}
}

23
src/components/Button.tsx Normal file
View File

@@ -0,0 +1,23 @@
import styled from 'styled-components';
const Button = styled.div`
cursor: pointer;
height: 100%;
display: flex;
align-items: center;
padding: 0 0.25em;
color: inherit;
text-decoration: none;
:hover {
background: ${props => props.theme.highlight};
}
@media (max-width: 640px) {
span {
display: none;
}
}
`;
export default Button;

View File

@@ -1,11 +1,21 @@
import { useState, useEffect } from 'react';
import { ThemeProvider, createGlobalStyle } from 'styled-components';
import { useEffect, useState } from 'react';
import { isMobile } from 'react-device-detect';
import ls from 'local-storage';
import { ThemeProvider } from 'styled-components';
import usePreference from '../hooks/usePreference';
import themes, { Themes } from '../style/themes';
import EditorControls from './EditorControls';
import EditorGlobalStyle from './EditorGlobalStyle';
import EditorTextArea from './EditorTextArea';
import themes from '../style/themes';
export interface EditorProps {
forcedContent: string;
setForcedContent: (value: string) => void;
actualContent: string;
setActualContent: (value: string) => void;
contentType?: string;
pasteId?: string;
}
export default function Editor({
forcedContent,
@@ -14,16 +24,16 @@ export default function Editor({
setActualContent,
contentType,
pasteId,
}) {
const [language, setLanguage] = useState('plain');
const [readOnly, setReadOnly] = useState(isMobile && pasteId);
}: EditorProps) {
const [language, setLanguage] = useState<string>('plain');
const [readOnly, setReadOnly] = useState<boolean>(isMobile && !!pasteId);
const [theme, setTheme] = usePreference(
const [theme, setTheme] = usePreference<keyof Themes>(
'theme',
'dark',
pref => !!themes[pref]
);
const [fontSize, setFontSize, fontSizeCheck] = usePreference(
const [fontSize, setFontSize, fontSizeCheck] = usePreference<number>(
'fontsize',
16,
pref => pref >= 10 && pref <= 22
@@ -35,7 +45,7 @@ export default function Editor({
}
}, [contentType]);
function zoom(delta) {
function zoom(delta: number) {
const newFontSize = fontSize + delta;
if (fontSizeCheck(newFontSize)) {
setFontSize(newFontSize);
@@ -65,39 +75,8 @@ export default function Editor({
language={language}
fontSize={fontSize}
readOnly={readOnly}
setReadOnly={setReadOnly}
/>
</ThemeProvider>
</>
);
}
const EditorGlobalStyle = createGlobalStyle`
html, body {
color-scheme: ${props => props.theme.lightOrDark};
scrollbar-color: ${props => props.theme.lightOrDark};
background-color: ${props => props.theme.editor.background};
}
`;
// hook used to load "preference" settings from local storage, or fall back to a default value.
function usePreference(id, defaultValue, valid) {
const [value, setValue] = useState(() => {
const pref = ls.get(id);
if (pref && valid(pref)) {
return pref;
} else {
return defaultValue;
}
});
useEffect(() => {
if (value === defaultValue) {
ls.remove(id);
} else {
ls.set(id, value);
}
}, [value, id, defaultValue]);
return [value, setValue, valid];
}

View File

@@ -1,13 +1,25 @@
import { useState, useEffect, useCallback } from 'react';
import styled from 'styled-components';
import { gzip } from 'pako';
import history from 'history/browser';
import copy from 'copy-to-clipboard';
import history from 'history/browser';
import { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { MenuButton, Button } from './Menu';
import themes, { Themes } from '../style/themes';
import { languages } from '../util/highlighting';
import themes from '../style/themes';
import { postUrl } from '../util/constants';
import { saveToBytebin } from '../util/storage';
import Button from './Button';
import MenuButton from './MenuButton';
export interface EditorControlsProps {
actualContent: string;
setForcedContent: (value: string) => void;
language: string;
setLanguage: (value: string) => void;
readOnly: boolean;
setReadOnly: (value: boolean) => void;
theme: keyof Themes;
setTheme: (value: keyof Themes) => void;
zoom: (delta: number) => void;
}
export default function EditorControls({
actualContent,
@@ -19,9 +31,9 @@ export default function EditorControls({
theme,
setTheme,
zoom,
}) {
const [saving, setSaving] = useState(false);
const [recentlySaved, setRecentlySaved] = useState(false);
}: EditorControlsProps) {
const [saving, setSaving] = useState<boolean>(false);
const [recentlySaved, setRecentlySaved] = useState<boolean>(false);
useEffect(() => {
setRecentlySaved(false);
@@ -46,7 +58,7 @@ export default function EditorControls({
}, [actualContent, language, recentlySaved]);
useEffect(() => {
const listener = e => {
const listener = (e: KeyboardEvent) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === 's' || e.key === 'S') {
e.preventDefault();
@@ -100,7 +112,7 @@ export default function EditorControls({
label="theme"
value={theme}
setValue={setTheme}
ids={Object.keys(themes)}
ids={Object.keys(themes) as (keyof Themes)[]}
/>
<Button
className="optional"
@@ -139,36 +151,3 @@ const Section = styled.div`
}
}
`;
function langaugeToContentType(language) {
if (language === 'json') {
return 'application/json';
} else {
return 'text/' + language;
}
}
async function saveToBytebin(code, language) {
try {
const compressed = gzip(code);
const contentType = langaugeToContentType(language);
const resp = await fetch(postUrl, {
method: 'POST',
headers: {
'Content-Type': contentType,
'Content-Encoding': 'gzip',
'Accept': 'application/json',
},
body: compressed,
});
if (resp.ok) {
const json = await resp.json();
return json.key;
}
} catch (e) {
console.log(e);
}
return null;
}

View File

@@ -0,0 +1,12 @@
import { createGlobalStyle, ThemeProps } from 'styled-components';
import { Theme } from '../style/themes';
const EditorGlobalStyle = createGlobalStyle<ThemeProps<Theme>>`
html, body {
color-scheme: ${props => props.theme.lightOrDark};
scrollbar-color: ${props => props.theme.lightOrDark};
background-color: ${props => props.theme.editor.background};
}
`;
export default EditorGlobalStyle;

View File

@@ -1,8 +1,25 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import Editor, {
BeforeMount,
Monaco,
OnChange,
OnMount,
} from '@monaco-editor/react';
import history from 'history/browser';
import Editor from '@monaco-editor/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import themes, { makeMonacoTheme } from '../style/themes';
import themes, { makeMonacoTheme, Theme } from '../style/themes';
import type { editor } from 'monaco-editor';
export interface EditorTextAreaProps {
forcedContent: string;
actualContent: string;
setActualContent: (value: string) => void;
theme: Theme;
language: string;
fontSize: number;
readOnly: boolean;
}
export default function EditorTextArea({
forcedContent,
@@ -12,11 +29,12 @@ export default function EditorTextArea({
language,
fontSize,
readOnly,
}) {
const [editor, setEditor] = useState();
const [monaco, setMonaco] = useState();
}: EditorTextAreaProps) {
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const [monaco, setMonaco] = useState<Monaco>();
const [selected, toggleSelected] = useSelectedLine();
const editorAreaRef = useRef();
const editorAreaRef = useRef<HTMLDivElement>(null);
useLineNumberMagic(
editorAreaRef,
selected,
@@ -25,9 +43,10 @@ export default function EditorTextArea({
editor,
monaco
);
usePlaceholderText(actualContent, editor, monaco);
function beforeMount(monaco) {
const beforeMount: BeforeMount = monaco => {
for (const [id, theme] of Object.entries(themes)) {
monaco.editor.defineTheme(id, makeMonacoTheme(theme));
}
@@ -36,32 +55,35 @@ export default function EditorTextArea({
noSemanticValidation: true,
noSyntaxValidation: true,
});
}
};
function onMount(editor, monaco) {
const onMount: OnMount = (editor, monaco) => {
editor.addAction({
id: 'search',
label: 'Search with Google',
contextMenuGroupId: '9_cutcopypaste',
contextMenuOrder: 5,
run: (editor) => {
const selection = editor.getSelection()
if (!selection.isEmpty()) {
const query = editor.getModel().getValueInRange(selection);
window.open('https://www.google.com/search?q=' + query, '_blank');
run: editor => {
const selection = editor.getSelection();
if (selection && !selection.isEmpty()) {
const model = editor.getModel();
if (model) {
const query = model.getValueInRange(selection);
window.open('https://www.google.com/search?q=' + query, '_blank');
}
}
}
})
},
});
setEditor(editor);
setMonaco(monaco);
editor.focus();
}
};
const onChange = useCallback(
const onChange: OnChange = useCallback(
value => {
setActualContent(value);
setActualContent(value as string);
},
[setActualContent]
);
@@ -72,8 +94,13 @@ export default function EditorTextArea({
return;
}
editor.getModel().detectIndentation(true, 2);
}, [editor, forcedContent])
const model = editor.getModel();
if (!model) {
return;
}
model.detectIndentation(true, 2);
}, [editor, forcedContent]);
return (
<EditorArea ref={editorAreaRef}>
@@ -84,9 +111,9 @@ export default function EditorTextArea({
fontFamily: 'JetBrains Mono',
fontSize: fontSize,
fontLigatures: true,
wordWrap: true,
wordWrap: 'on',
renderLineHighlight: 'none',
renderValidationDecorations: false,
renderValidationDecorations: 'off',
readOnly,
domReadOnly: readOnly,
}}
@@ -124,13 +151,17 @@ const EditorArea = styled.main`
}
`;
function usePlaceholderText(actualContent, editor, monaco) {
function usePlaceholderText(
actualContent: string,
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined
) {
useEffect(() => {
if (!editor || !monaco || actualContent) {
return;
}
const decoration = {
range: new monaco.Range(1, 1, 1, 1),
range: new monaco.Range(1, 0, 1, 1),
options: {
after: {
content: '\u200B',
@@ -146,13 +177,63 @@ function usePlaceholderText(actualContent, editor, monaco) {
}, [editor, monaco, actualContent]);
}
type SelectedLine = [number, number];
type ToggleSelectedFunction = (lineNo: number, e: MouseEvent) => void;
function useSelectedLine(): [SelectedLine, ToggleSelectedFunction] {
// extract highlighted lines from window hash
const [selected, setSelected] = useState<[number, number]>(() => {
const hash = window.location.hash;
if (/^#L\d+(-\d+)?$/.test(hash)) {
const [start, end] = hash.substring(2).split('-').map(Number);
return [start, isNaN(end) ? start : end];
} else {
return [-1, -1];
}
});
// update window hash when a new line is highlighted
useEffect(() => {
let hash = '';
if (selected[0] !== -1) {
if (selected[1] !== selected[0]) {
const start = Math.min(...selected);
const end = Math.max(...selected);
hash = `#L${start}-${end}`;
} else {
hash = `#L${selected[0]}`;
}
}
history.replace({ hash });
}, [selected]);
// toggle the highlighting for a given line
const toggleSelected = useCallback(
(lineNo: number, e: MouseEvent) => {
const shift = e.shiftKey;
if (selected[0] === lineNo && selected[1] === lineNo) {
setSelected([-1, -1]);
} else if (selected[0] === -1 || !shift) {
setSelected([lineNo, lineNo]);
} else {
setSelected([selected[0], lineNo]);
}
},
[selected, setSelected]
);
return [selected, toggleSelected];
}
function useLineNumberMagic(
editorAreaRef,
selected,
toggleSelected,
forcedContent,
editor,
monaco
editorAreaRef: React.RefObject<HTMLDivElement>,
selected: SelectedLine,
toggleSelected: ToggleSelectedFunction,
forcedContent: string,
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined
) {
// add an event listener for clicking on line numbers
useEffect(() => {
@@ -161,9 +242,13 @@ function useLineNumberMagic(
return;
}
const handler = click => {
const target = click?.target;
if (target && target.classList.contains('line-numbers')) {
const handler = (click: MouseEvent) => {
const target = click?.target as HTMLElement;
if (
target &&
target.classList.contains('line-numbers') &&
target.textContent
) {
toggleSelected(parseInt(target.textContent), click);
}
};
@@ -204,47 +289,3 @@ function useLineNumberMagic(
};
}, [editor, monaco, selected, forcedContent]);
}
function useSelectedLine() {
// extract highlighted lines from window hash
const [selected, setSelected] = useState(() => {
const hash = window.location.hash;
if (/^#L\d+(-\d+)?$/.test(hash)) {
const [start, end] = hash.substring(2).split('-').map(Number);
return [start, isNaN(end) ? start : end];
} else {
return [-1, -1];
}
});
// update window hash when a new line is highlighted
useEffect(() => {
let hash = '';
if (selected[0] !== -1) {
if (selected[1] !== selected[0]) {
const start = Math.min(...selected);
const end = Math.max(...selected);
hash = `#L${start}-${end}`;
} else {
hash = `#L${selected[0]}`;
}
}
history.replace({ hash });
}, [selected]);
// toggle the highlighting for a given line
function toggleSelected(lineNo, e) {
const shift = e.shiftKey;
if (selected[0] === lineNo && selected[1] === lineNo) {
setSelected([-1, -1]);
} else if (selected[0] === -1 || !shift) {
setSelected([lineNo, lineNo]);
} else {
setSelected([selected[0], lineNo]);
}
}
return [selected, toggleSelected];
}

View File

@@ -1,25 +1,74 @@
import { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import Button from './Button';
export const Button = styled.div`
cursor: pointer;
height: 100%;
display: flex;
align-items: center;
padding: 0 0.25em;
color: inherit;
text-decoration: none;
interface MenuButtonProps<T extends string> {
label: string;
ids: T[] | Record<string, T[]>;
value: T;
setValue: (value: T) => void;
}
:hover {
background: ${props => props.theme.highlight};
export default function MenuButton<T extends string>({
label,
ids,
value,
setValue,
}: MenuButtonProps<T>) {
const [open, setOpen] = useState<boolean>(false);
// close the menu when a click is made elsewhere
useEffect(() => {
const listener = (e: MouseEvent) => setOpen(false);
window.addEventListener('click', listener);
return () => window.removeEventListener('click', listener);
}, [setOpen]);
function toggleOpen(e: React.MouseEvent) {
e.stopPropagation();
setOpen(!open);
}
@media (max-width: 640px) {
span {
display: none;
}
function select(e: React.MouseEvent<HTMLLIElement>, id: T) {
e.stopPropagation();
setOpen(false);
setValue(id);
}
`;
function make(ids: T[]) {
return ids.map(id => (
<li
key={id}
className={id === value ? 'selected' : undefined}
onClick={e => select(e, id)}
>
{id}
</li>
));
}
let items;
if (Array.isArray(ids)) {
items = make(ids);
} else {
items = Object.entries(ids).map(([title, values]) =>
[
<li className="title" key={title} onClick={e => e.stopPropagation()}>
[{title}]
</li>,
]
.concat(make(values))
.flat()
);
}
return (
<Button onClick={toggleOpen}>
[<span>{label}: </span>
{value}]{open && <Menu>{items}</Menu>}
</Button>
);
}
const Menu = styled.ul`
position: absolute;
@@ -60,58 +109,3 @@ const Menu = styled.ul`
background-color: ${props => props.theme.secondary};
}
`;
export const MenuButton = ({ label, ids, value, setValue }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
const listener = e => setOpen(false);
window.addEventListener('click', listener);
return () => window.removeEventListener('click', listener);
}, [setOpen]);
function toggleOpen(e) {
e.stopPropagation();
setOpen(!open);
}
function select(e, id) {
e.stopPropagation();
setOpen(false);
setValue(id);
}
function make(ids) {
return ids.map(id => (
<li
key={id}
className={id === value ? 'selected' : undefined}
onClick={e => select(e, id)}
>
{id}
</li>
));
}
let items;
if (Array.isArray(ids)) {
items = make(ids);
} else {
items = Object.entries(ids).map(([title, values]) =>
[
<li className="title" key={title} onClick={e => e.stopPropagation()}>
[{title}]
</li>,
]
.concat(make(values))
.flat()
);
}
return (
<Button onClick={toggleOpen}>
[<span>{label}: </span>
{value}]{open && <Menu>{items}</Menu>}
</Button>
);
};

View File

@@ -0,0 +1,28 @@
import { get as lsGet, remove as lsRemove, set as lsSet } from 'local-storage';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
// hook used to load "preference" settings from local storage, or fall back to a default value.
export default function usePreference<T>(
id: string,
defaultValue: T,
valid: (value: T) => boolean
): [T, Dispatch<SetStateAction<T>>, (value: T) => boolean] {
const [value, setValue] = useState<T>(() => {
const pref = lsGet(id) as T;
if (pref && valid(pref)) {
return pref;
} else {
return defaultValue;
}
});
useEffect(() => {
if (value === defaultValue) {
lsRemove(id);
} else {
lsSet(id, value);
}
}, [value, id, defaultValue]);
return [value, setValue, valid];
}

View File

@@ -1,12 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './style/base.css';
import App from './App';
import './style/base.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -1,4 +1,43 @@
const themes = {
import type { editor } from 'monaco-editor';
export interface Theme {
id: string;
primary: string;
secondary: string;
highlight: string;
lightOrDark: string;
editor: {
background: string;
lineNumber: string;
lineNumberHl: string;
lineNumberHlBackground: string;
primary: string;
selection: string;
comment: string;
commentTag: string;
punctuation: string;
annotation: string;
namespace: string;
property: string;
constant: string;
number: string;
selector: string;
operator: string;
keyword: string;
function: string;
className: string;
variable: string;
};
}
export interface Themes {
light: Theme;
blue: Theme;
dark: Theme;
}
const themes: Themes = {
light: {
id: 'light',
primary: '#aaddff',
@@ -93,7 +132,7 @@ const themes = {
export default themes;
export function makeMonacoTheme(theme) {
export function makeMonacoTheme(theme: Theme): editor.IStandaloneThemeData {
return {
base: theme.lightOrDark === 'light' ? 'vs' : 'vs-dark',
inherit: true,

83
src/util/storage.ts Normal file
View File

@@ -0,0 +1,83 @@
import { gzip } from 'pako';
import MIMEType from 'whatwg-mimetype';
import { bytebinUrl, postUrl } from './constants';
import { languageIds } from './highlighting';
interface LoadResultSuccess {
ok: true;
content: string;
type?: string;
}
interface LoadResultFail {
ok: false;
content?: never;
type?: never;
}
export type LoadResult = LoadResultSuccess | LoadResultFail;
export async function loadFromBytebin(id: string): Promise<LoadResult> {
try {
const resp = await fetch(bytebinUrl + id);
if (resp.ok) {
const content = await resp.text();
const type = contentTypeToLanguage(
resp.headers.get('content-type') as string
);
document.title = 'pastes | ' + id;
return { ok: true, content, type };
} else {
return { ok: false };
}
} catch (e) {
return { ok: false };
}
}
export async function saveToBytebin(
code: string,
language: string
): Promise<string | null> {
try {
const compressed = gzip(code);
const contentType = languageToContentType(language);
const resp = await fetch(postUrl, {
method: 'POST',
headers: {
'Content-Type': contentType,
'Content-Encoding': 'gzip',
'Accept': 'application/json',
},
body: compressed,
});
if (resp.ok) {
const json = await resp.json();
return json.key;
}
} catch (e) {
console.log(e);
}
return null;
}
export function contentTypeToLanguage(contentType: string) {
const { type, subtype: subType } = new MIMEType(contentType);
if (type === 'application' && subType === 'json') {
return 'json';
}
if (type === 'text' && languageIds.includes(subType.toLowerCase())) {
return subType.toLowerCase();
}
}
export function languageToContentType(language: string) {
if (language === 'json') {
return 'application/json';
} else {
return 'text/' + language;
}
}

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src/*.ts",
"src/*.tsx"
]
}

100
yarn.lock
View File

@@ -1565,10 +1565,10 @@
dependencies:
state-local "^1.0.6"
"@monaco-editor/react@^4.3.1":
version "4.4.5"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.5.tgz#beabe491efeb2457441a00d1c7651c653697f65b"
integrity sha512-IImtzU7sRc66OOaQVCG+5PFHkSWnnhrUWGBuH6zNmH2h0YgmAhcjHZQc/6MY9JWEbUtVF1WPBMJ9u1XuFbRrVA==
"@monaco-editor/react@4.4.6":
version "4.4.6"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.6.tgz#8ae500b0edf85276d860ed702e7056c316548218"
integrity sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA==
dependencies:
"@monaco-editor/loader" "^1.3.2"
prop-types "^15.7.2"
@@ -1961,6 +1961,14 @@
dependencies:
"@types/node" "*"
"@types/hoist-non-react-statics@*":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/html-minifier-terser@^6.0.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"
@@ -2000,6 +2008,14 @@
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/jest@^29.2.2":
version "29.2.2"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.2.tgz#874e7dc6702fa6a3fe6107792aa98636dcc480b4"
integrity sha512-og1wAmdxKoS71K2ZwSVqWPX6OVn3ihZ6ZT2qvZvZQm90lJVDyXIjYcu4Khx2CNIeaFv12rOU/YObOsI3VOkzog==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
@@ -2020,6 +2036,16 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.14.tgz#0fe081752a3333392d00586d815485a17c2cf3c9"
integrity sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==
"@types/node@^18.11.9":
version "18.11.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/pako@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.0.tgz#12ab4c19107528452e73ac99132c875ccd43bdfb"
integrity sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -2057,6 +2083,13 @@
dependencies:
"@types/react" "*"
"@types/react-dom@^18.0.8":
version "18.0.8"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.8.tgz#d2606d855186cd42cc1b11e63a71c39525441685"
integrity sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==
dependencies:
"@types/react" "*"
"@types/react@*":
version "18.0.18"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.18.tgz#9f16f33d57bc5d9dca848d12c3572110ff9429ac"
@@ -2066,6 +2099,15 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^18.0.25":
version "18.0.25"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44"
integrity sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -2110,6 +2152,15 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/styled-components@^5.1.26":
version "5.1.26"
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.26.tgz#5627e6812ee96d755028a98dae61d28e57c233af"
integrity sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==
dependencies:
"@types/hoist-non-react-statics" "*"
"@types/react" "*"
csstype "^3.0.2"
"@types/testing-library__jest-dom@^5.9.1":
version "5.14.5"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f"
@@ -2122,6 +2173,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@types/whatwg-mimetype@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#4ff45f787a085e7d22b7e01f3724f28a97d84c67"
integrity sha512-xHFOhd41VpUR6Y0k8ZinlyFv5cyhC/r2zghJgWWN8oNxqNo45Nf0qCBInJsFeifLeoHcIF4voEfap4A2GYHWkw==
"@types/ws@^8.5.1":
version "8.5.3"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
@@ -3234,11 +3290,6 @@ content-disposition@0.5.4:
dependencies:
safe-buffer "5.2.1"
content-type-parser@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
integrity sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@@ -4764,7 +4815,7 @@ history@^5.0.0:
dependencies:
"@babel/runtime" "^7.7.6"
hoist-non-react-statics@^3.0.0:
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6213,6 +6264,11 @@ mkdirp@~0.5.1:
dependencies:
minimist "^1.2.6"
monaco-editor@^0.34.1:
version "0.34.1"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.34.1.tgz#1b75c4ad6bc4c1f9da656d740d98e0b850a22f87"
integrity sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -7196,6 +7252,16 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
prettier-plugin-organize-imports@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.1.1.tgz#e581cb6fa528cf72f7d95b807ce38c3e51c3c27c"
integrity sha512-6bHIQzybqA644h0WGUW3gpWEVbMBvzui5wCMRBi7qA++d5ov2xjjfDk8pxJJ/ardfZrGAwizKMq/fQMFdJ+0Zw==
prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -7389,7 +7455,7 @@ react-device-detect@^2.1.2:
dependencies:
ua-parser-js "^1.0.2"
react-dom@^18.1.0:
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
@@ -7477,7 +7543,7 @@ react-scripts@5.0.1:
optionalDependencies:
fsevents "^2.3.2"
react@^18.1.0:
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
@@ -8547,6 +8613,11 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@^4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
ua-parser-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
@@ -8871,6 +8942,11 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-mimetype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"