Add front-end

This commit is contained in:
Miroslav Vasilev 2023-10-26 11:07:30 +03:00
parent 43a961a70c
commit bc4dbd2c77
27 changed files with 45202 additions and 3 deletions

View file

@ -0,0 +1 @@
REACT_APP_PASTE_API_LOCATION=http://localhost:8000/api

View file

@ -0,0 +1 @@
REACT_APP_PASTE_API_LOCATION=http://localhost:8000/api

23
paste-eater-frontend/.gitignore vendored Normal file
View 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*

18172
paste-eater-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
{
"name": "paste-eater-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@uiw/codemirror-theme-darcula": "^4.21.20",
"bootstrap": "^5.3.2",
"env-cmd": "^10.1.0",
"monaco-editor": "^0.44.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-dom": "^18.2.0",
"react-monaco-editor": "^0.54.0",
"react-router-dom": "^6.17.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start:dev": "env-cmd -f .env.development react-scripts start",
"start:prod": "env-cmd -f .env.production react-scripts start",
"build": "env-cmd -f .env.production react-scripts build",
"test": "env-cmd -f .env.development 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"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,14 @@
import React from 'react'
import { Routes, Route } from "react-router-dom";
import HomePage from './pages/Home.js'
import PastePage from './pages/Paste.js'
export default function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/:pasteId" element={<PastePage />} />
</Routes>
)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
import ThemeProvider from 'react-bootstrap/ThemeProvider';
function Layout({children}) {
return (
<main>
<ThemeProvider breakpoints={['xxxl', 'xxl', 'xl', 'lg', 'md', 'sm', 'xs', 'xxs']} minBreakpoint='lg'>
<div className="container-lg themed-container">
{children}
</div>
</ThemeProvider>
</main>
);
}
export default Layout;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,109 @@
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Navbar from 'react-bootstrap/Navbar';
import InputGroup from 'react-bootstrap/InputGroup';
import fetchEditorThemes from '../EditorThemes';
const DEFAULT_EDITOR_THEMES = ["vs-dark", "vs-light", "hc-black", "hc-light"];
export default function Topbar(props) {
let themeList = fetchEditorThemes();
let selectOptions = themeList.map(value => (<option value={value.id}>{value.name}</option>));
selectOptions.push((<option value="vs-dark">Visual Studio Dark</option>));
selectOptions.push((<option value="vs-light">Visual Studio Light</option>));
selectOptions.push((<option value="hc-black">High Contrast Black</option>));
selectOptions.push((<option value="hc-light">High Contrast Light</option>));
return (
<div className="border border-primary">
<Navbar className="d-flex justify-content-between">
<Navbar.Brand href="/">
<img
src={process.env.PUBLIC_URL + '/jar.svg'}
width="30"
height="30"
className="d-inline-block align-top"
alt="React Bootstrap logo"
/>
</Navbar.Brand>
<Form>
<InputGroup>
<InputGroup.Text id="editor-language">Language</InputGroup.Text>
<Form.Select
disabled={props.paste != null}
value={props.paste?.language?.toLowerCase()}
aria-label="Language"
onChange={(event) => props.onLanguageChange(event.target.value)}
>
<option value="plaintext">None</option>
<option value="csharp">C#</option>
<option value="java">Java</option>
<option value="go">Go</option>
<option value="rust">Rust</option>
<option value="cpp">C++</option>
<option value="python">Python</option>
</Form.Select>
</InputGroup>
</Form>
<Form>
<InputGroup>
<InputGroup.Text id="editor-theme">Theme</InputGroup.Text>
<Form.Select
aria-label="Theme"
type="select"
onChange={(event) => props.onThemeChange(event.target.value)}
defaultValue={props.defaultThemeValue}
>
{
selectOptions
}
</Form.Select>
</InputGroup>
</Form>
{
props.paste?.encrypted &&
<Form>
<InputGroup>
<Button variant="outline-secondary" id="button-decrypt">
Decrypt
</Button>
<Form.Control
aria-label="Decrypt the paste"
/>
</InputGroup>
</Form>
}
{
!props.paste &&
<Form>
<InputGroup>
<Button variant="outline-secondary" id="button-encrypt">
Encrypt
</Button>
<Form.Control
aria-label="Encrypt the paste and create it"
/>
</InputGroup>
</Form>
}
{
props.paste != null &&
<Form>
<Button variant="outline-danger" id="button-delete">
Delete
</Button>
</Form>
}
{
!props.paste &&
<Form>
<Button variant="outline-primary" id="button-create">
Create
</Button>
</Form>
}
</Navbar>
</div>
);
}

View file

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from "react-router-dom";
//import 'bootstrap/dist/css/bootstrap.min.css';
import './bootstrap-themes/vapor/bootstrap.min.css'
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

View file

@ -0,0 +1,40 @@
import React, { useState, useEffect } from "react";
import Layout from "../Layout";
import Topbar from "../components/Topbar";
import MonacoEditor from 'react-monaco-editor';
import fetchEditorThemes from '../EditorThemes';
import metadata from 'monaco-editor/esm/metadata';
export default function HomePage() {
let themeList = fetchEditorThemes();
let [ editorLanguage, setEditorLanguage ] = useState("rust");
let [ editorTheme, setEditorTheme ] = useState("vs-dark");
function beforeEditorMount(editor, monaco) {
themeList.forEach(value => {
monaco.editor.defineTheme(value.id, value.theme);
})
}
console.log(metadata.languages);
return (
<Layout>
<Topbar
onThemeChange={(value) => setEditorTheme(value)}
onLanguageChange={(value) => setEditorLanguage(value)}
defaultThemeValue="vs-dark"
/>
<MonacoEditor
language="javascript"
theme={editorTheme}
height="95vh"
options={{
selectOnLineNumbers: true
}}
editorDidMount={beforeEditorMount}
/>
</Layout>
);
}

View file

@ -0,0 +1,70 @@
import React, { useState, useEffect } from "react";
import Layout from "../Layout";
import Topbar from "../components/Topbar";
import { useParams, useNavigate } from "react-router-dom";
import MonacoEditor from 'react-monaco-editor';
import fetchEditorThemes from '../EditorThemes';
export default function PastePage() {
let themeList = fetchEditorThemes();
let { pasteId } = useParams();
let [ paste, setPaste ] = useState({});
let [ editorLanguage, setEditorLanguage ] = useState("rust");
let [ editorTheme, setEditorTheme ] = useState("vs-dark");
useEffect(() => {
fetch(`${process.env.REACT_APP_PASTE_API_LOCATION}/paste/${pasteId}`)
.then(response => {
if (response.ok) {
return response.json()
}
throw response;
})
.then(response => {
if (response.error) {
throw response;
}
setPaste(response.paste);
console.log(response);
})
.catch(error => {
console.log(error);
setPaste({ data: "Failed to retrieve paste, or paste does not exist." })
});
}, [])
function beforeEditorMount(editor, monaco) {
themeList.forEach(value => {
monaco.editor.defineTheme(value.id, value.theme);
})
}
return (
<Layout>
<Topbar
paste={paste}
onThemeChange={(value) => setEditorTheme(value)}
onLanguageChange={(value) => setEditorLanguage(value)}
defaultThemeValue="vs-dark"
/>
<MonacoEditor
language={editorLanguage}
theme={editorTheme}
value={paste.data}
height="95vh"
options={{
selectOnLineNumbers: true,
readOnly: true
}}
editorDidMount={beforeEditorMount}
disabled
/>
</Layout>
);
}

File diff suppressed because it is too large Load diff

View file

@ -217,5 +217,18 @@ impl PasteHandler {
Ok(paste) Ok(paste)
} }
pub fn delete_paste(&self, uid: PasteUID) -> Result<(), PasteError> {
let config = self.config_handler.fetch_config()?;
let mut file_path = Path::new(&config.files_location).to_path_buf();
file_path.push(Path::new(&format!("{}.paste", uid)));
match std::fs::remove_file(file_path) {
Ok(_) => Ok(()),
Err(e) => Err(PasteError::new_internal(&format!("Failed to delete paste '{}'.", uid), Box::new(e))),
}
}
} }

View file

@ -1,5 +1,5 @@
use clap::Parser; use clap::Parser;
use rocket::routes; use rocket::{routes, fairing::{Fairing, Info, Kind}, http::Header, Request, Response};
use crate::{server::endpoints, args::Args, config::ConfigurationHandler, paste::PasteHandler, error::PasteEaterError}; use crate::{server::endpoints, args::Args, config::ConfigurationHandler, paste::PasteHandler, error::PasteEaterError};
@ -13,7 +13,9 @@ pub async fn start_paste_eater() -> Result<(), rocket::Error> {
let _rocket = rocket::build() let _rocket = rocket::build()
.manage(paste_handler) .manage(paste_handler)
.mount("/api", routes![endpoints::create_paste, endpoints::get_paste]) .attach(Cors)
.mount("/api", routes![endpoints::create_paste, endpoints::get_paste, endpoints::delete_paste])
// .mount("/", FileServer::from(relative!("paste-eater-frontend/dist")))
.launch() .launch()
.await?; .await?;
@ -27,3 +29,22 @@ fn create_paste_handler() -> Result<PasteHandler, PasteEaterError> {
Ok(PasteHandler::new(config_handler)) Ok(PasteHandler::new(config_handler))
} }
pub struct Cors;
#[rocket::async_trait]
impl Fairing for Cors {
fn info(&self) -> Info {
Info {
name: "Add CORS headers to responses",
kind: Kind::Response
}
}
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, PATCH, OPTIONS"));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
}

View file

@ -1,4 +1,4 @@
use rocket::{post, get, response::status::Custom, http::Status, State, serde::json::Json}; use rocket::{post, get, response::status::Custom, http::Status, State, serde::json::Json, delete};
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
use crate::paste::{PasteHandler, PasteOutput, PasteLanguage}; use crate::paste::{PasteHandler, PasteOutput, PasteLanguage};
@ -31,3 +31,11 @@ pub fn get_paste(uid: String, paste_handler: &State<PasteHandler>) -> Custom<Jso
Err(e) => Custom(Status::InternalServerError, Json(PasteResponse { paste: None, error: Some(format!("{}", e)) })), Err(e) => Custom(Status::InternalServerError, Json(PasteResponse { paste: None, error: Some(format!("{}", e)) })),
} }
} }
#[delete("/paste/<uid>")]
pub fn delete_paste(uid: String, paste_handler: &State<PasteHandler>) -> Custom<String> {
match paste_handler.delete_paste(uid) {
Ok(_) => Custom(Status::Ok, "".to_string()),
Err(e) => Custom(Status::InternalServerError, format!("{}", e))
}
}