Merge pull request #49 from zer0bin-dev/single-view-pastes

This commit is contained in:
Kainoa Kanter 2022-04-03 19:00:08 -07:00 committed by GitHub
commit 4fad42ff6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 209 additions and 70 deletions

2
backend/Cargo.lock generated
View File

@ -268,7 +268,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backend"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"actix-cors",
"actix-governor",

View File

@ -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

View File

@ -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

View File

@ -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>,
}

View File

@ -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) => {

View File

@ -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:&nbsp;
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:&nbsp;
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')

View File

@ -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": {

View File

@ -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,
})
}
}
}

View File

@ -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"]
)
}
})
}

View File

@ -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;

View File

@ -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;
}