Add paste web front page v2 (Bootstrap)

Separate the web file into submodule

Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
Joe Ma 2023-03-26 03:17:23 +08:00
parent bf8c1be285
commit 361f5f40e3
No known key found for this signature in database
GPG key ID: 7A0ECF5F5EDC587F
7 changed files with 397 additions and 1 deletions

175
web/v1/paste.html Normal file
View file

@ -0,0 +1,175 @@
<!--
~ This file is part of paste.
~ Copyright (c) 2022-2023 Joe Ma <rikkaneko23@gmail.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Lesser General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU Lesser General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Paste</title>
</head>
<body>
<h2>Paste Service</h2>
<p>
<a href="https://pb.nekoul.com">pb.nekoul.com</a> is a pastebin-like service hosted on Cloudflare Worker.<br>
This service is primarily designed for own usage and interest only.<br>
All data may be deleted or expired without any notification and guarantee. Please <b>DO NOT</b> abuse this
service.<br>
The limit for file upload is <b>10 MB</b> and the paste will be kept for <b>28 days</b> only by default.<br>
The source code is available in my GitHub repository <a href="https://github.com/rikkaneko/paste">[here]</a>.<br>
This webpage is designed for upload files only.
For other operations like changing paste settings and deleting paste, please make use of the
<a href="https://github.com/rikkaneko/paste#api-specification">API call</a> with <a
href="https://wiki.archlinux.org/title/CURL">curl</a>.
</p>
<form id="upload_file_form" action="https://pb.nekoul.com" method="POST" enctype=multipart/form-data>
<div>
<div>
<h4>Upload file</h4>
<input id="upload_file" type="file" name="u" style="font-size: 14px">
</div>
<div>
<h4>Upload text</h4>
<div>
<textarea id="text_input"
style="width: 50%; max-width: 80%; font-size: 14px; margin-top: 2px"
rows="10" name="u" spellcheck="false"></textarea><br>
(<span id="text_length">0 characters</span>)
</div>
</div>
<h4>Settings</h4>
<table style="padding-bottom: 2px;">
<tr>
<td>
<label for="title_input">Title: </label>
</td>
<td>
<input id="title_input" type="text" name="title" spellcheck="false" style="width: 20em">
</td>
</tr>
<tr>
<td>
<label for="pass_input">Password: </label>
</td>
<td>
<input id="pass_input" type="password" name="pass" style="width: 20em">
<input id="show_pass_button" type="checkbox">
<label for="show_pass_button">Show</label>
</td>
</tr>
<tr>
<td>
<label for="read_limit_input">Read limit: </label>
</td>
<td>
<input id="read_limit_input" type="number" name="read-limit" min="1" style="width: 3em">
</td>
</tr>
</table>
<input id="show_qr_checkbox" type="checkbox" name="qrcode" checked>
<label for="show_qr_checkbox">Show QR code on sumbitted</label>
</div>
<div>
<input id="gen_link" type="checkbox" name="paste-type" value="link">
<label for="gen_link">Generate as shorten URL link</label>
</div>
<br>
<div>
<input id="reset_button" type="reset" value="Reset" style="font-size: 14px">
<input id="sumbit_form_button" type="submit" value="Sumbit" style="font-size: 14px"> (<span
id="file_stats">0 bytes</span>)
</div>
</form>
<script>
function update_textarea() {
const length = document.getElementById('text_length');
length.textContent = `${this.value.length} characters`;
}
function update_file_status() {
const status = document.getElementById('file_stats');
const title = document.getElementById('title_input');
if (this.files[0] === undefined) {
status.textContent = '0 bytes';
return;
}
let bytes = this.files[0]?.size ?? 0;
let size = bytes + ' bytes';
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
for (let i = 0, approx = bytes / 1024; approx > 1; approx /= 1024, i++) {
size = approx.toFixed(3) + ' ' + units[i];
}
title.value = this.files[0]?.name || '';
status.textContent = `${this.files[0]?.type || 'application/octet-stream'}, ${size}`;
}
function toggle_password() {
let input_field = document.getElementById('pass_input');
if (this.checked) {
input_field.type = 'text';
} else {
input_field.type = 'password';
}
}
function reset_form() {
// Re-enable all input elements
let elements = document.getElementById('upload_file_form').elements;
for (let i = 0; i < elements.length; i++) {
elements[i].disabled = false;
}
let size = document.getElementById('file_stats');
size.textContent = '0 bytes';
}
function handle_submit_form(event) {
let elements = this.elements;
let select_file = elements.namedItem('upload_file');
let text = elements.namedItem('text_input');
if (!!select_file.value.length ^ !!text.value.length) {
// Check file size
const size = select_file.files[0]?.size ?? 0;
if (size > 10485760) {
alert('Upload file size must not excess 10 MB.');
event.preventDefault();
return false;
}
for (let i = 0; i < elements.length; i++) {
elements[i].disabled = elements[i].value.length === 0;
}
} else {
alert('You must either upload a file or upload text, but not both or neither.');
// Prevent default submission
event.preventDefault();
}
}
document.getElementById('upload_file').addEventListener('change', update_file_status, false);
document.getElementById('text_input').addEventListener('input', update_textarea, false);
document.getElementById('show_pass_button').addEventListener('change', toggle_password, false);
document.getElementById('reset_button').addEventListener('click', reset_form, false);
document.getElementById('upload_file_form').addEventListener('submit', handle_submit_form, false);
</script>
<br>
<a href="https://nekoul.com">[Homepage]</a><a href="https://github.com/rikkaneko/paste#api-specification">[API]</a>
<p>&copy; 2022 rikkaneko</p>
</body>
</html>

25
web/v2/css/paste.css Normal file
View file

@ -0,0 +1,25 @@
/*
* This file is part of paste.
* Copyright (c) 2023 Joe Ma <rikkaneko23@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.card-header .fa {
transition: .3s transform ease-in-out;
}
.card-header .collapsed .fa {
transform: rotate(90deg);
}

167
web/v2/js/paste.js Normal file
View file

@ -0,0 +1,167 @@
/*
* This file is part of paste.
* Copyright (c) 2023 Joe Ma <rikkaneko23@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
let input_div = {
file: null,
text: null,
url: null,
};
let inputs = {
file: null,
text: null,
url: null,
};
let selected_type = 'file';
function validate_url(path) {
let url;
try {
url = new URL(path);
} catch (_) {
return false;
}
return url.protocol === 'http:' || url.protocol === 'https:';
}
function show_pop_alert(message, alert_type = 'alert-primary') {
remove_pop_alert();
$('body').prepend(jQuery.parseHTML(
`<div class="alert ${alert_type} alert-dismissible position-absolute top-0 start-50 translate-middle-x outer"
style="margin-top: 80px; max-width: 500px; width: 80%" id="pop_alert" role="alert"> \
<div>${message}</div> \
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> \
</div>`,
));
}
function remove_pop_alert() {
const alert = $('#pop_alert');
if (alert.length)
alert.remove();
}
$(function () {
input_div.file = $('#file_upload_layout');
input_div.text = $('#text_input_layout');
input_div.url = $('#url_input_layout');
inputs.file = $('#file_upload');
inputs.text = $('#text_input');
inputs.url = $('#url_input');
let file_stat = $('#file_stats');
let title = $('#paste_title');
let char_count = $('#char_count');
let pass_input = $('#pass_input');
let show_pass_icon = $('#show_pass_icon');
let upload_button = $('#upload_button');
let url_validate_result = $('#url_validate_result');
let tos_btn = $('#tos_btn');
inputs.file.on('change', function () {
inputs.file.removeClass('is-invalid');
file_stat.removeClass('text-danger');
if (this.files[0] === undefined) {
file_stat.textContent = '0 bytes';
return;
}
let bytes = this.files[0]?.size ?? 0;
let size = bytes + ' bytes';
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
for (let i = 0, approx = bytes / 1024; approx > 1; approx /= 1024, i++) {
size = approx.toFixed(3) + ' ' + units[i];
}
title.val(this.files[0]?.name || '');
file_stat.text(`${this.files[0]?.type || 'application/octet-stream'}, ${size}`);
// Check length
if (bytes > 10485760) {
inputs.file.addClass('is-invalid');
file_stat.addClass('text-danger');
file_stat.text('The uploaded file is larger than the 10 MB limit.');
}
});
inputs.text.on('input', function () {
inputs.text.removeClass('is-invalid');
char_count.removeClass('text-danger');
char_count.text(`${this.value.length} characters`);
if (this.value.length <= 0) {
inputs.text.addClass('is-invalid');
char_count.addClass('text-danger');
char_count.text('Input text cannot be empty.');
}
});
$('#show_pass_button').on('click', function () {
if (pass_input.attr('type') === 'password') {
pass_input.attr('type', 'text');
show_pass_icon.removeClass('bi-eye bi-eye-slash');
show_pass_icon.addClass('bi-eye');
} else if (pass_input.attr('type') === 'text') {
pass_input.attr('type', 'password');
show_pass_icon.removeClass('bi-eye bi-eye-slash');
show_pass_icon.addClass('bi-eye-slash');
}
});
inputs.url.on('input', function () {
inputs.url.removeClass('is-invalid');
url_validate_result.removeClass('text-danger');
if (!validate_url(this.value)) {
inputs.url.addClass('is-invalid');
url_validate_result.addClass('text-danger');
url_validate_result.text('Invalid URL');
}
});
upload_button.on('click', function () {
inputs[selected_type].trigger('input');
const form = $('#upload_form')[0];
const formdata = new FormData(form);
let content = {};
formdata.forEach((val, key) => {
content[key] = val;
});
if (inputs[selected_type].hasClass('is-invalid') || !(!!content.u?.size || !!content.u?.length)) {
show_pop_alert('Please check your upload file or content', 'alert-danger');
return false;
}
if (!tos_btn.prop('checked')) {
show_pop_alert('Please read the TOS before upload', 'alert-warning');
return false;
}
// TODO Upload to pb service
show_pop_alert('Paste created!', 'alert-success');
});
});
function select_input_type(name) {
selected_type = name;
Object.keys(input_div).forEach(key => {
input_div[key].collapse('hide');
inputs[key].prop('disabled', true);
});
input_div[name].collapse('show');
inputs[name].prop('disabled', false);
inputs[name].prop('required', true);
}

190
web/v2/paste.html Normal file
View file

@ -0,0 +1,190 @@
<!--
~ This file is part of paste.
~ Copyright (c) 2022-2023 Joe Ma <rikkaneko23@gmail.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Lesser General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU Lesser General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<title>Paste</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width initial-scale=1 shrink-to-fit=1">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.2/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.9.1/font/bootstrap-icons.min.css"
rel="stylesheet">
<link href="css/paste.css" rel="stylesheet">
</head>
<body>
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">Paste</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar_supported_content"
aria-controls="navbar_supported_content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar_supported_content">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="https://nekoul.com">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/rikkaneko/paste#api-specification">API</a>
</li>
</ul>
<form class="d-flex">
<input class="form-control me-2" type="search" placeholder="" aria-label="go">
<button class="btn btn-outline-success" type="button">Go</button>
</form>
</div>
</div>
</nav>
<div class="d-flex align-items-center justify-content-center vh-100">
<form class="container" style="max-width: 400px" id="upload_form">
<div class="mb-3">
<div><label class="form-label">Paste Type</label></div>
<div class="btn-group w-100" role="group" aria-label="Paste type group">
<input type="radio" class="btn-check" name="paste_type" id="paste_type_file" autocomplete="off" checked
onclick="select_input_type('file')" value="file">
<label class="btn btn-outline-primary" for="paste_type_file">File</label>
<input type="radio" class="btn-check" name="paste_type" id="paste_type_text" autocomplete="off"
onclick="select_input_type('text')" value="text">
<label class="btn btn-outline-primary" for="paste_type_text">Text</label>
<input type="radio" class="btn-check" name="paste_type" id="paste_type_url" autocomplete="off"
onclick="select_input_type('url')" value="url">
<label class="btn btn-outline-primary" for="paste_type_url">URL</label>
</div>
</div>
<div id="file_upload_layout" class="collapse show">
<div class="mb-2">
<label for="file_upload" class="form-label">Upload File</label>
<input class="form-control" type="file" id="file_upload" name="u">
</div>
<div class="text-sm-start mb-3">
<small id="file_stats">0 bytes</small>
</div>
</div>
<div id="text_input_layout" class="collapse">
<div class="mb-2">
<label for="text_input" class="form-label">Upload Text</label>
<textarea class="form-control" id="text_input" rows="10" name="u" disabled></textarea>
</div>
<div class="text-sm-start mb-3">
<small id="char_count">0 characters</small>
</div>
</div>
<div id="url_input_layout" class="collapse">
<div class="mb-2">
<label for="url_input" class="form-label">URL Address</label>
<input type="url" class="form-control" id="url_input" placeholder="https://example.com" name="u" disabled>
</div>
<div class="text-sm-start mb-3">
<small id="url_validate_result"></small>
</div>
</div>
<div class="mb-3">
<label for="paste_title" class="form-label">Title</label>
<input type="text" class="form-control" id="paste_title" placeholder="" name="title">
</div>
<div class="card-header mb-3">
<span data-bs-toggle="collapse" data-bs-target="#advanced_settings_layout" aria-expanded="false"
aria-controls="advanced_settings_layout" id="advanced_settings_control"
class="d-block">
<i class="fa fa-chevron-down pull-right mt-1"></i>
Advanced Settings
</span>
</div>
<div id="advanced_settings_layout" class="collapse">
<div class="mb-3">
<label class="form-label" for="pass_input">Password</label>
<div class="input-group mb-3">
<input class="form-control password lock" id="pass_input" type="password" name="pass"/>
<span class="input-group-text" style="cursor: pointer" id="show_pass_button">
<i class="bi bi-eye-slash" id="show_pass_icon"></i>
</span>
</div>
</div>
<div class="mb-3">
<label for="read_limit_input" class="form-label">Read limit</label>
<input type="number" class="form-control" id="read_limit_input" min="1" name="read-limit">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="show_qrcode_checkbox" name="qrcode" checked>
<label class="form-check-label" for="show_qrcode_checkbox">
Show QR code after upload
</label>
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="tos_btn" required>
<label class="form-check-label" for="tos_btn">
I understand <a class="link-primary" data-bs-toggle="modal" data-bs-target="#tos_model" role="button">the terms and conditions</a>
</label>
</div>
<div class="mb-3 text-end">
<button type="button" class="btn btn-primary" id="upload_button">Upload</button>
</div>
</form>
</div>
<div class="modal fade" id="tos_model" tabindex="-1" aria-labelledby="tos_model_label" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="tos_model_label">Terms and conditions</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h3>Paste Service</h3>
<p>
<a href="https://pb.nekoul.com">pb.nekoul.com</a> is a pastebin-like service hosted on Cloudflare Worker.<br>
This service is primarily designed for own usage and interest only.<br>
All data may be deleted or expired without any notification and guarantee. Please <b>DO NOT</b> abuse this
service.<br>
The limit for file upload is <b>10 MB</b> and the paste will be kept for <b>28 days</b> only by default.<br>
The source code is available in my GitHub repository <a href="https://github.com/rikkaneko/paste">[here]</a>.<br>
This webpage is designed for upload files only.
For other operations like changing paste settings and deleting paste, please make use of the
<a href="https://github.com/rikkaneko/paste#api-specification">API call</a> with <a
href="https://wiki.archlinux.org/title/CURL">curl</a>.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.6/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.2/js/bootstrap.min.js"></script>
<script src="js/paste.js"></script>
</body>
</html>