ui: defaults
This commit is contained in:
parent
71a381b62a
commit
3f46b7669d
4 changed files with 138 additions and 101 deletions
|
|
@ -169,12 +169,12 @@ impl Config {
|
|||
.unwrap_or_default(),
|
||||
form_prompt: env::var("BOOK_FORM_PROMPT").unwrap_or_default(),
|
||||
button_text: env::var("BOOK_BUTTON_TEXT")
|
||||
.unwrap_or_else(|_| "sign".into()),
|
||||
.unwrap_or_else(|_| "Submit".into()),
|
||||
label_name: env::var("BOOK_LABEL_NAME").unwrap_or_else(|_| "name".into()),
|
||||
label_website: env::var("BOOK_LABEL_WEBSITE")
|
||||
.unwrap_or_else(|_| "website (optional)".into()),
|
||||
label_message: env::var("BOOK_LABEL_MESSAGE")
|
||||
.unwrap_or_else(|_| "message".into()),
|
||||
.unwrap_or_else(|_| "message (optional)".into()),
|
||||
textarea_width: env::var("BOOK_TEXTAREA_WIDTH")
|
||||
.unwrap_or_else(|_| "320".into())
|
||||
.parse()
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub fn render_page(template: &str, config: &Config, entries: &[Entry], form_html
|
|||
pub fn render_form(config: &Config) -> String {
|
||||
let website_section = if config.enable_website_links {
|
||||
format!(
|
||||
"\n<label class=\"guestbook-label\" for=\"website\">{label}</label>\n<input class=\"guestbook-input\" id=\"website\" name=\"website\" placeholder=\"{label}\">\n",
|
||||
"<label class=\"guestbook-label\" for=\"website\">{label}</label>\n<input class=\"guestbook-input\" id=\"website\" name=\"website\">\n",
|
||||
label = config.label_website
|
||||
)
|
||||
} else {
|
||||
|
|
@ -41,7 +41,7 @@ pub fn render_form(config: &Config) -> String {
|
|||
|
||||
let captcha_section = if config.enable_captcha {
|
||||
format!(
|
||||
"\n<label class=\"guestbook-label\" for=\"captcha\">{label}</label>\n<input class=\"guestbook-input\" id=\"captcha\" name=\"captcha\" placeholder=\"{label}\" required>\n",
|
||||
"<label class=\"guestbook-label\" for=\"captcha\">{label}</label>\n<input class=\"guestbook-input\" id=\"captcha\" name=\"captcha\" required>\n",
|
||||
label = config.captcha_question
|
||||
)
|
||||
} else {
|
||||
|
|
@ -50,9 +50,9 @@ pub fn render_form(config: &Config) -> String {
|
|||
|
||||
let drawing_section = if config.enable_drawings {
|
||||
format!(
|
||||
r##"<span class="guestbook-drawing-wrap"><span class="guestbook-drawing-inline"><a href="#" class="guestbook-drawing-toggle">add a drawing</a></span>
|
||||
<span class="guestbook-drawing-content"></span></span><input type="hidden" name="drawing"><script>(function(){{
|
||||
var inl=document.querySelector('.guestbook-drawing-inline'),
|
||||
r##"<span class="guestbook-label">drawing (optional)</span>
|
||||
<span class="guestbook-drawing-wrap"><span class="guestbook-drawing-tools"></span><span class="guestbook-drawing-content"></span></span><input type="hidden" name="drawing"><script>(function(){{
|
||||
var inl=document.querySelector('.guestbook-drawing-tools'),
|
||||
cnt=document.querySelector('.guestbook-drawing-content'),
|
||||
hid=document.querySelector('[name=drawing]'),
|
||||
c,x,d=false,lx,ly,h=[],col='#000',sz=5;
|
||||
|
|
@ -69,13 +69,11 @@ pub fn render_form(config: &Config) -> String {
|
|||
c.addEventListener('touchstart',function(e){{e.preventDefault();save();var p=tpos(e);lx=p[0];ly=p[1];dot(lx,ly)}});
|
||||
c.addEventListener('touchmove',function(e){{e.preventDefault();var p=tpos(e);x.beginPath();x.moveTo(lx,ly);x.lineTo(p[0],p[1]);x.stroke();lx=p[0];ly=p[1]}});
|
||||
}}
|
||||
function showCanvas(){{
|
||||
inl.innerHTML='';
|
||||
var sw=[{{c:'#000',n:'black'}},{{c:'#e03131',n:'red'}},{{c:'#2f9e44',n:'green'}},{{c:'#1971c2',n:'blue'}}];
|
||||
sw.forEach(function(s,i){{
|
||||
var sp=document.createElement('span');
|
||||
sp.className='guestbook-swatch'+(i===0?' active':'');
|
||||
sp.setAttribute('data-c',s.c);sp.style.background=s.c;
|
||||
sp.style.background=s.c;
|
||||
sp.setAttribute('role','button');sp.setAttribute('aria-label',s.n);
|
||||
sp.addEventListener('click',function(){{
|
||||
inl.querySelectorAll('.guestbook-swatch').forEach(function(el){{el.classList.remove('active')}});
|
||||
|
|
@ -92,25 +90,17 @@ pub fn render_form(config: &Config) -> String {
|
|||
undo.addEventListener('click',function(e){{e.preventDefault();if(h.length)x.putImageData(h.pop(),0,0)}});
|
||||
inl.appendChild(undo);
|
||||
inl.appendChild(document.createTextNode(' | '));
|
||||
var disc=document.createElement('a');disc.href='#';disc.textContent='discard';
|
||||
disc.addEventListener('click',function(e){{
|
||||
e.preventDefault();h=[];col='#000';sz=5;d=false;cnt.innerHTML='';hid.value='';setInit();
|
||||
var clr=document.createElement('a');clr.href='#';clr.textContent='clear';
|
||||
clr.addEventListener('click',function(e){{
|
||||
e.preventDefault();h=[];x.clearRect(0,0,c.width,c.height);hid.value='';
|
||||
}});
|
||||
inl.appendChild(disc);
|
||||
inl.appendChild(clr);
|
||||
c=document.createElement('canvas');c.className='guestbook-canvas';c.width={w};c.height={h};c.setAttribute('aria-label','Drawing canvas');
|
||||
cnt.innerHTML='';cnt.appendChild(c);bindCanvas();
|
||||
cnt.appendChild(c);bindCanvas();
|
||||
c.closest('form').addEventListener('submit',function(){{
|
||||
var px=new Uint32Array(x.getImageData(0,0,c.width,c.height).data.buffer);
|
||||
if(px.some(function(v){{return v!==0}})){{hid.value=c.toDataURL('image/png')}}
|
||||
}});
|
||||
}}
|
||||
function setInit(){{
|
||||
inl.innerHTML='';
|
||||
var a=document.createElement('a');a.href='#';a.textContent='add a drawing';
|
||||
a.addEventListener('click',function(e){{e.preventDefault();showCanvas()}});
|
||||
inl.appendChild(a);
|
||||
}}
|
||||
inl.querySelector('a').addEventListener('click',function(e){{e.preventDefault();showCanvas()}});
|
||||
}})();</script>"##,
|
||||
w = config.canvas_width,
|
||||
h = config.canvas_height,
|
||||
|
|
@ -121,9 +111,10 @@ pub fn render_form(config: &Config) -> String {
|
|||
|
||||
let voice_note_section = if config.enable_voice_notes {
|
||||
format!(
|
||||
r##"<span class="guestbook-voice-wrap"><span class="guestbook-voice-inline"><a href="#" class="guestbook-voice-record">add a voice note</a> <span class="guestbook-voice-timer"></span></span><span class="guestbook-voice-playback"></span></span><input type="hidden" name="voice_note"><script>(function(){{
|
||||
r##"<span class="guestbook-label">voice note (optional)</span>
|
||||
<span class="guestbook-voice-wrap"><span class="guestbook-voice-controls"></span><span class="guestbook-voice-playback"></span></span><input type="hidden" name="voice_note"><script>(function(){{
|
||||
var maxDur={max_dur};
|
||||
var inl=document.querySelector('.guestbook-voice-inline'),
|
||||
var inl=document.querySelector('.guestbook-voice-controls'),
|
||||
pb=document.querySelector('.guestbook-voice-playback'),
|
||||
hid=document.querySelector('[name=voice_note]'),
|
||||
rec=null,chunks=[],iv=null,st=0;
|
||||
|
|
@ -132,7 +123,8 @@ pub fn render_form(config: &Config) -> String {
|
|||
if(rec&&rec.state==='recording'){{rec.stop();rec.stream.getTracks().forEach(function(t){{t.stop()}})}}
|
||||
rec=null;chunks=[];clearInterval(iv);iv=null;pb.innerHTML='';hid.value='';
|
||||
inl.innerHTML='';
|
||||
var a=document.createElement('a');a.href='#';a.textContent='add a voice note';
|
||||
var a=document.createElement('a');a.href='#';a.className='guestbook-voice-record';
|
||||
a.textContent='record';
|
||||
a.addEventListener('click',function(e){{e.preventDefault();startRec()}});
|
||||
inl.appendChild(a);
|
||||
}}
|
||||
|
|
@ -160,7 +152,7 @@ pub fn render_form(config: &Config) -> String {
|
|||
inl.appendChild(re);inl.appendChild(document.createTextNode(' | '));inl.appendChild(disc);
|
||||
var url=URL.createObjectURL(blob);
|
||||
var au=document.createElement('audio');au.controls=true;au.preload='metadata';au.src=url;
|
||||
pb.innerHTML='';pb.appendChild(au);
|
||||
pb.appendChild(au);
|
||||
var rd=new FileReader();rd.onload=function(){{hid.value=rd.result}};rd.readAsDataURL(blob);
|
||||
}}
|
||||
function startRec(){{
|
||||
|
|
@ -171,11 +163,11 @@ pub fn render_form(config: &Config) -> String {
|
|||
rec.onstop=function(){{setResult()}};
|
||||
rec.start();setRec();
|
||||
}}).catch(function(){{
|
||||
inl.querySelector('a').textContent='add a voice note';
|
||||
inl.appendChild(document.createTextNode(' (mic denied)'));
|
||||
inl.innerHTML='';
|
||||
inl.appendChild(document.createTextNode('mic access denied'));
|
||||
}});
|
||||
}}
|
||||
inl.querySelector('a').addEventListener('click',function(e){{e.preventDefault();startRec()}});
|
||||
setInit();
|
||||
}})();</script>"##,
|
||||
max_dur = config.voice_note_max_duration,
|
||||
)
|
||||
|
|
@ -184,23 +176,24 @@ pub fn render_form(config: &Config) -> String {
|
|||
};
|
||||
|
||||
format!(
|
||||
r#"<form class="guestbook-form" method="post" action="/submit" accept-charset="UTF-8">
|
||||
r#"<details class="guestbook-details">
|
||||
<summary class="guestbook-summary">Leave your own message.</summary>
|
||||
<form class="guestbook-form" method="post" action="/submit" accept-charset="UTF-8">
|
||||
<label class="guestbook-label" for="name">{label_name}</label>
|
||||
<input class="guestbook-input" id="name" name="name" placeholder="{label_name}" required>
|
||||
{website_section}
|
||||
<label class="guestbook-label" for="message">{label_message}</label>
|
||||
<textarea class="guestbook-textarea" id="message" name="message" placeholder="{label_message}" style="width:{tw}px;height:{th}px" required></textarea>
|
||||
{captcha_section}
|
||||
{drawing_section}{voice_note_section}<input name="url" aria-hidden="true" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)" tabindex="-1" autocomplete="off"><button class="guestbook-button" type="submit">{button}</button>
|
||||
</form>"#,
|
||||
<input class="guestbook-input" id="name" name="name" required>
|
||||
{website_section}<label class="guestbook-label" for="message">{label_message}</label>
|
||||
<textarea class="guestbook-textarea" id="message" name="message" style="width:{tw}px;height:{th}px"></textarea>
|
||||
{drawing_section}{voice_note_section}{captcha_section}<input name="url" aria-hidden="true" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)" tabindex="-1" autocomplete="off"><button class="guestbook-button" type="submit">{button}</button>
|
||||
</form>
|
||||
</details>"#,
|
||||
label_name = config.label_name,
|
||||
website_section = website_section,
|
||||
label_message = config.label_message,
|
||||
tw = config.textarea_width,
|
||||
th = config.textarea_height,
|
||||
captcha_section = captcha_section,
|
||||
drawing_section = drawing_section,
|
||||
voice_note_section = voice_note_section,
|
||||
captcha_section = captcha_section,
|
||||
button = config.button_text,
|
||||
)
|
||||
}
|
||||
|
|
@ -357,9 +350,9 @@ mod tests {
|
|||
style: String::new(),
|
||||
form_prompt: "Thanks for visiting. Sign the guestbook!".into(),
|
||||
button_text: "sign".into(),
|
||||
label_name: "Your name:".into(),
|
||||
label_website: "Your website (optional):".into(),
|
||||
label_message: "Your message:".into(),
|
||||
label_name: "name".into(),
|
||||
label_website: "website (optional)".into(),
|
||||
label_message: "message (optional)".into(),
|
||||
textarea_width: 400,
|
||||
textarea_height: 150,
|
||||
}
|
||||
|
|
@ -521,7 +514,7 @@ mod tests {
|
|||
let html = render_page(DEFAULT_TEMPLATE, &config, &[entry], &form);
|
||||
assert!(html.contains("<b>hacker</b>"));
|
||||
assert!(html.contains("<script>alert('xss')</script>"));
|
||||
assert!(!html.contains("<script>"));
|
||||
assert!(!html.contains("<script>alert("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -547,8 +540,8 @@ mod tests {
|
|||
let mut config = test_config();
|
||||
config.enable_drawings = true;
|
||||
let form = render_form(&config);
|
||||
assert!(form.contains("add a drawing"));
|
||||
assert!(form.contains("guestbook-drawing-toggle"));
|
||||
assert!(form.contains("guestbook-drawing-wrap"));
|
||||
assert!(form.contains("guestbook-drawing-content"));
|
||||
assert!(form.contains("name=\"drawing\""));
|
||||
}
|
||||
|
||||
|
|
@ -556,7 +549,7 @@ mod tests {
|
|||
fn test_render_form_hides_drawing_when_disabled() {
|
||||
let config = test_config();
|
||||
let form = render_form(&config);
|
||||
assert!(!form.contains("add a drawing"));
|
||||
assert!(!form.contains("guestbook-drawing-wrap"));
|
||||
assert!(!form.contains("name=\"drawing\""));
|
||||
}
|
||||
|
||||
|
|
@ -657,8 +650,8 @@ mod tests {
|
|||
let mut config = test_config();
|
||||
config.enable_voice_notes = true;
|
||||
let form = render_form(&config);
|
||||
assert!(form.contains("add a voice note"));
|
||||
assert!(form.contains("guestbook-voice-record"));
|
||||
assert!(form.contains("guestbook-voice-wrap"));
|
||||
assert!(form.contains("guestbook-voice-controls"));
|
||||
assert!(form.contains("name=\"voice_note\""));
|
||||
}
|
||||
|
||||
|
|
@ -666,7 +659,7 @@ mod tests {
|
|||
fn test_render_form_hides_voice_note_when_disabled() {
|
||||
let config = test_config();
|
||||
let form = render_form(&config);
|
||||
assert!(!form.contains("add a voice note"));
|
||||
assert!(!form.contains("guestbook-voice-wrap"));
|
||||
assert!(!form.contains("name=\"voice_note\""));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
65
src/web.rs
65
src/web.rs
|
|
@ -200,8 +200,8 @@ async fn submit(
|
|||
}
|
||||
}
|
||||
|
||||
if name.is_empty() || message.is_empty() {
|
||||
return Html(render_error_page(&state.config, "Name and message are required."));
|
||||
if name.is_empty() {
|
||||
return Html(render_error_page(&state.config, "Name is required."));
|
||||
}
|
||||
let max_name = state.config.max_name_length;
|
||||
if max_name > 0 && name.chars().count() > max_name {
|
||||
|
|
@ -277,6 +277,10 @@ async fn submit(
|
|||
None
|
||||
};
|
||||
|
||||
if message.is_empty() && drawing_bytes.is_none() && voice_note_bytes.is_none() {
|
||||
return Html(render_error_page(&state.config, "Please leave a message, drawing, or voice note."));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let date = now.format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||
|
||||
|
|
@ -412,9 +416,9 @@ mod tests {
|
|||
style: String::new(),
|
||||
form_prompt: "Thanks for visiting. Sign the guestbook!".into(),
|
||||
button_text: "sign".into(),
|
||||
label_name: "Your name:".into(),
|
||||
label_website: "Your website (optional):".into(),
|
||||
label_message: "Your message:".into(),
|
||||
label_name: "name".into(),
|
||||
label_website: "website (optional)".into(),
|
||||
label_message: "message (optional)".into(),
|
||||
textarea_width: 400,
|
||||
textarea_height: 150,
|
||||
}
|
||||
|
|
@ -962,10 +966,59 @@ mod tests {
|
|||
let (app, _rx) = test_app(config);
|
||||
let (_, body) = post_form(&app, "name=&message=").await;
|
||||
assert!(body.contains("<!DOCTYPE html>"));
|
||||
assert!(body.contains("Name and message are required"));
|
||||
assert!(body.contains("Name is required"));
|
||||
assert!(body.contains("back"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_rejects_when_message_drawing_voicenote_all_empty() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = true;
|
||||
config.enable_voice_notes = true;
|
||||
let (app, _rx) = test_app(config);
|
||||
let (_, body) = post_form(&app, "name=alice&message=").await;
|
||||
assert!(body.contains("Please leave a message"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_accepts_drawing_only() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = true;
|
||||
config.canvas_width = 400;
|
||||
config.canvas_height = 200;
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let png = fake_png(400, 200);
|
||||
let drawing_data = base64::engine::general_purpose::STANDARD.encode(&png);
|
||||
let data_url = format!("data:image/png;base64,{drawing_data}");
|
||||
let body = format!(
|
||||
"name=alice&message=&drawing={}",
|
||||
urlencoding::encode(&data_url)
|
||||
);
|
||||
let (_, resp) = post_form(&app, &body).await;
|
||||
assert!(resp.contains("pending approval"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_accepts_voice_note_only() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_voice_notes = true;
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let webm = fake_webm();
|
||||
let voice_data = base64::engine::general_purpose::STANDARD.encode(&webm);
|
||||
let data_url = format!("data:audio/webm;codecs=opus;base64,{voice_data}");
|
||||
let body = format!(
|
||||
"name=alice&message=&voice_note={}",
|
||||
urlencoding::encode(&data_url)
|
||||
);
|
||||
let (_, resp) = post_form(&app, &body).await;
|
||||
assert!(resp.contains("pending approval"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_with_voice_note() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -16,25 +16,17 @@ body {
|
|||
}
|
||||
.guestbook-form {}
|
||||
.guestbook-label {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
.guestbook-input {
|
||||
display: block;
|
||||
margin-bottom: 0.2em;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.guestbook-textarea {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
margin-bottom: 0.2em;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.guestbook-button {
|
||||
display: block;
|
||||
|
|
@ -48,21 +40,16 @@ body {
|
|||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.guestbook-canvas-tools {
|
||||
display: block;
|
||||
}
|
||||
.guestbook-canvas-tools a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.guestbook-drawing-wrap {
|
||||
display: block;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.guestbook-drawing-inline a {
|
||||
.guestbook-drawing-tools {
|
||||
display: block;
|
||||
}
|
||||
.guestbook-drawing-tools a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.guestbook-drawing-content:empty {
|
||||
display: none;
|
||||
}
|
||||
.guestbook-drawing-content {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -91,6 +78,10 @@ body {
|
|||
/* Voice notes */
|
||||
.guestbook-voice-wrap {
|
||||
display: block;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.guestbook-voice-controls a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.guestbook-voice-record.recording {
|
||||
color: red;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue