initial commit
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
6
README.md
Normal file
6
README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<h3 align="center"><img src="https://i.imgur.com/Aifb3lU.png"></h3>
|
||||||
|
<h1 align="center">paste</h1>
|
||||||
|
|
||||||
|
paste is a simple, "code friendly" web frontend for [bytebin](https://github.com/lucko/bytebin).
|
||||||
|
|
||||||
|
It is written in React using [react-simple-code-editor](https://github.com/satya164/react-simple-code-editor) and [Prism](https://github.com/PrismJS/prism).
|
||||||
43
package.json
Normal file
43
package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "paste",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
|
"@testing-library/react": "^11.1.0",
|
||||||
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"content-type-parser": "^1.0.2",
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"history": "^5.0.0",
|
||||||
|
"pako": "^2.0.3",
|
||||||
|
"prismjs": "^1.23.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-scripts": "4.0.3",
|
||||||
|
"react-simple-code-editor": "^0.11.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/assets/logo180.png
Normal file
BIN
public/assets/logo180.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/assets/logo192.png
Normal file
BIN
public/assets/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/assets/logo512.png
Normal file
BIN
public/assets/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
31
public/index.html
Normal file
31
public/index.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>paste</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#aaddff" />
|
||||||
|
<meta name="description" content="lucko's simple pastebin." />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:title" content="paste" />
|
||||||
|
<meta name="twitter:description" content="lucko's simple pastebin." />
|
||||||
|
<meta name="twitter:image" content="%PUBLIC_URL%/assets/logo180.png" />
|
||||||
|
|
||||||
|
<meta property="og:title" content="paste" />
|
||||||
|
<meta property="og:description" content="lucko's simple pastebin." />
|
||||||
|
<meta property="og:type" content="product" />
|
||||||
|
<meta property="og:image" content="%PUBLIC_URL%/assets/logo180.png" />
|
||||||
|
<meta property="og:url" content="https://paste.lucko.me/" />
|
||||||
|
<meta property="og:site_name" content="paste" />
|
||||||
|
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<link href="%PUBLIC_URL%/assets/logo512.png" rel="shortcut icon" sizes="512x512" type="image/png">
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/logo192.png" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
src/App.js
Normal file
65
src/App.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Editor from './components/Editor';
|
||||||
|
import parseContentType from 'content-type-parser';
|
||||||
|
import { languageIds } from './highlighting';
|
||||||
|
|
||||||
|
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('https://bytebin.lucko.me/' + id);
|
||||||
|
if (resp.ok) {
|
||||||
|
const content = await resp.text();
|
||||||
|
const { type, subtype: subType } = parseContentType(resp.headers.get('content-type'));
|
||||||
|
|
||||||
|
document.title = 'paste | ' + id;
|
||||||
|
if (type === 'text' && languageIds.includes(subType.toLowerCase())) {
|
||||||
|
return { ok: true, content, type: subType.toLowerCase() };
|
||||||
|
} else {
|
||||||
|
return { ok: true, content };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL = Symbol();
|
||||||
|
const LOADING = Symbol();
|
||||||
|
const LOADED = Symbol();
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [pasteId] = useState(getPasteIdFromUrl);
|
||||||
|
const [state, setState] = useState(INITIAL);
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const [contentType, setContentType] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pasteId && state === INITIAL) {
|
||||||
|
setState(LOADING);
|
||||||
|
setContent('// Loading, please wait...')
|
||||||
|
loadFromBytebin(pasteId).then(({ ok, content, type }) => {
|
||||||
|
if (ok) {
|
||||||
|
setContent(content);
|
||||||
|
if (type) {
|
||||||
|
setContentType(type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setContent('// Unable to load a paste with the id \'' + pasteId + '\'\n// Are you sure it exists? Maybe it expired?');
|
||||||
|
}
|
||||||
|
setState(LOADED);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [pasteId, state, setContent])
|
||||||
|
|
||||||
|
return <Editor content={content} setContent={setContent} contentType={contentType} />;
|
||||||
|
}
|
||||||
18
src/components/Editor.js
Normal file
18
src/components/Editor.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import EditorControls from './EditorControls';
|
||||||
|
import EditorTextArea from './EditorTextArea';
|
||||||
|
|
||||||
|
export default function Editor({ content, setContent, contentType }) {
|
||||||
|
const [language, setLanguage] = useState('plain');
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentType) {
|
||||||
|
setLanguage(contentType);
|
||||||
|
}
|
||||||
|
}, [contentType]);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<EditorControls code={content} language={language} setLanguage={setLanguage} />
|
||||||
|
<EditorTextArea code={content} setCode={setContent} language={language} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
92
src/components/EditorControls.js
Normal file
92
src/components/EditorControls.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { languageIds } from '../highlighting';
|
||||||
|
import { gzip } from 'pako';
|
||||||
|
import history from 'history/browser';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
|
||||||
|
export default function EditorControls({ code, language, setLanguage }) {
|
||||||
|
const [langMenuOpen, setLangMenuOpen] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [recentlySaved, setRecentlySaved] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRecentlySaved(false);
|
||||||
|
}, [code, language])
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!code || recentlySaved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaving(true);
|
||||||
|
saveToBytebin(code, language).then((pasteId) => {
|
||||||
|
setSaving(false);
|
||||||
|
setRecentlySaved(true);
|
||||||
|
history.replace({
|
||||||
|
pathname: pasteId,
|
||||||
|
hash: ''
|
||||||
|
});
|
||||||
|
copy(window.location.href);
|
||||||
|
document.title = 'paste | ' + pasteId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLangMenu() {
|
||||||
|
setLangMenuOpen(!langMenuOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLanguage(e, language) {
|
||||||
|
e.stopPropagation();
|
||||||
|
setLangMenuOpen(false);
|
||||||
|
setLanguage(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<div className="section">
|
||||||
|
<div className="button" onClick={save}>
|
||||||
|
{recentlySaved
|
||||||
|
? '[link copied!]'
|
||||||
|
: saving ? '[saving...]' : '[save]'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="button" onClick={toggleLangMenu}>
|
||||||
|
[language: {language}]
|
||||||
|
{langMenuOpen && (
|
||||||
|
<ul>
|
||||||
|
{languageIds.map(id => <li key={id} onClick={e => selectLanguage(e, id)}>{id}</li>)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section">
|
||||||
|
<a className="button" href="https://bytebin.lucko.me/" target="_blank" rel="noreferrer">[not pasting code?]</a>
|
||||||
|
<a className="button" href="https://github.com/lucko/paste" target="_blank" rel="noreferrer">[about paste]</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveToBytebin(code, language) {
|
||||||
|
try {
|
||||||
|
const compressed = gzip(code);
|
||||||
|
const contentType = 'text/' + language;
|
||||||
|
|
||||||
|
const resp = await fetch('https://bytebin.lucko.me/post', {
|
||||||
|
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;
|
||||||
|
}
|
||||||
29
src/components/EditorTextArea.js
Normal file
29
src/components/EditorTextArea.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import ReactEditor from 'react-simple-code-editor';
|
||||||
|
import { getHighlighter } from '../highlighting';
|
||||||
|
|
||||||
|
import 'prismjs/themes/prism.css';
|
||||||
|
|
||||||
|
export default function EditorTextArea({ code, setCode, language }) {
|
||||||
|
const highlight = getHighlighter(language);
|
||||||
|
|
||||||
|
function highlightWithLineNumbers(input, grammar) {
|
||||||
|
return highlight(input, grammar)
|
||||||
|
.split('\n')
|
||||||
|
.map((line, i) => `<span class='editorLineNumber'>${i + 1}</span>${line}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<ReactEditor
|
||||||
|
value={code}
|
||||||
|
onValueChange={setCode}
|
||||||
|
highlight={highlightWithLineNumbers}
|
||||||
|
placeholder={'Type some code...'}
|
||||||
|
padding={10}
|
||||||
|
textareaId='code-area'
|
||||||
|
className='editor'
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
62
src/highlighting.js
Normal file
62
src/highlighting.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||||
|
|
||||||
|
import 'prismjs/components/prism-markup';
|
||||||
|
import 'prismjs/components/prism-css';
|
||||||
|
import 'prismjs/components/prism-clike';
|
||||||
|
import 'prismjs/components/prism-javascript';
|
||||||
|
import 'prismjs/components/prism-bash';
|
||||||
|
import 'prismjs/components/prism-diff';
|
||||||
|
import 'prismjs/components/prism-docker';
|
||||||
|
import 'prismjs/components/prism-go';
|
||||||
|
import 'prismjs/components/prism-groovy';
|
||||||
|
import 'prismjs/components/prism-haskell';
|
||||||
|
import 'prismjs/components/prism-java';
|
||||||
|
import 'prismjs/components/prism-javascript';
|
||||||
|
import 'prismjs/components/prism-json';
|
||||||
|
import 'prismjs/components/prism-kotlin';
|
||||||
|
//import 'prismjs/components/prism-log';
|
||||||
|
import 'prismjs/components/prism-markdown';
|
||||||
|
//import 'prismjs/components/prism-php';
|
||||||
|
import 'prismjs/components/prism-protobuf';
|
||||||
|
import 'prismjs/components/prism-python';
|
||||||
|
import 'prismjs/components/prism-jsx';
|
||||||
|
import 'prismjs/components/prism-typescript';
|
||||||
|
import 'prismjs/components/prism-ruby';
|
||||||
|
import 'prismjs/components/prism-rust';
|
||||||
|
import 'prismjs/components/prism-sql';
|
||||||
|
import 'prismjs/components/prism-toml';
|
||||||
|
import 'prismjs/components/prism-yaml';
|
||||||
|
|
||||||
|
export const languageIds = [
|
||||||
|
'plain',
|
||||||
|
'markup',
|
||||||
|
'css',
|
||||||
|
'clike',
|
||||||
|
'javascript',
|
||||||
|
'bash',
|
||||||
|
'diff',
|
||||||
|
'docker',
|
||||||
|
'go',
|
||||||
|
'groovy',
|
||||||
|
'haskell',
|
||||||
|
'java',
|
||||||
|
'json',
|
||||||
|
'kotlin',
|
||||||
|
//'log',
|
||||||
|
'markdown',
|
||||||
|
//'php',
|
||||||
|
'protobuf',
|
||||||
|
'python',
|
||||||
|
'jsx',
|
||||||
|
'typescript',
|
||||||
|
'ruby',
|
||||||
|
'rust',
|
||||||
|
'sql',
|
||||||
|
'toml',
|
||||||
|
'yaml'
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getHighlighter(language) {
|
||||||
|
const grammar = language === 'plain' ? {} : languages[language];
|
||||||
|
return (input) => highlight(input, grammar);
|
||||||
|
}
|
||||||
11
src/index.js
Normal file
11
src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import './style/base.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
91
src/style/base.css
Normal file
91
src/style/base.css
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital@0;1&display=swap');
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
height: 2em;
|
||||||
|
background: #025;
|
||||||
|
color: #adf;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .button {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .button:hover {
|
||||||
|
background: #36368c;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .button > ul {
|
||||||
|
position: absolute;
|
||||||
|
top: 2em;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
background-color: #36368c;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .button > ul > li {
|
||||||
|
padding: .15em .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .button > ul > li:hover {
|
||||||
|
background-color: #025;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
counter-reset: line;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: 0;
|
||||||
|
min-height: calc(100vh - 2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor #code-area {
|
||||||
|
outline: none;
|
||||||
|
padding-left: 60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor pre {
|
||||||
|
padding-left: 60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor .editorLineNumber {
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
color: #cccccc;
|
||||||
|
text-align: right;
|
||||||
|
width: 40px;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user