mirror of https://github.com/zer0bin-dev/zer0bin
Merge pull request #49 from zer0bin-dev/single-view-pastes
This commit is contained in:
commit
4fad42ff6a
|
@ -268,7 +268,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||
|
||||
[[package]]
|
||||
name = "backend"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-governor",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "backend"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
|
|
@ -2,10 +2,13 @@ CREATE TABLE IF NOT EXISTS pastes (
|
|||
"id" TEXT PRIMARY KEY,
|
||||
"content" TEXT NOT NULL,
|
||||
"views" BIGINT DEFAULT 0,
|
||||
"single_view" BOOLEAN DEFAULT false,
|
||||
"expires_at" TIMESTAMP WITHOUT TIME ZONE,
|
||||
"created_at" TIMESTAMP WITHOUT TIME ZONE DEFAULT(NOW() AT TIME ZONE 'utc')
|
||||
);
|
||||
|
||||
-- ALTER TABLE pastes ADD COLUMN single_view BOOLEAN DEFAULT false;
|
||||
|
||||
CREATE OR REPLACE FUNCTION deleteExpiredPastes() RETURNS trigger AS $pastes_expire$ BEGIN
|
||||
DELETE FROM pastes
|
||||
WHERE "expires_at" IS NOT NULL
|
||||
|
|
|
@ -7,12 +7,14 @@ pub struct Paste {
|
|||
pub id: String,
|
||||
pub content: String,
|
||||
pub views: i64,
|
||||
pub single_view: bool,
|
||||
pub expires_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PartialPaste {
|
||||
pub content: String,
|
||||
pub single_view: bool
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -25,6 +27,7 @@ pub struct ApiResponse<T> {
|
|||
pub struct NewPasteResponse {
|
||||
pub id: String,
|
||||
pub content: String,
|
||||
pub single_view: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -32,6 +35,7 @@ pub struct GetPasteResponse {
|
|||
pub id: String,
|
||||
pub content: String,
|
||||
pub views: i64,
|
||||
pub single_view: bool,
|
||||
pub expires_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
|
|
|
@ -32,14 +32,21 @@ pub async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> imp
|
|||
|
||||
match res {
|
||||
Ok(p) => {
|
||||
// this may be worth handling at some point..
|
||||
let _ = sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
// Only increment views if its not a single view paste
|
||||
if p.single_view {
|
||||
let _ = sqlx::query(r#"DELETE FROM pastes WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
} else {
|
||||
let _ = sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
if state.config.logging.on_get_paste {
|
||||
println!("[GET] id={} views={}", id, p.views + 1);
|
||||
println!("[GET] id={} views={} single_view={}", id, p.views + 1, p.single_view);
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(ApiResponse {
|
||||
|
@ -48,6 +55,7 @@ pub async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> imp
|
|||
id: p.id,
|
||||
content: p.content,
|
||||
views: p.views + 1,
|
||||
single_view: p.single_view,
|
||||
expires_at: p.expires_at,
|
||||
},
|
||||
})
|
||||
|
@ -87,13 +95,25 @@ pub async fn get_raw_paste(state: web::Data<AppState>, id: web::Path<String>) ->
|
|||
|
||||
match res {
|
||||
Ok(p) => {
|
||||
// this may be worth handling at some point..
|
||||
let _ = sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
if p.single_view {
|
||||
let _ = sqlx::query(r#"DELETE FROM pastes WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
} else {
|
||||
let _ = sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
HttpResponse::Ok().content_type("text/plain").body(p.content)
|
||||
if state.config.logging.on_get_paste {
|
||||
println!("[GET] raw id={} views={} single_view={}", id, p.views + 1, p.single_view);
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/plain")
|
||||
.body(p.content)
|
||||
}
|
||||
Err(e) => match e {
|
||||
sqlx::Error::RowNotFound => {
|
||||
|
@ -144,11 +164,13 @@ pub async fn new_paste(
|
|||
};
|
||||
|
||||
let content = data.content.clone();
|
||||
let single_view = data.single_view;
|
||||
|
||||
let res =
|
||||
sqlx::query(r#"INSERT INTO pastes("id", "content", "expires_at") VALUES ($1, $2, $3)"#)
|
||||
sqlx::query(r#"INSERT INTO pastes("id", "content", "single_view", "expires_at") VALUES ($1, $2, $3, $4)"#)
|
||||
.bind(id.clone())
|
||||
.bind(content.clone())
|
||||
.bind(single_view)
|
||||
.bind(expires_at)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
@ -156,11 +178,15 @@ pub async fn new_paste(
|
|||
match res {
|
||||
Ok(_) => {
|
||||
if state.config.logging.on_post_paste {
|
||||
println!("[POST] id={} length={}", id, content.len());
|
||||
println!("[POST] id={} length={} single_view={}", id, content.len(), single_view);
|
||||
}
|
||||
HttpResponse::Ok().json(ApiResponse {
|
||||
success: true,
|
||||
data: NewPasteResponse { id, content },
|
||||
data: NewPasteResponse {
|
||||
id,
|
||||
content,
|
||||
single_view,
|
||||
},
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
|
@ -1,44 +1,51 @@
|
|||
doctype html
|
||||
head
|
||||
meta(charset='UTF-8')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
||||
title zer0bin
|
||||
link(rel='shortcut icon' type='image/x-icon' href='favicon.ico')
|
||||
meta(name='theme-color' content='#90BDC9')
|
||||
meta(name='keywords' content='zerobin, zer0bin, paste, paste bin, pastebin, pastebin alternative, foss pastebin, open source pastebin, free pastebin, ghostbin, hastebin, pastebin.com, free pastebin online, paste online, github paste, github gist, github gist paste')
|
||||
meta(name='darkreader' content='NO-DARKREADER-PLUGIN')
|
||||
link(href='style/style.scss' rel='stylesheet')
|
||||
link(href='min/rosepine.min.css' rel='stylesheet')
|
||||
meta(name='title' content='zer0bin')
|
||||
meta(name='description' content='🖊 Just a place to paste')
|
||||
meta(property='og:type' content='website')
|
||||
meta(property='og:title' content='zer0bin')
|
||||
meta(property='og:description' content='🖊 Just a place to paste')
|
||||
meta(property='og:image' content='https://raw.githubusercontent.com/zer0bin-dev/.github/main/zer0bin.png')
|
||||
meta(property='twitter:title' content='zer0bin')
|
||||
meta(property='twitter:description' content='🖊 Just a place to paste')
|
||||
meta(property='twitter:image' content='https://raw.githubusercontent.com/zer0bin-dev/.github/main/zer0bin.png')
|
||||
script(defer='' type='module' src='src/index.ts')
|
||||
html(lang='en')
|
||||
head
|
||||
meta(charset='UTF-8')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
||||
title zer0bin
|
||||
link(rel='shortcut icon' type='image/x-icon' href='favicon.ico')
|
||||
meta(name='theme-color' content='#90BDC9')
|
||||
meta(name='keywords' content='zerobin, zer0bin, paste, paste bin, pastebin, pastebin alternative, foss pastebin, open source pastebin, free pastebin, ghostbin, hastebin, pastebin.com, free pastebin online, paste online, github paste, github gist, github gist paste')
|
||||
meta(name='darkreader' content='NO-DARKREADER-PLUGIN')
|
||||
link(href='style/style.scss' rel='stylesheet')
|
||||
link(href='min/rosepine.min.css' rel='stylesheet')
|
||||
meta(name='title' content='zer0bin')
|
||||
meta(name='description' content='🖊 Just a place to paste')
|
||||
meta(property='og:type' content='website')
|
||||
meta(property='og:title' content='zer0bin')
|
||||
meta(property='og:description' content='🖊 Just a place to paste')
|
||||
meta(property='og:image' content='https://raw.githubusercontent.com/zer0bin-dev/.github/main/zer0bin.png')
|
||||
meta(property='twitter:title' content='zer0bin')
|
||||
meta(property='twitter:description' content='🖊 Just a place to paste')
|
||||
meta(property='twitter:image' content='https://raw.githubusercontent.com/zer0bin-dev/.github/main/zer0bin.png')
|
||||
script(defer='' type='module' src='src/index.ts')
|
||||
|
||||
ul.noselect#messages
|
||||
.button-wrapper.noselect()
|
||||
a.logo.noselect(href='/') zer0bin
|
||||
.buttons.noselect
|
||||
button#save-button.btn(aria-label='Save')
|
||||
a(href='/' aria-label='New paste')
|
||||
button#new-button.btn(aria-label='New paste')
|
||||
button#copy-button.btn(aria-label='Copy')
|
||||
a(href='https://github.com/zer0bin-dev/zer0bin' aria-label='GitHub repo')
|
||||
button#github-button.btn(aria-label='GitHub')
|
||||
span.viewcounter.noselect#viewcounter-label(style='display: none') Views:
|
||||
span.viewcounter.noselect#viewcounter-count(style='display: none')
|
||||
body
|
||||
ul.noselect#messages
|
||||
.button-wrapper.noselect()
|
||||
a.logo.noselect(href='/') zer0bin
|
||||
.buttons.noselect
|
||||
button#save-button.btn(aria-label='Save')
|
||||
button#single-view-button.btn(aria-label='Single View')
|
||||
.fireBody(style='display: none')
|
||||
- var parts = 20;
|
||||
- while (parts--) {
|
||||
.particle
|
||||
- }
|
||||
button#new-button.btn(aria-label='New paste')
|
||||
button#copy-button.btn(aria-label='Copy')
|
||||
a(href='https://github.com/zer0bin-dev/zer0bin' aria-label='GitHub repo')
|
||||
button#github-button.btn(aria-label='GitHub')
|
||||
span.viewcounter.noselect#viewcounter-label(style='display: none') Views:
|
||||
span.viewcounter.noselect#viewcounter-count(style='display: none')
|
||||
|
||||
.hide-button-wrapper
|
||||
button#hide-button.btn(aria-label='Hide')
|
||||
.hide-button-wrapper
|
||||
button#hide-button.btn(aria-label='Hide')
|
||||
|
||||
.scrollbar-container
|
||||
.wrapper
|
||||
.line-numbers.noselect
|
||||
pre#code-view-pre(style='display: none')
|
||||
code#code-view
|
||||
textarea#text-area(spellcheck='false' autofocus='' name='value' aria-label='Paste input area' disabled='' style='display: none')
|
||||
.scrollbar-container
|
||||
.wrapper
|
||||
.line-numbers.noselect
|
||||
pre#code-view-pre(style='display: none')
|
||||
code#code-view
|
||||
textarea#text-area(spellcheck='false' autofocus='' name='value' aria-label='Paste input area' disabled='' style='display: none')
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "zer0bin",
|
||||
"source": "index.pug",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
@ -7,7 +7,8 @@ import {
|
|||
HeartOutlined,
|
||||
StarOutlined,
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined
|
||||
EyeInvisibleOutlined,
|
||||
FireOutlined,
|
||||
} from "@ant-design/icons-svg"
|
||||
import { renderIconDefinitionToSVGElement } from "@ant-design/icons-svg/es/helpers"
|
||||
import tippy from "tippy.js"
|
||||
|
@ -19,6 +20,9 @@ const newButton = <HTMLButtonElement>document.getElementById("new-button")
|
|||
const copyButton = <HTMLButtonElement>document.getElementById("copy-button")
|
||||
const hideButton = <HTMLButtonElement>document.getElementById("hide-button")
|
||||
const githubButton = <HTMLButtonElement>document.getElementById("github-button")
|
||||
const singleViewButton = <HTMLButtonElement>(
|
||||
document.getElementById("single-view-button")
|
||||
)
|
||||
|
||||
const extraSVGAttrs = {
|
||||
width: "1em",
|
||||
|
@ -41,6 +45,9 @@ githubButton.innerHTML += renderIconDefinitionToSVGElement(GithubOutlined, {
|
|||
hideButton.innerHTML += renderIconDefinitionToSVGElement(EyeInvisibleOutlined, {
|
||||
extraSVGAttrs: extraSVGAttrs,
|
||||
})
|
||||
singleViewButton.innerHTML += renderIconDefinitionToSVGElement(FireOutlined, {
|
||||
extraSVGAttrs: extraSVGAttrs,
|
||||
})
|
||||
|
||||
tippy("#save-button", {
|
||||
content: "Save paste<br><span class='keybind'>Ctrl + S</span>",
|
||||
|
@ -50,6 +57,15 @@ tippy("#save-button", {
|
|||
allowHTML: true,
|
||||
})
|
||||
|
||||
tippy("#single-view-button", {
|
||||
content:
|
||||
"Single view<br><span class='keybind'>Deletes after seen 👻</span>",
|
||||
placement: "bottom",
|
||||
animation: "scale",
|
||||
theme: "rosepine",
|
||||
allowHTML: true,
|
||||
})
|
||||
|
||||
tippy("#new-button", {
|
||||
content: "New paste<br><span class='keybind'>Ctrl + N</span>",
|
||||
placement: "bottom",
|
||||
|
@ -89,7 +105,6 @@ tippy("#hide-button", {
|
|||
allowHTML: true,
|
||||
})
|
||||
|
||||
|
||||
const observer = new MutationObserver(callback)
|
||||
|
||||
function callback() {
|
||||
|
@ -113,12 +128,15 @@ observer.observe(document.getElementById("code-view-pre"), {
|
|||
|
||||
export function toggleHiddenIcon(hidden: boolean) {
|
||||
if (!hidden) {
|
||||
hideButton.innerHTML = renderIconDefinitionToSVGElement(EyeInvisibleOutlined, {
|
||||
extraSVGAttrs: extraSVGAttrs,
|
||||
})
|
||||
hideButton.innerHTML = renderIconDefinitionToSVGElement(
|
||||
EyeInvisibleOutlined,
|
||||
{
|
||||
extraSVGAttrs: extraSVGAttrs,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
hideButton.innerHTML = renderIconDefinitionToSVGElement(EyeOutlined, {
|
||||
extraSVGAttrs: extraSVGAttrs,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ const apiUrl = config.api_url
|
|||
const confettiChance = parseInt(config.confetti_chance)
|
||||
let rawContent = ""
|
||||
let buttonPaneHidden = false
|
||||
let singleView = false
|
||||
|
||||
const jsConfetti = new JSConfetti()
|
||||
|
||||
|
@ -33,6 +34,9 @@ const saveButton = <HTMLButtonElement>document.getElementById("save-button")
|
|||
const newButton = <HTMLButtonElement>document.getElementById("new-button")
|
||||
const copyButton = <HTMLButtonElement>document.getElementById("copy-button")
|
||||
const hideButton = <HTMLButtonElement>document.getElementById("hide-button")
|
||||
const singleViewButton = <HTMLButtonElement>(
|
||||
document.getElementById("single-view-button")
|
||||
)
|
||||
|
||||
function hide(element: HTMLElement) {
|
||||
element.style.display = "none"
|
||||
|
@ -51,7 +55,7 @@ function enable(element: HTMLButtonElement) {
|
|||
}
|
||||
|
||||
async function postPaste(content: string, callback: Function) {
|
||||
const payload = { content }
|
||||
const payload = { content, single_view: singleView }
|
||||
await fetch(`${apiUrl}/p/n`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
@ -106,6 +110,7 @@ function newPaste() {
|
|||
enable(saveButton)
|
||||
disable(newButton)
|
||||
disable(copyButton)
|
||||
enable(singleViewButton)
|
||||
|
||||
editor.value = ""
|
||||
rawContent = ""
|
||||
|
@ -131,7 +136,7 @@ function addMessage(message: string) {
|
|||
}, 3000)
|
||||
}
|
||||
|
||||
function viewPaste(content: string, views: string) {
|
||||
function viewPaste(content: string, views: string, singleView: boolean) {
|
||||
lineNumbers.innerHTML = ""
|
||||
if (
|
||||
content.startsWith("---") ||
|
||||
|
@ -146,11 +151,17 @@ function viewPaste(content: string, views: string) {
|
|||
codeView.innerHTML = hljs.highlightAuto(content).value
|
||||
}
|
||||
|
||||
if (singleView) {
|
||||
hide(singleViewButton.firstElementChild as HTMLElement)
|
||||
singleViewButton.lastElementChild.classList.add("fire")
|
||||
}
|
||||
|
||||
disable(saveButton)
|
||||
enable(newButton)
|
||||
enable(copyButton)
|
||||
hide(editor)
|
||||
disable(singleViewButton)
|
||||
|
||||
hide(editor)
|
||||
show(codeViewPre)
|
||||
show(viewCounterLabel)
|
||||
show(viewCounter)
|
||||
|
@ -177,7 +188,7 @@ async function savePaste() {
|
|||
window.history.pushState(null, "", `/${res["data"]["id"]}`)
|
||||
|
||||
rawContent = res["data"]["content"]
|
||||
viewPaste(rawContent, "0")
|
||||
viewPaste(rawContent, "0", res["data"]["single_view"])
|
||||
|
||||
const rand = Math.floor(Math.random() * confettiChance * 6)
|
||||
|
||||
|
@ -277,6 +288,18 @@ hideButton.addEventListener("click", function () {
|
|||
toggleHiddenIcon(buttonPaneHidden)
|
||||
})
|
||||
|
||||
singleViewButton.addEventListener("click", function () {
|
||||
if (singleView) {
|
||||
singleView = false
|
||||
hide(singleViewButton.firstElementChild as HTMLElement)
|
||||
singleViewButton.lastElementChild.classList.remove("fire")
|
||||
} else {
|
||||
singleView = true
|
||||
show(singleViewButton.firstElementChild as HTMLElement)
|
||||
singleViewButton.lastElementChild.classList.add("fire")
|
||||
}
|
||||
})
|
||||
|
||||
async function handlePopstate() {
|
||||
const path = window.location.pathname
|
||||
|
||||
|
@ -292,7 +315,11 @@ async function handlePopstate() {
|
|||
newPaste()
|
||||
} else {
|
||||
rawContent = res["data"]["content"]
|
||||
viewPaste(rawContent, res["data"]["views"].toString())
|
||||
viewPaste(
|
||||
rawContent,
|
||||
res["data"]["views"].toString(),
|
||||
res["data"]["single_view"]
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -129,8 +129,9 @@ textarea {
|
|||
position: fixed !important;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 1rem 1rem 0.5rem 1rem;
|
||||
padding: 0.7rem 0.7rem 0.5rem 0.7rem;
|
||||
background-color: $bg_surface;
|
||||
border-bottom-left-radius: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
.buttons {
|
||||
|
@ -220,6 +221,58 @@ a {
|
|||
animation: rainbow 3s ease infinite !important;
|
||||
}
|
||||
}
|
||||
.fire {
|
||||
color: $love;
|
||||
}
|
||||
$fireColor: $love;
|
||||
$fireColorT: $love;
|
||||
$dur: 1s;
|
||||
$blur: 0.02em;
|
||||
$fireRad: 3em;
|
||||
$parts: 20;
|
||||
$partSize: 5em;
|
||||
|
||||
.fireBody {
|
||||
font-size: 3px;
|
||||
filter: blur($blur);
|
||||
-webkit-filter: blur($blur);
|
||||
margin: 2em auto 0 auto;
|
||||
position: absolute;
|
||||
width: 6em;
|
||||
height: 5em;
|
||||
z-index: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.particle {
|
||||
animation: rise $dur ease-in infinite;
|
||||
background-image: radial-gradient($fireColor 100%, $fireColorT 100%);
|
||||
border-radius: 50%;
|
||||
mix-blend-mode: screen;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: $partSize;
|
||||
height: $partSize;
|
||||
@for $p from 1 through $parts {
|
||||
&:nth-of-type(#{$p}) {
|
||||
animation-delay: $dur * random();
|
||||
left: calc((100% - #{$partSize}) * #{($p - 1) / $parts});
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10em) scale(0);
|
||||
}
|
||||
}
|
||||
#messages {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
@ -12,6 +12,7 @@ $font-mono: "Cartograph CF", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
|||
padding: 10px;
|
||||
font-family: $font-mono;
|
||||
font-size: 13px;
|
||||
border-radius: 10px;
|
||||
> .tippy-backdrop {
|
||||
background-color: $bg;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue