add(frontend): vibrant integration
remove(frontend): atomic pastes
This commit is contained in:
parent
61f9e88523
commit
c778a29581
|
@ -1,6 +1,6 @@
|
|||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, Responder};
|
||||
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::db::{self, AtomicPasteFSFile, DefaultReturn, FullPaste, PasteMetadata};
|
||||
use crate::db::{self, DefaultReturn, FullPaste, PasteMetadata};
|
||||
use crate::{markdown, ssm, utility};
|
||||
|
||||
#[derive(Default, PartialEq, serde::Deserialize)]
|
||||
|
@ -485,187 +485,3 @@ pub async fn read_atomic_request(req: HttpRequest, data: web::Data<db::AppData>)
|
|||
.append_header(("Content-Type", "text/plain"))
|
||||
.body(existing.unwrap().content.clone());
|
||||
}
|
||||
|
||||
#[post("/api/atomic/crud/{url:.*}/{path:.*}")]
|
||||
/// Update an atomic paste's "file system"
|
||||
pub async fn update_atomic_request(
|
||||
req: HttpRequest,
|
||||
body: String, // text/plain
|
||||
data: web::Data<db::AppData>,
|
||||
) -> impl Responder {
|
||||
// this is essentially the same as edit_request but it handles the atomic JSON file system
|
||||
// ...it does NOT accept an edit password! users must be authenticated
|
||||
let custom_url: String = req.match_info().get("url").unwrap().to_string();
|
||||
let path: String = format!("/{}", req.match_info().get("path").unwrap());
|
||||
let content: String = body.clone();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
// get paste
|
||||
let paste: DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
|
||||
data.db.get_paste_by_url(custom_url.clone()).await;
|
||||
|
||||
if paste.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&paste).unwrap());
|
||||
}
|
||||
|
||||
// make sure paste is an atomic paste
|
||||
let unwrap = paste.payload.unwrap();
|
||||
let is_atomic = unwrap.paste.content.contains("\"_is_atomic\":true");
|
||||
|
||||
if is_atomic == false {
|
||||
return HttpResponse::NotFound().body("Paste is not atomic");
|
||||
}
|
||||
|
||||
// get file from path
|
||||
let real_content = serde_json::from_str::<db::AtomicPaste>(&unwrap.paste.content);
|
||||
|
||||
if real_content.is_err() {
|
||||
return HttpResponse::NotAcceptable().body("Paste failed to deserialize");
|
||||
}
|
||||
|
||||
let mut decoded = real_content.unwrap();
|
||||
|
||||
// check for existing file in atomic paste fs
|
||||
let existing = decoded.files.iter().position(|f| f.path == path);
|
||||
|
||||
if existing.is_some() {
|
||||
// remove existing file
|
||||
decoded.files.remove(existing.unwrap());
|
||||
}
|
||||
|
||||
// insert file
|
||||
decoded.files.push(AtomicPasteFSFile {
|
||||
path,
|
||||
content: content.clone(),
|
||||
});
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_paste_by_url(
|
||||
custom_url,
|
||||
serde_json::to_string::<db::AtomicPaste>(&decoded).unwrap(), // encode content
|
||||
String::new(),
|
||||
Option::None,
|
||||
Option::None,
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[delete("/api/atomic/crud/{url:.*}/{path:.*}")]
|
||||
/// Delete in an atomic paste's "file system"
|
||||
pub async fn delete_atomic_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::AppData>,
|
||||
) -> impl Responder {
|
||||
// this is essentially the same as edit_request but it handles the atomic JSON file system
|
||||
// ...it does NOT accept an edit password! users must be authenticated
|
||||
let custom_url: String = req.match_info().get("url").unwrap().to_string();
|
||||
let path: String = format!("/{}", req.match_info().get("path").unwrap());
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
// get paste
|
||||
let paste: DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
|
||||
data.db.get_paste_by_url(custom_url.clone()).await;
|
||||
|
||||
if paste.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&paste).unwrap());
|
||||
}
|
||||
|
||||
// make sure paste is an atomic paste
|
||||
let unwrap = paste.payload.unwrap();
|
||||
let is_atomic = unwrap.paste.content.contains("\"_is_atomic\":true");
|
||||
|
||||
if is_atomic == false {
|
||||
return HttpResponse::NotFound().body("Paste is not atomic");
|
||||
}
|
||||
|
||||
// get file from path
|
||||
let real_content = serde_json::from_str::<db::AtomicPaste>(&unwrap.paste.content);
|
||||
|
||||
if real_content.is_err() {
|
||||
return HttpResponse::NotAcceptable().body("Paste failed to deserialize");
|
||||
}
|
||||
|
||||
let mut decoded = real_content.unwrap();
|
||||
|
||||
// check for existing file in atomic paste fs
|
||||
let existing = decoded.files.iter().position(|f| f.path == path);
|
||||
|
||||
if existing.is_some() {
|
||||
// remove existing file
|
||||
decoded.files.remove(existing.unwrap());
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_paste_by_url(
|
||||
custom_url,
|
||||
serde_json::to_string::<db::AtomicPaste>(&decoded).unwrap(), // encode content
|
||||
String::new(),
|
||||
Option::None,
|
||||
Option::None,
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
|
87
src/db.rs
87
src/db.rs
|
@ -635,84 +635,6 @@ impl Database {
|
|||
};
|
||||
}
|
||||
|
||||
/// Get all atomic [pastes](Paste) owned by a specific user
|
||||
///
|
||||
/// # Arguments:
|
||||
/// * `owner` - `String` of the owner's `username`
|
||||
pub async fn get_atomic_pastes_by_owner(
|
||||
&self,
|
||||
owner: String,
|
||||
) -> DefaultReturn<Option<Vec<PasteIdentifier>>> {
|
||||
// check in cache
|
||||
let cached = self
|
||||
.base
|
||||
.cachedb
|
||||
.get(format!("pastes-by-owner-atomic:{}:atomic", owner))
|
||||
.await;
|
||||
|
||||
if cached.is_some() {
|
||||
// ...
|
||||
let pastes =
|
||||
serde_json::from_str::<Vec<PasteIdentifier>>(cached.unwrap().as_str()).unwrap();
|
||||
|
||||
// return
|
||||
return DefaultReturn {
|
||||
success: true,
|
||||
message: owner,
|
||||
payload: Option::Some(pastes),
|
||||
};
|
||||
}
|
||||
|
||||
// ...
|
||||
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
|
||||
"SELECT * FROM \"Pastes\" WHERE \"metadata\" LIKE ? AND \"content\" LIKE ?"
|
||||
} else {
|
||||
"SELECT * FROM \"Pastes\" WHERE \"metadata\" LIKE $1 AND \"content\" LIKE $2"
|
||||
};
|
||||
|
||||
let c = &self.base.db.client;
|
||||
let res = sqlquery(query)
|
||||
.bind::<&String>(&format!("%\"owner\":\"{}\"%", &owner))
|
||||
.bind("%\"_is_atomic\":true%")
|
||||
.fetch_all(c)
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
return DefaultReturn {
|
||||
success: false,
|
||||
message: String::from(res.err().unwrap().to_string()),
|
||||
payload: Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
// build res
|
||||
let mut full_res: Vec<PasteIdentifier> = Vec::new();
|
||||
|
||||
for row in res.unwrap() {
|
||||
let row = self.base.textify_row(row).data;
|
||||
full_res.push(PasteIdentifier {
|
||||
custom_url: row.get("custom_url").unwrap().to_string(),
|
||||
id: row.get("id").unwrap().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// store in cache
|
||||
self.base
|
||||
.cachedb
|
||||
.set(
|
||||
format!("pastes-by-owner:{}:atomic", owner),
|
||||
serde_json::to_string::<Vec<PasteIdentifier>>(&full_res).unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return DefaultReturn {
|
||||
success: true,
|
||||
message: owner,
|
||||
payload: Option::Some(full_res),
|
||||
};
|
||||
}
|
||||
|
||||
// SET
|
||||
/// Create a new [`Paste`] given various properties
|
||||
///
|
||||
|
@ -781,6 +703,15 @@ impl Database {
|
|||
};
|
||||
}
|
||||
|
||||
// project cannot have names we may need
|
||||
if ["dashboard", "api"].contains(&p.custom_url.as_str()) {
|
||||
return DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Custom URL is invalid"),
|
||||
payload: Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
// (characters used)
|
||||
let regex = regex::RegexBuilder::new("^[\\w\\_\\-\\.\\!\\p{Extended_Pictographic}]+$")
|
||||
.multi_line(true)
|
||||
|
|
|
@ -116,8 +116,6 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(crate::api::pastes::render_paste_ssm_request)
|
||||
// atomic api
|
||||
.service(crate::api::pastes::read_atomic_request)
|
||||
.service(crate::api::pastes::update_atomic_request)
|
||||
.service(crate::api::pastes::delete_atomic_request)
|
||||
// GET api
|
||||
.service(crate::api::pastes::get_from_url_request)
|
||||
.service(crate::api::pastes::get_from_id_request)
|
||||
|
@ -130,10 +128,6 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(crate::pages::settings::user_settings_request)
|
||||
.service(crate::pages::settings::paste_settings_request)
|
||||
.service(crate::pages::paste_view::dashboard_request)
|
||||
// GET dashboard (atomic pastes)
|
||||
.service(crate::pages::atomic_editor::dashboard_request)
|
||||
.service(crate::pages::atomic_editor::new_request)
|
||||
.service(crate::pages::atomic_editor::edit_request)
|
||||
// GET staff
|
||||
.service(crate::pages::staff::dashboard_request)
|
||||
.service(crate::pages::staff::staff_boards_dashboard_request)
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
use actix_web::HttpRequest;
|
||||
use actix_web::{get, web, HttpResponse, Responder};
|
||||
|
||||
use super::base;
|
||||
use askama::Template;
|
||||
|
||||
use crate::db::{self, AtomicPasteFSFile, FullPaste, PasteMetadata};
|
||||
|
||||
#[derive(Default, PartialEq, serde::Deserialize)]
|
||||
struct EditQueryProps {
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "paste/atomic/overview.html")]
|
||||
struct FSOverviewTemplate {
|
||||
custom_url: String,
|
||||
files: Vec<db::AtomicPasteFSFile>,
|
||||
// required fields (super::base)
|
||||
info: String,
|
||||
auth_state: bool,
|
||||
guppy: String,
|
||||
puffer: String,
|
||||
site_name: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "paste/atomic/editor.html")]
|
||||
struct EditorTemplate {
|
||||
custom_url: String,
|
||||
file: db::AtomicPasteFSFile,
|
||||
file_content: String,
|
||||
// required fields
|
||||
site_name: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "paste/atomic/new.html")]
|
||||
struct NewTemplate {
|
||||
// required fields (super::base)
|
||||
info: String,
|
||||
auth_state: bool,
|
||||
guppy: String,
|
||||
puffer: String,
|
||||
site_name: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "paste/atomic/dashboard.html")]
|
||||
struct DashboardTemplate {
|
||||
pastes: Vec<db::PasteIdentifier>,
|
||||
// required fields (super::base)
|
||||
info: String,
|
||||
auth_state: bool,
|
||||
guppy: String,
|
||||
puffer: String,
|
||||
site_name: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
||||
#[get("/dashboard/atomic")]
|
||||
/// Available at "/dashboard/atomic"
|
||||
pub async fn dashboard_request(req: HttpRequest, data: web::Data<db::AppData>) -> impl Responder {
|
||||
// verify auth status
|
||||
let (set_cookie, _, token_user) = base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
if token_user.is_none() {
|
||||
// you must have an account to use atomic pastes
|
||||
return super::errors::error401(req, data).await;
|
||||
}
|
||||
|
||||
// fetch pastes
|
||||
let pastes = data
|
||||
.db
|
||||
.get_atomic_pastes_by_owner(token_user.clone().unwrap().payload.unwrap().user.username)
|
||||
.await;
|
||||
|
||||
// ...
|
||||
let base = base::get_base_values(token_user.is_some());
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Set-Cookie", set_cookie))
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
DashboardTemplate {
|
||||
pastes: pastes.payload.unwrap(),
|
||||
// required fields
|
||||
info: base.info,
|
||||
auth_state: base.auth_state,
|
||||
guppy: base.guppy,
|
||||
puffer: base.puffer,
|
||||
site_name: base.site_name,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[get("/dashboard/atomic/new")]
|
||||
/// Available at "/dashboard/atomic/new"
|
||||
pub async fn new_request(req: HttpRequest, data: web::Data<db::AppData>) -> impl Responder {
|
||||
// verify auth status
|
||||
let (set_cookie, _, token_user) = base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
if token_user.is_none() {
|
||||
// you must have an account to use atomic pastes
|
||||
return super::errors::error401(req, data).await;
|
||||
}
|
||||
|
||||
// ...
|
||||
let base = base::get_base_values(token_user.is_some());
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Set-Cookie", set_cookie))
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
NewTemplate {
|
||||
// required fields
|
||||
info: base.info,
|
||||
auth_state: base.auth_state,
|
||||
guppy: base.guppy,
|
||||
puffer: base.puffer,
|
||||
site_name: base.site_name,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[get("/dashboard/atomic/{id:.*}")]
|
||||
/// Available at "/dashboard/atomic/{id}"
|
||||
pub async fn edit_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::AppData>,
|
||||
info: web::Query<EditQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
let (set_cookie, _, token_user) = base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
if token_user.is_none() {
|
||||
// you must have an account to use atomic pastes
|
||||
// we'll likely track requests used by atomic pastes and limit it in the future, similar to Vibrant
|
||||
// ...or just migrate to Vibrant
|
||||
return super::errors::error401(req, data).await;
|
||||
}
|
||||
|
||||
// get paste
|
||||
let id: String = req.match_info().get("id").unwrap().to_string();
|
||||
let paste: db::DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
|
||||
data.db.get_paste_by_id(id).await;
|
||||
|
||||
if paste.success == false {
|
||||
return super::errors::error404(req, data).await;
|
||||
}
|
||||
|
||||
// make sure paste is an atomic paste
|
||||
let unwrap = paste.payload.unwrap().paste;
|
||||
let is_atomic = unwrap.content.contains("\"_is_atomic\":true");
|
||||
|
||||
if is_atomic == false {
|
||||
return HttpResponse::NotFound().body("Paste is not atomic");
|
||||
}
|
||||
|
||||
// get file from path
|
||||
let real_content = serde_json::from_str::<db::AtomicPaste>(&unwrap.content);
|
||||
|
||||
if real_content.is_err() {
|
||||
return HttpResponse::NotAcceptable().body("Paste failed to deserialize");
|
||||
}
|
||||
|
||||
let decoded = real_content.unwrap();
|
||||
|
||||
// show file list if path is none
|
||||
if info.path.is_none() {
|
||||
let base = base::get_base_values(token_user.is_some());
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Set-Cookie", set_cookie))
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
FSOverviewTemplate {
|
||||
custom_url: unwrap.custom_url.clone(),
|
||||
files: decoded.files,
|
||||
// required fields
|
||||
info: base.info,
|
||||
auth_state: base.auth_state,
|
||||
guppy: base.guppy,
|
||||
puffer: base.puffer,
|
||||
site_name: base.site_name,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let path_unwrap = info.path.clone().unwrap();
|
||||
|
||||
// ...
|
||||
let mut file = decoded.files.iter().find(|f| f.path == path_unwrap);
|
||||
let blank_file = AtomicPasteFSFile {
|
||||
path: path_unwrap.clone(),
|
||||
content: String::from("<!-- New HTML Page -->"),
|
||||
};
|
||||
|
||||
if file.is_none() {
|
||||
file = Option::Some(&blank_file);
|
||||
}
|
||||
|
||||
// ...
|
||||
let file = file.unwrap().to_owned();
|
||||
let file_content = file
|
||||
.content
|
||||
.replace("\\", "\\\\")
|
||||
.replace("`", "\\`")
|
||||
.replace("$", "\\$")
|
||||
.replace("/", "\\/");
|
||||
|
||||
let base = base::get_base_values(token_user.is_some());
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Set-Cookie", set_cookie))
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
EditorTemplate {
|
||||
custom_url: unwrap.custom_url,
|
||||
file,
|
||||
file_content,
|
||||
// required fields
|
||||
site_name: base.site_name,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
|
@ -7,6 +7,7 @@ pub struct BaseTemplate {
|
|||
pub auth_state: bool,
|
||||
pub guppy: String,
|
||||
pub puffer: String,
|
||||
pub vibrant: String,
|
||||
pub site_name: String,
|
||||
pub body_embed: String,
|
||||
}
|
||||
|
@ -34,6 +35,7 @@ pub fn get_base_values(token_cookie: bool) -> BaseTemplate {
|
|||
auth_state: token_cookie,
|
||||
guppy: std::env::var("GUPPY_ROOT").unwrap_or(String::new()),
|
||||
puffer: std::env::var("PUFFER_ROOT").unwrap_or(String::new()),
|
||||
vibrant: std::env::var("VIBRANT_ROOT").unwrap_or(String::new()),
|
||||
site_name: std::env::var("SITE_NAME").unwrap_or("Bundlrs".to_string()),
|
||||
body_embed,
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ struct DashboardTemplate {
|
|||
auth_state: bool,
|
||||
guppy: String,
|
||||
puffer: String,
|
||||
vibrant: String,
|
||||
site_name: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
@ -48,6 +49,7 @@ struct InboxTemplate {
|
|||
auth_state: bool,
|
||||
guppy: String,
|
||||
puffer: String,
|
||||
vibrant: String,
|
||||
site_name: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
@ -180,7 +182,7 @@ Allow: /
|
|||
Disallow: /api
|
||||
Disallow: /admin
|
||||
Disallow: /paste
|
||||
Disallow: /d/atomic
|
||||
Disallow: /dashboard
|
||||
Disallow: /*?",
|
||||
);
|
||||
}
|
||||
|
@ -219,6 +221,7 @@ pub async fn dashboard_request(req: HttpRequest, data: web::Data<AppData>) -> im
|
|||
auth_state: base.auth_state,
|
||||
guppy: base.guppy,
|
||||
puffer: base.puffer,
|
||||
vibrant: base.vibrant,
|
||||
site_name: base.site_name,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
|
@ -267,6 +270,7 @@ pub async fn inbox_request(
|
|||
auth_state: base.auth_state,
|
||||
guppy: base.guppy,
|
||||
puffer: base.puffer,
|
||||
vibrant: base.vibrant,
|
||||
site_name: base.site_name,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//! Page Routes ("/...")
|
||||
pub mod atomic_editor;
|
||||
pub mod base;
|
||||
pub mod errors;
|
||||
pub mod home;
|
||||
|
|
|
@ -48,6 +48,7 @@ struct DashboardTemplate {
|
|||
auth_state: bool,
|
||||
guppy: String,
|
||||
puffer: String,
|
||||
vibrant: String,
|
||||
site_name: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
@ -440,6 +441,7 @@ pub async fn dashboard_request(
|
|||
auth_state: base.auth_state,
|
||||
guppy: base.guppy,
|
||||
puffer: base.puffer,
|
||||
vibrant: base.vibrant,
|
||||
site_name: base.site_name,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
|
|
|
@ -1,689 +0,0 @@
|
|||
/**
|
||||
* @file Handle Atomic Paste file editor
|
||||
* @name AtomicEditor.ts
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// codemirror
|
||||
import { EditorState } from "@codemirror/state";
|
||||
|
||||
import { EditorView, keymap, placeholder } from "@codemirror/view";
|
||||
|
||||
import {
|
||||
syntaxHighlighting,
|
||||
indentOnInput,
|
||||
foldKeymap,
|
||||
HighlightStyle,
|
||||
indentUnit,
|
||||
} from "@codemirror/language";
|
||||
|
||||
import {
|
||||
autocompletion,
|
||||
completionKeymap,
|
||||
closeBracketsKeymap,
|
||||
CompletionContext,
|
||||
} from "@codemirror/autocomplete";
|
||||
|
||||
import {
|
||||
defaultKeymap,
|
||||
historyKeymap,
|
||||
indentWithTab,
|
||||
} from "@codemirror/commands";
|
||||
|
||||
import { basicSetup } from "codemirror";
|
||||
import { html, htmlCompletionSource } from "@codemirror/lang-html";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { css, cssCompletionSource } from "@codemirror/lang-css";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
import { linter, Diagnostic, lintGutter } from "@codemirror/lint";
|
||||
|
||||
// prettier
|
||||
// @ts-ignore
|
||||
import * as prettier from "prettier/standalone.mjs";
|
||||
import type { Options } from "prettier";
|
||||
|
||||
import EstreePlugin from "prettier/plugins/estree";
|
||||
import BabelParser from "prettier/plugins/babel";
|
||||
import CSSParser from "prettier/plugins/postcss";
|
||||
import HTMLParser from "prettier/plugins/html";
|
||||
|
||||
// create editor theme
|
||||
export const DefaultHighlight = HighlightStyle.define([
|
||||
{
|
||||
tag: tags.keyword,
|
||||
color: "var(--red3)",
|
||||
},
|
||||
{
|
||||
tag: tags.tagName,
|
||||
color: "var(--red3)",
|
||||
textShadow: "0 0 1px var(--red3)",
|
||||
},
|
||||
{
|
||||
tag: tags.variableName,
|
||||
color: "var(--blue2)",
|
||||
},
|
||||
{
|
||||
tag: tags.propertyName,
|
||||
color: "var(--red)",
|
||||
},
|
||||
{
|
||||
tag: tags.comment,
|
||||
color: "var(--text-color-faded)",
|
||||
},
|
||||
{
|
||||
tag: tags.number,
|
||||
color: "var(--yellow)",
|
||||
},
|
||||
{
|
||||
tag: tags.string,
|
||||
color: "var(--green)",
|
||||
},
|
||||
{
|
||||
tag: tags.operator,
|
||||
color: "var(--red3)",
|
||||
},
|
||||
{
|
||||
tag: tags.bool,
|
||||
color: "var(--blue2)",
|
||||
},
|
||||
{
|
||||
tag: tags.attributeName,
|
||||
color: "var(--blue2)",
|
||||
},
|
||||
{
|
||||
tag: tags.attributeValue,
|
||||
color: "var(--green)",
|
||||
},
|
||||
]);
|
||||
|
||||
// create lint
|
||||
import { HTMLHint } from "htmlhint";
|
||||
|
||||
let LastLint = performance.now();
|
||||
export const HTMLLint = linter((view) => {
|
||||
let diagnostics: Diagnostic[] = [];
|
||||
|
||||
// get hints
|
||||
const hints = HTMLHint.verify(
|
||||
view.state.sliceDoc(0, view.state.doc.length),
|
||||
{
|
||||
"doctype-first": true,
|
||||
// attributes (https://htmlhint.com/docs/user-guide/list-rules#attributes)
|
||||
"attr-lowercase": true,
|
||||
"attr-value-not-empty": true,
|
||||
"attr-value-double-quotes": true,
|
||||
// tags (https://htmlhint.com/docs/user-guide/list-rules#tags)
|
||||
"tag-self-close": true,
|
||||
"tag-pair": true,
|
||||
// id (https://htmlhint.com/docs/user-guide/list-rules#id)
|
||||
"id-unique": true,
|
||||
}
|
||||
);
|
||||
|
||||
// turn hints into diagnostics
|
||||
if (hints.length > 0 && performance.now() - LastLint > 100) {
|
||||
LastLint = performance.now(); // can only run lint every 100ms
|
||||
|
||||
// ...
|
||||
for (const hint of hints) {
|
||||
if (hint.line === view.state.doc.lines) hint.line = 1; // do not add an error to the last line (breaks editor)
|
||||
const line = view.state.doc.line(hint.line);
|
||||
|
||||
diagnostics.push({
|
||||
from: line.from + hint.col - 1,
|
||||
to: line.from + hint.col + hint.raw.length - 1,
|
||||
severity: hint.type,
|
||||
message: `${hint.message} (${hint.line}:${hint.col})\n${hint.rule.id}: ${hint.rule.description}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// return
|
||||
return diagnostics;
|
||||
});
|
||||
|
||||
export const EmptyLint = linter((view) => {
|
||||
let diagnostics: Diagnostic[] = [];
|
||||
|
||||
// return
|
||||
return diagnostics;
|
||||
});
|
||||
|
||||
// create completion context
|
||||
|
||||
/**
|
||||
* @function BasicCompletion
|
||||
*
|
||||
* @param {CompletionContext} context
|
||||
* @return {*}
|
||||
*/
|
||||
function BasicCompletion(context: CompletionContext): any {
|
||||
let word = context.matchBefore(/\w*/);
|
||||
if (!word || (word.from == word.to && !context.explicit)) return null;
|
||||
|
||||
return {
|
||||
from: word.from,
|
||||
options: [
|
||||
{
|
||||
label: "boilerplate",
|
||||
type: "variable",
|
||||
apply: `<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<span>Hello, world!</span>
|
||||
</body>
|
||||
</html>`,
|
||||
info: "Basic HTML Page Boilerplate",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// create editor function
|
||||
export function create_editor(
|
||||
element: HTMLElement,
|
||||
custom_url: string,
|
||||
path: string
|
||||
) {
|
||||
if (globalThis.Bun) return; // must be run from client
|
||||
const file_type = path.split(".").pop();
|
||||
|
||||
const view = new EditorView({
|
||||
// @ts-ignore
|
||||
state: EditorState.create({
|
||||
doc: "",
|
||||
extensions: [
|
||||
placeholder(path),
|
||||
syntaxHighlighting(DefaultHighlight, { fallback: true }),
|
||||
autocompletion({
|
||||
override: [
|
||||
BasicCompletion,
|
||||
path.endsWith("css")
|
||||
? cssCompletionSource
|
||||
: htmlCompletionSource, // html should always be the default
|
||||
],
|
||||
activateOnTyping: true,
|
||||
}),
|
||||
lintGutter(),
|
||||
// EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(async (update) => {
|
||||
if (update.docChanged) {
|
||||
const content = update.state.doc.toString();
|
||||
if (content === "") return;
|
||||
|
||||
(globalThis as any).AtomicEditor.Content = content;
|
||||
}
|
||||
}),
|
||||
// keymaps
|
||||
keymap.of({
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...indentWithTab,
|
||||
}),
|
||||
indentOnInput(),
|
||||
indentUnit.of(" "),
|
||||
// language
|
||||
path.endsWith("css")
|
||||
? css()
|
||||
: path.endsWith("js")
|
||||
? javascript()
|
||||
: html({ autoCloseTags: true }),
|
||||
path.endsWith("html") ? HTMLLint : EmptyLint,
|
||||
// default
|
||||
basicSetup,
|
||||
],
|
||||
}),
|
||||
parent: element,
|
||||
});
|
||||
|
||||
// global functions
|
||||
(globalThis as any).AtomicEditor = {
|
||||
Content: "",
|
||||
Update: (content: string, clear: boolean = false) => {
|
||||
const transaction = view.state.update({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: content,
|
||||
},
|
||||
scrollIntoView: true,
|
||||
});
|
||||
|
||||
if (transaction) {
|
||||
view.dispatch(transaction);
|
||||
}
|
||||
},
|
||||
Format: async () => {
|
||||
try {
|
||||
const formatted = await prettier.format(
|
||||
(globalThis as any).AtomicEditor.Content,
|
||||
{
|
||||
parser: "html",
|
||||
plugins: [
|
||||
EstreePlugin,
|
||||
BabelParser,
|
||||
HTMLParser,
|
||||
CSSParser,
|
||||
],
|
||||
htmlWhitespaceSensitivity: "ignore",
|
||||
// all from the project's .prettierrc
|
||||
useTabs: false,
|
||||
singleQuote: false,
|
||||
tabWidth: 4,
|
||||
trailingComma: "es5",
|
||||
printWidth: 85,
|
||||
semi: true,
|
||||
} as Options
|
||||
);
|
||||
|
||||
(globalThis as any).AtomicEditor.Update(formatted);
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// handle interactions
|
||||
let view_split: boolean = false;
|
||||
|
||||
const preview_button = document.getElementById(
|
||||
"preview"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
const split_button = document.getElementById(
|
||||
"split_view"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
const preview_browser = document.getElementById(
|
||||
"_preview_browser"
|
||||
) as HTMLDivElement | null;
|
||||
|
||||
const preview_pane = document.getElementById(
|
||||
"_preview_pane"
|
||||
) as HTMLIFrameElement | null;
|
||||
|
||||
if (split_button && preview_browser) {
|
||||
if (file_type !== "html") {
|
||||
split_button.remove();
|
||||
}
|
||||
|
||||
// split view on click
|
||||
split_button.addEventListener("click", () => {
|
||||
view_split = !view_split;
|
||||
|
||||
if (view_split) {
|
||||
preview_browser.style.display = "block";
|
||||
|
||||
split_button.classList.remove("red");
|
||||
split_button.classList.add("green");
|
||||
preview_button?.click(); // refresh preview
|
||||
} else {
|
||||
preview_browser.style.display = "none";
|
||||
|
||||
split_button.classList.remove("green");
|
||||
split_button.classList.add("red");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (preview_button && preview_pane) {
|
||||
let url: string = "";
|
||||
preview_button.addEventListener("click", () => {
|
||||
if (url.length > 0) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// create blob
|
||||
const blob = new Blob([(globalThis as any).AtomicEditor.Content], {
|
||||
type: "text/html",
|
||||
});
|
||||
|
||||
// get url
|
||||
url = URL.createObjectURL(blob);
|
||||
|
||||
// load
|
||||
preview_pane.src = url;
|
||||
|
||||
// interactions
|
||||
preview_pane.addEventListener("load", () => {
|
||||
// functions
|
||||
(globalThis as any).update_document_content = () => {
|
||||
// update content
|
||||
(globalThis as any).AtomicEditor.Update(
|
||||
`<!DOCTYPE html>\n\n${preview_pane.contentDocument?.documentElement.outerHTML}`
|
||||
);
|
||||
|
||||
// (globalThis as any).AtomicEditor.Format();
|
||||
};
|
||||
|
||||
// element focus
|
||||
preview_pane.contentDocument?.addEventListener("click", (e) => {
|
||||
build_element_property_window(e.target as HTMLElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const save_button = document.getElementById(
|
||||
"save"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
if (save_button) {
|
||||
save_button.addEventListener("click", async () => {
|
||||
const res = await fetch(`/api/atomic/crud/${custom_url}${path}`, {
|
||||
method: "POST",
|
||||
body: (globalThis as any).AtomicEditor.Content,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
return alert(json.message);
|
||||
} else {
|
||||
return alert("File saved");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// prevent exit
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
e.preventDefault();
|
||||
e.returnValue = true;
|
||||
});
|
||||
|
||||
// return
|
||||
return view;
|
||||
}
|
||||
|
||||
// ...
|
||||
function build_element_attribute_field(
|
||||
property_display: string,
|
||||
property_name: string,
|
||||
value: string
|
||||
): HTMLDivElement {
|
||||
const field = document.createElement("div");
|
||||
|
||||
field.className =
|
||||
"card less-padding secondary border round full flex flex-column g-2";
|
||||
|
||||
field.innerHTML = `<b>${property_display}</b><input
|
||||
value="${value}"
|
||||
placeholder="${property_name}"
|
||||
oninput="current_element.setAttribute('${property_name}', event.target.value); window.update_document_content();"
|
||||
onchange="window.AtomicEditor.Format();"
|
||||
class="full round"
|
||||
style="height: 35px !important;"
|
||||
/>`;
|
||||
|
||||
// return
|
||||
return field;
|
||||
}
|
||||
|
||||
function build_element_style_field(
|
||||
property_display: string,
|
||||
property_name: string,
|
||||
value: string
|
||||
): HTMLDivElement {
|
||||
const field = document.createElement("div");
|
||||
|
||||
field.className =
|
||||
"card less-padding secondary border round full flex flex-column g-2";
|
||||
|
||||
field.innerHTML = `<b>${property_display}</b><input
|
||||
value="${value}"
|
||||
placeholder="${property_name}"
|
||||
oninput="current_element.style.setProperty('${property_name}', event.target.value); window.update_document_content();"
|
||||
onchange="window.AtomicEditor.Format();"
|
||||
class="full round"
|
||||
style="height: 35px !important;"
|
||||
/>`;
|
||||
|
||||
// return
|
||||
return field;
|
||||
}
|
||||
|
||||
function build_element_field(
|
||||
property_display: string,
|
||||
property_name: string,
|
||||
value: string
|
||||
): HTMLDivElement {
|
||||
const field = document.createElement("div");
|
||||
|
||||
field.className =
|
||||
"card less-padding secondary border round full flex flex-column g-2";
|
||||
|
||||
field.innerHTML = `<b>${property_display}</b><input
|
||||
value="${value}"
|
||||
placeholder="${property_name}"
|
||||
oninput="current_element['${property_name}'] = event.target.value; window.update_document_content();"
|
||||
onchange="window.AtomicEditor.Format();"
|
||||
class="full round"
|
||||
style="height: 35px !important;"
|
||||
/>`;
|
||||
|
||||
// return
|
||||
return field;
|
||||
}
|
||||
|
||||
function build_element_property_window(element: HTMLElement): void {
|
||||
if (document.getElementById("property_window")) {
|
||||
document.getElementById("property_window")!.remove();
|
||||
(globalThis as any).current_element.style.removeProperty("box-shadow");
|
||||
}
|
||||
|
||||
if (document.getElementById("preview_box")) {
|
||||
document.getElementById("preview_box")!.remove();
|
||||
}
|
||||
|
||||
(globalThis as any).current_element = element;
|
||||
|
||||
// preview box
|
||||
(globalThis as any).create_preview_box = () => {
|
||||
const preview_box = document.createElement("div");
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
preview_box.style.position = "absolute";
|
||||
preview_box.style.top = `${rect.top}px`;
|
||||
preview_box.style.left = `${rect.left}px`;
|
||||
preview_box.style.width = `${rect.width}px`;
|
||||
preview_box.style.height = `${rect.height}px`;
|
||||
preview_box.style.background = "transparent";
|
||||
preview_box.style.boxShadow = "0 0 0 4px #00FF00";
|
||||
|
||||
preview_box.id = "preview_box";
|
||||
element.appendChild(preview_box);
|
||||
};
|
||||
|
||||
(globalThis as any).remove_preview_box = () => {
|
||||
if (element.querySelector("#preview_box")) {
|
||||
element.querySelector("#preview_box")!.remove();
|
||||
}
|
||||
};
|
||||
|
||||
// create property window
|
||||
const property_window = document.createElement("div");
|
||||
|
||||
property_window.style.position = "fixed";
|
||||
property_window.style.top = "0";
|
||||
property_window.style.left = "0";
|
||||
property_window.style.width = "25rem";
|
||||
property_window.style.maxWidth = "100dvw";
|
||||
property_window.style.maxHeight = "calc(50% - 22px)";
|
||||
property_window.style.boxShadow = "-2px 2px 4px hsla(0, 0%, 0%, 25%)";
|
||||
property_window.style.overflow = "auto";
|
||||
|
||||
property_window.className = "card border flex flex-column g-4";
|
||||
property_window.id = "property_window";
|
||||
|
||||
// titlebar
|
||||
const titlebar = document.createElement("div");
|
||||
|
||||
titlebar.className =
|
||||
"bg-0 full flex align-center justify-space-between g-4";
|
||||
|
||||
// titlebar.style.position = "sticky";
|
||||
// titlebar.style.top = "0";
|
||||
|
||||
property_window.appendChild(titlebar);
|
||||
|
||||
const titlebar_title = document.createElement("b");
|
||||
titlebar_title.innerText = element.nodeName;
|
||||
titlebar.appendChild(titlebar_title);
|
||||
|
||||
const close_button = document.createElement("button");
|
||||
close_button.className = "round";
|
||||
close_button.innerText = "Close";
|
||||
|
||||
close_button.addEventListener("click", () => {
|
||||
property_window.remove();
|
||||
(globalThis as any).current_element.style.removeProperty("box-shadow");
|
||||
(globalThis as any).update_document_content();
|
||||
(globalThis as any).AtomicEditor.Format();
|
||||
});
|
||||
|
||||
titlebar.appendChild(close_button);
|
||||
|
||||
// basic fields
|
||||
property_window.appendChild(
|
||||
build_element_field("Text Content", "innerText", element.innerText)
|
||||
);
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_field("Class Name", "className", element.className)
|
||||
);
|
||||
|
||||
property_window.appendChild(build_element_field("ID", "id", element.id));
|
||||
|
||||
// attributes
|
||||
property_window.appendChild(document.createElement("hr"));
|
||||
|
||||
const attribute_list = document.createElement("div");
|
||||
attribute_list.className = "flex flex-column g-2";
|
||||
attribute_list.id = "attribute_list";
|
||||
property_window.appendChild(attribute_list);
|
||||
|
||||
// "add field" button
|
||||
const add_attr_button = document.createElement("button");
|
||||
add_attr_button.innerText = "Add Custom Attribute";
|
||||
add_attr_button.className = "full round";
|
||||
add_attr_button.addEventListener("click", () => {
|
||||
const name = prompt("Attribute Name: ");
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
attribute_list.appendChild(
|
||||
build_element_attribute_field(name, name, "")
|
||||
);
|
||||
});
|
||||
|
||||
attribute_list.appendChild(add_attr_button);
|
||||
|
||||
// from existing attributes
|
||||
const attributes = element.attributes;
|
||||
|
||||
for (const attr of Object.values(attributes)) {
|
||||
attribute_list.appendChild(
|
||||
build_element_attribute_field(attr.name, attr.name, attr.value)
|
||||
);
|
||||
}
|
||||
|
||||
// style fields
|
||||
property_window.appendChild(document.createElement("hr"));
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_style_field(
|
||||
"Background",
|
||||
"background",
|
||||
element.style.background
|
||||
)
|
||||
);
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_style_field("Color", "color", element.style.color)
|
||||
);
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_style_field("Border", "border", element.style.border)
|
||||
);
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_style_field("Width", "width", element.style.width)
|
||||
);
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_style_field("Padding", "padding", element.style.padding)
|
||||
);
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_style_field("Margin", "margin", element.style.margin)
|
||||
);
|
||||
|
||||
property_window.appendChild(
|
||||
build_element_style_field("Display", "display", element.style.display)
|
||||
);
|
||||
|
||||
property_window.appendChild(document.createElement("hr"));
|
||||
|
||||
// "add field" button
|
||||
const add_button = document.createElement("button");
|
||||
add_button.innerText = "Add Custom Style";
|
||||
add_button.className = "full round";
|
||||
add_button.addEventListener("click", () => {
|
||||
const name = prompt("Property Name: ");
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
property_window.appendChild(build_element_style_field(name, name, ""));
|
||||
});
|
||||
|
||||
property_window.appendChild(add_button);
|
||||
|
||||
// from style attribute
|
||||
const styles_list = document.createElement("div");
|
||||
styles_list.className = "flex flex-column g-2";
|
||||
styles_list.id = "styles_list";
|
||||
property_window.appendChild(styles_list);
|
||||
|
||||
const styles_from_attribute = element.style;
|
||||
|
||||
if (styles_from_attribute) {
|
||||
for (const style of Object.values(styles_from_attribute)) {
|
||||
const value = element.style.getPropertyValue(style);
|
||||
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
styles_list.appendChild(
|
||||
build_element_style_field(style, style, value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// append
|
||||
document.body.appendChild(property_window);
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {
|
||||
DefaultHighlight,
|
||||
create_editor,
|
||||
};
|
|
@ -420,6 +420,13 @@ export default function CreateEditor(ElementID: string, content: string) {
|
|||
|
||||
window.localStorage.setItem("LastEditURL", window.location.href);
|
||||
|
||||
// vibrant warning
|
||||
if (content && content.includes('"_is_atomic":true')) {
|
||||
alert(
|
||||
'This paste needs to be moved to a Vibrant project. Please check the "Vibrant" tab on your user dashboard for more information.'
|
||||
);
|
||||
}
|
||||
|
||||
// create editor
|
||||
const view = new EditorView({
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
const error: HTMLElement = document.getElementById("error")!;
|
||||
const success: HTMLElement = document.getElementById("success")!;
|
||||
|
||||
function init_delete_buttons() {
|
||||
const delete_buttons: HTMLButtonElement[] = Array.from(
|
||||
document.getElementsByClassName("action:delete-file")
|
||||
) as HTMLButtonElement[];
|
||||
|
||||
if (delete_buttons) {
|
||||
// delete files
|
||||
for (const delete_button of delete_buttons) {
|
||||
delete_button.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch(
|
||||
delete_button.getAttribute("data-endpoint")!,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
success.style.display = "block";
|
||||
success.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
}
|
||||
|
||||
(
|
||||
document.getElementById("more-modal") as HTMLDialogElement
|
||||
).close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const more_buttons: HTMLButtonElement[] = Array.from(
|
||||
document.getElementsByClassName("action:more-modal")
|
||||
) as HTMLButtonElement[];
|
||||
|
||||
const more_modal_actions: HTMLDivElement | null = document.getElementById(
|
||||
"more-modal-actions"
|
||||
) as HTMLDivElement | null;
|
||||
|
||||
if (more_buttons && more_modal_actions) {
|
||||
for (const button of more_buttons) {
|
||||
button.addEventListener("click", () => {
|
||||
const data_suffix = button.getAttribute("data-suffix")!;
|
||||
|
||||
more_modal_actions.innerHTML = `<a class="button full justify-start round" target="_blank" href="/+${data_suffix}" title="View File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
View
|
||||
</a>
|
||||
|
||||
<button class="red round full justify-start action:delete-file" data-endpoint="/api/atomic/crud/${data_suffix}" title="Delete File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||
Delete
|
||||
</button>`;
|
||||
|
||||
(
|
||||
document.getElementById("more-modal") as HTMLDialogElement
|
||||
).showModal();
|
||||
|
||||
init_delete_buttons();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const custom_url = (document.getElementById("custom_url") as HTMLDivElement)
|
||||
.innerText;
|
||||
|
||||
const delete_button = document.getElementById(
|
||||
"delete"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
if (delete_button) {
|
||||
delete_button.addEventListener("click", async () => {
|
||||
const _confirm = confirm(
|
||||
"Are you sure you would like to do this? This URL will be available for anybody to claim. **This will delete the entire paste and all its files!"
|
||||
);
|
||||
|
||||
if (!_confirm) return;
|
||||
|
||||
const edit_password = prompt(
|
||||
"Please enter this paste's edit password:"
|
||||
);
|
||||
|
||||
if (!edit_password) return;
|
||||
|
||||
const res = await fetch("/api/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
custom_url,
|
||||
edit_password: edit_password,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
return alert(json.message);
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
|
@ -1,44 +0,0 @@
|
|||
const error: HTMLElement = document.getElementById("error")!;
|
||||
const create_form: HTMLFormElement | null = document.getElementById(
|
||||
"create-site"
|
||||
) as HTMLFormElement | null;
|
||||
|
||||
if (create_form) {
|
||||
// create site
|
||||
create_form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/new", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
custom_url: create_form.custom_url.value,
|
||||
edit_password: crypto.randomUUID(),
|
||||
group_name: "",
|
||||
content: JSON.stringify({
|
||||
// db::bundlesdb::AtomicPaste
|
||||
_is_atomic: true,
|
||||
files: [
|
||||
{
|
||||
path: "/index.html",
|
||||
content: "<!-- New HTML Page -->",
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
window.location.href = `/d/atomic/${json.payload.id}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
|
@ -9,15 +9,12 @@ if (process.env.DO_NOT_CLEAR_DIST === undefined)
|
|||
|
||||
const output = await build({
|
||||
entrypoints: [
|
||||
"./static/ts/editors/AtomicEditor.ts",
|
||||
"./static/ts/editors/MarkdownEditor.ts",
|
||||
"./static/ts/editors/ClientFixMarkdown.ts",
|
||||
"./static/ts/editors/SettingsEditor.ts",
|
||||
"./static/ts/pages/Footer.ts",
|
||||
"./static/ts/pages/NewAtomic.ts",
|
||||
"./static/ts/pages/ManageBoardPost.ts",
|
||||
"./static/ts/pages/SDManageUser.ts",
|
||||
"./static/ts/pages/AtomicOverview.ts",
|
||||
],
|
||||
minify: {
|
||||
identifiers: true,
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
{% extends "../../toolbar_base.html" %}
|
||||
|
||||
{% block title %}Dashboard - {{ site_name }}{% endblock %}
|
||||
{% block main_stuff %}style="overflow: hidden; max-height: 100%;"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="link-header" style="display: flex;" class="flex-column bg-1">
|
||||
<div class="link-header-top"></div>
|
||||
|
||||
<div class="link-header-middle">
|
||||
<h1 class="no-margin">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="link-header-bottom">
|
||||
<a href="/dashboard" class="button">Home</a>
|
||||
<a href="/dashboard/pastes" class="button">Pastes</a>
|
||||
<a href="/dashboard/atomic" class="button active">Atomic</a>
|
||||
<a href="{{ puffer }}/dashboard" class="button">Boards</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="small flex flex-column g-4">
|
||||
<div class="flex justify-space-between align-center">
|
||||
<b>Atomic Pastes</b>
|
||||
|
||||
<a class="button theme:primary round" href="/dashboard/atomic/new">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-plus-square">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M8 12h8" />
|
||||
<path d="M12 8v8" />
|
||||
</svg>
|
||||
New
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card round secondary flex g-4 flex-column justify-center" id="pastes_list">
|
||||
{% for p in pastes.iter() %}
|
||||
<a class="button secondary round full justify-start no-shadow" href="/dashboard/atomic/{{ p.id }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-folder-archive">
|
||||
<circle cx="15" cy="19" r="2" />
|
||||
<path
|
||||
d="M20.9 19.8A2 2 0 0 0 22 18V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h5.1" />
|
||||
<path d="M15 11v-1" />
|
||||
<path d="M15 17v-2" />
|
||||
</svg>
|
||||
{{ p.custom_url }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -1,105 +0,0 @@
|
|||
{% extends "../../base.html" %}
|
||||
|
||||
{% block title %}{{ file.path }} - {{ custom_url }}{% endblock %}
|
||||
|
||||
{% block base_content %}
|
||||
<div class="flex flex-column" style="height: 100dvh;">
|
||||
<div class="panes flex mobile:flex-column" style="height: 100%; overflow: auto;">
|
||||
<div id="_doc" class="full" style="height: 100%; overflow: auto; display: block;"></div>
|
||||
|
||||
<div id="_preview_browser" class="full" style="height: 100%; overflow: hidden; display: none;">
|
||||
<div class="full flex g-4 bg-0" style="padding: var(--u-04); height: 47.8px;">
|
||||
<button class="round" id="preview" title="Refresh Preview">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-refresh-cw">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
||||
<path d="M21 3v5h-5" />
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
||||
<path d="M8 16H3v5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<iframe id="_preview_pane" class="full" style="height: calc(100% - 47.8px); overflow: auto;" frameborder="0"
|
||||
src="about:blank"></iframe>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#_preview_pane {
|
||||
background: white;
|
||||
}
|
||||
|
||||
#_preview_browser {
|
||||
border-left: solid 1px var(--background-surface2a);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
#_preview_browser {
|
||||
border-left: 0;
|
||||
border-top: solid 1px var(--background-surface2a);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<div class="card secondary flex mobile:justify-center justify-space-between align-center" style="
|
||||
overflow: auto hidden;
|
||||
border-top: 1px solid var(--background-surface2a);
|
||||
padding: var(--u-04);
|
||||
height: 47.8px;
|
||||
">
|
||||
<b style="min-width: max-content;" class="device:desktop">{{ file.path }}</b>
|
||||
|
||||
<div class="flex g-4">
|
||||
<button class="round secondary green" id="save" title="Save File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-save">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
||||
<polyline points="17 21 17 13 7 13 7 21" />
|
||||
<polyline points="7 3 7 8 15 8" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<a href="?" class="button round secondary" id="file_explorer" title="Manage Files">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-folder-tree">
|
||||
<path
|
||||
d="M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z" />
|
||||
<path
|
||||
d="M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z" />
|
||||
<path d="M3 5a2 2 0 0 0 2 2h3" />
|
||||
<path d="M3 3v13a2 2 0 0 0 2 2h3" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<div class="hr-left"></div>
|
||||
|
||||
<button class="round secondary red" id="split_view" title="Split View">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-columns-2">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M12 3v18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { create_editor } from "/static/js/AtomicEditor.js";
|
||||
create_editor(document.getElementById('_doc'), '{{ custom_url }}', '{{ file.path }}');
|
||||
globalThis.AtomicEditor.Update(`{{ file_content|safe }}`)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.cm-editor,
|
||||
.cm-line,
|
||||
.cm-line span {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,49 +0,0 @@
|
|||
{% extends "../../toolbar_base.html" %}
|
||||
|
||||
{% block title %}Dashboard - {{ site_name }}{% endblock %}
|
||||
{% block main_stuff %}style="overflow: hidden; max-height: 100%;"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="link-header" style="display: flex;" class="flex-column bg-1">
|
||||
<div class="link-header-top"></div>
|
||||
|
||||
<div class="link-header-middle">
|
||||
<h1 class="no-margin">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="link-header-bottom">
|
||||
<a href="/dashboard" class="button">Home</a>
|
||||
<a href="/dashboard/pastes" class="button">Pastes</a>
|
||||
<a href="/dashboard/atomic" class="button active">Atomic</a>
|
||||
<a href="{{ puffer }}/dashboard" class="button">Boards</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="flex align-center flex-column g-4 small">
|
||||
<div class="card secondary round border" style="width: 25rem;" id="forms">
|
||||
<div id="error" class="mdnote note-error full" style="display: none;"></div>
|
||||
<form class="full flex flex-column g-4" action="/api/auth/register" id="create-site">
|
||||
<label for="custom_url"><b>Custom URL</b></label>
|
||||
|
||||
<input type="text" name="custom_url" id="custom_url" placeholder="Custom URL" class="full round"
|
||||
minlength="4" maxlength="32" required="true" />
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="theme:primary full round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-plus">
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
Create Site
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import "/static/js/NewAtomic.js";
|
||||
</script>
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -1,137 +0,0 @@
|
|||
{% extends "../../toolbar_base.html" %}
|
||||
|
||||
{% block title %}{{ custom_url }} - {{ site_name }}{% endblock %}
|
||||
{% block main_stuff %}style="overflow: hidden; max-height: 100%;"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="link-header" style="display: flex;" class="flex-column bg-1">
|
||||
<div class="link-header-top"></div>
|
||||
|
||||
<div class="link-header-middle">
|
||||
<h1 class="no-margin">{{ custom_url }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="link-header-bottom">
|
||||
<a href="/dashboard" class="button">Home</a>
|
||||
<a href="/dashboard/pastes" class="button">Pastes</a>
|
||||
<a href="/dashboard/atomic" class="button active">Atomic</a>
|
||||
<a href="{{ puffer }}/dashboard" class="button">Boards</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="flex flex-column g-4 small">
|
||||
<div id="error" class="mdnote note-error full" style="display: none;"></div>
|
||||
<div id="success" class="mdnote note-note full" style="display: none;"></div>
|
||||
|
||||
<div id="custom_url" style="display: none;">{{ custom_url }}</div>
|
||||
|
||||
<form class="flex justify-center align-center g-4">
|
||||
<input type="text" placeholder="/index.(html|css|js)" name="path" class="round full" minlength="4" />
|
||||
<button class="round theme:primary" style="min-width: max-content;">Open</button>
|
||||
</form>
|
||||
|
||||
<table class="full stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for p in files.iter() %}
|
||||
<tr>
|
||||
<td><a href="?path={{ p.path }}">{{ p.path }}</a></td>
|
||||
|
||||
<td class="flex g-4 flex-wrap">
|
||||
<a class="button secondary round no-shadow" href="?path={{ p.path }}" title="Edit File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-file-pen-line">
|
||||
<path d="m18 5-3-3H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2" />
|
||||
<path d="M8 18h1" />
|
||||
<path d="M18.4 9.6a2 2 0 1 1 3 3L17 17l-4 1 1-4Z" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<button class="secondary round action:more-modal no-shadow" data-suffix="{{ custom_url }}{{ p.path }}"
|
||||
title="More Options">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-wrench">
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr />
|
||||
|
||||
<h6 class="no-margin">Paste Options</h6>
|
||||
|
||||
<table class="full stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>View</td>
|
||||
<td>
|
||||
<a class="button round secondary no-shadow" target="_blank" href="/{{ custom_url }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-circle-play">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polygon points="10 8 16 12 10 16 10 8" />
|
||||
</svg>
|
||||
Run
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Delete</td>
|
||||
<td>
|
||||
<button class="round secondary no-shadow" id="delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-circle-play">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polygon points="10 8 16 12 10 16 10 8" />
|
||||
</svg>
|
||||
Run
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<dialog id="more-modal">
|
||||
<div style="width: 25rem; max-width: 100%;">
|
||||
<h2 class="no-margin full text-center">More Options</h2>
|
||||
|
||||
<hr />
|
||||
<div id="more-modal-actions" class="flex flex-column g-4"></div>
|
||||
<hr />
|
||||
|
||||
<div class="full flex justify-right">
|
||||
<a class="button round red" href="javascript:document.getElementById('more-modal').close();">
|
||||
Close
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script type="module">
|
||||
import "/static/js/AtomicOverview.js";
|
||||
</script>
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -14,7 +14,7 @@
|
|||
<div class="link-header-bottom">
|
||||
<a href="/dashboard" class="button">Home</a>
|
||||
<a href="/dashboard/pastes" class="button active">Pastes</a>
|
||||
<a href="/dashboard/atomic" class="button">Atomic</a>
|
||||
<a href="{{ vibrant }}/dashboard" class="button">Vibrant</a>
|
||||
<a href="{{ puffer }}/dashboard" class="button">Boards</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="link-header-bottom">
|
||||
<a href="/dashboard" class="button active">Home</a>
|
||||
<a href="/dashboard/pastes" class="button">Pastes</a>
|
||||
<a href="/dashboard/atomic" class="button">Atomic</a>
|
||||
<a href="{{ vibrant }}/dashboard" class="button">Vibrant</a>
|
||||
<a href="{{ puffer }}/dashboard" class="button">Boards</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,7 +44,7 @@ padding-top: var(--u-04);" class="flex flex-column align-center"{% endblock %}
|
|||
|
||||
<form class="flex flex-wrap mobile:justify-center justify-space-between g-4 align-center" id="save-changes">
|
||||
{% if edit_mode == false %}
|
||||
<div class="mobile:justify-center flex g-4 justify-start">
|
||||
<div class="device:desktop mobile:justify-center flex g-4 justify-start">
|
||||
<button class="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
|
@ -60,13 +60,42 @@ padding-top: var(--u-04);" class="flex flex-column align-center"{% endblock %}
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mobile:justify-center flex-wrap flex g-4 justify-start">
|
||||
<div class="flex g-4 justify-start">
|
||||
<input class="secondary round" type="text" placeholder="Custom URL" minlength="2" maxlength="500"
|
||||
name="custom_url" id="custom_url" autocomplete="off" />
|
||||
|
||||
<input class="secondary round" type="text" placeholder="Edit Password" minlength="5" name="edit_password" />
|
||||
<input class="secondary round" type="text" placeholder="Edit Password" minlength="5" name="edit_password"
|
||||
id="edit_password" />
|
||||
</div>
|
||||
|
||||
<div class="full device:mobile mobile:flex mobile:justify-center flex g-4 justify-start">
|
||||
<button class="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-plus">
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
Publish
|
||||
</button>
|
||||
|
||||
<a class="button round border" href="javascript:document.getElementById('more-modal').showModal();">
|
||||
More
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media screen and (max-width: 900px) {
|
||||
#custom_url {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#edit_password {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<dialog id="more-modal">
|
||||
<div style="width: 25rem; max-width: 100%;">
|
||||
<h2 class="no-margin full text-center">More Options</h2>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="link-header-bottom">
|
||||
<a href="/dashboard" class="button">Home</a>
|
||||
<a href="/dashboard/pastes" class="button">Pastes</a>
|
||||
<a href="/dashboard/atomic" class="button">Atomic</a>
|
||||
<a href="{{ vibrant }}/dashboard" class="button">Vibrant</a>
|
||||
<a href="{{ puffer }}/dashboard" class="button">Boards</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue