refactor(templates/render): Add generic post editing template

Adds a generic template that can be used for submitting, responding to
and editing posts.
This commit is contained in:
Vincent Ambo 2018-04-15 21:13:20 +02:00
parent 4c0e6552e8
commit ec712cc4c0
6 changed files with 156 additions and 72 deletions

View file

@ -80,7 +80,7 @@ impl Handler<GetThread> for DbExecutor {
/// Message used to create a new thread /// Message used to create a new thread
pub struct CreateThread { pub struct CreateThread {
pub new_thread: NewThread, pub new_thread: NewThread,
pub body: String, pub post: String,
} }
impl Message for CreateThread { impl Message for CreateThread {
@ -105,7 +105,7 @@ impl Handler<CreateThread> for DbExecutor {
// ... then create the first post in the thread. // ... then create the first post in the thread.
let new_post = NewPost { let new_post = NewPost {
thread_id: thread.id, thread_id: thread.id,
body: msg.body, body: msg.post,
author_name: msg.new_thread.author_name.clone(), author_name: msg.new_thread.author_name.clone(),
author_email: msg.new_thread.author_email.clone(), author_email: msg.new_thread.author_email.clone(),
}; };

View file

@ -99,7 +99,7 @@ fn anonymous() -> Author {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct NewThreadForm { pub struct NewThreadForm {
pub title: String, pub title: String,
pub body: String, pub post: String,
} }
const NEW_THREAD_LENGTH_ERR: &'static str = "Title and body can not be empty!"; const NEW_THREAD_LENGTH_ERR: &'static str = "Title and body can not be empty!";
@ -112,16 +112,16 @@ pub fn submit_thread(state: State<AppState>,
// Trim whitespace out of inputs: // Trim whitespace out of inputs:
let input = NewThreadForm { let input = NewThreadForm {
title: input.title.trim().into(), title: input.title.trim().into(),
body: input.body.trim().into(), post: input.post.trim().into(),
}; };
// Perform simple validation and abort here if it fails: // Perform simple validation and abort here if it fails:
if input.title.is_empty() || input.body.is_empty() { if input.title.is_empty() || input.post.is_empty() {
return state.renderer return state.renderer
.send(NewThreadPage { .send(NewThreadPage {
alerts: vec![NEW_THREAD_LENGTH_ERR], alerts: vec![NEW_THREAD_LENGTH_ERR],
title: Some(input.title), title: Some(input.title),
body: Some(input.body), post: Some(input.post),
}) })
.flatten() .flatten()
.map(|res| HttpResponse::Ok().content_type(HTML).body(res)) .map(|res| HttpResponse::Ok().content_type(HTML).body(res))
@ -140,7 +140,7 @@ pub fn submit_thread(state: State<AppState>,
let msg = CreateThread { let msg = CreateThread {
new_thread, new_thread,
body: input.body, post: input.post,
}; };
state.db.send(msg) state.db.send(msg)
@ -158,7 +158,7 @@ pub fn submit_thread(state: State<AppState>,
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct NewPostForm { pub struct NewPostForm {
pub thread_id: i32, pub thread_id: i32,
pub body: String, pub post: String,
} }
/// This handler receives a "Reply"-form and redirects the user to the /// This handler receives a "Reply"-form and redirects the user to the
@ -172,7 +172,7 @@ pub fn reply_thread(state: State<AppState>,
let new_post = NewPost { let new_post = NewPost {
thread_id: input.thread_id, thread_id: input.thread_id,
body: input.body.trim().into(), body: input.post.trim().into(),
author_name: author.name, author_name: author.name,
author_email: author.email, author_email: author.email,
}; };

View file

@ -159,6 +159,41 @@ impl Handler<ThreadPage> for Renderer {
} }
} }
/// The different types of editing modes supported by the editing
/// template:
#[derive(Debug, Serialize)]
pub enum EditingMode {
NewThread,
PostReply,
EditPost,
}
impl Default for EditingMode {
fn default() -> EditingMode { EditingMode::NewThread }
}
/// This struct represents the context submitted to the template used
/// for rendering the new thread, edit post and reply to thread forms.
#[derive(Default, Serialize)]
pub struct FormContext {
/// Which editing mode is to be used by the template?
pub mode: EditingMode,
/// Potential alerts to display to the user (e.g. input validation
/// results)
pub alerts: Vec<&'static str>,
/// Either the title to be used in the subject field or the title
/// of the thread the user is responding to.
pub title: Option<String>,
/// Body of the post being edited, if present.
pub post: Option<String>,
/// ID of the thread being replied to or the post being edited.
pub id: Option<i32>,
}
/// Message used to render new thread page. /// Message used to render new thread page.
/// ///
/// It can optionally contain a vector of warnings to display to the /// It can optionally contain a vector of warnings to display to the
@ -167,7 +202,7 @@ impl Handler<ThreadPage> for Renderer {
pub struct NewThreadPage { pub struct NewThreadPage {
pub alerts: Vec<&'static str>, pub alerts: Vec<&'static str>,
pub title: Option<String>, pub title: Option<String>,
pub body: Option<String>, pub post: Option<String>,
} }
impl Message for NewThreadPage { impl Message for NewThreadPage {
@ -178,11 +213,13 @@ impl Handler<NewThreadPage> for Renderer {
type Result = Result<String>; type Result = Result<String>;
fn handle(&mut self, msg: NewThreadPage, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: NewThreadPage, _: &mut Self::Context) -> Self::Result {
let mut ctx = Context::new(); let ctx: FormContext = FormContext {
ctx.add("alerts", &msg.alerts); alerts: msg.alerts,
ctx.add("title", &msg.title.map(|s| escape_html(&s))); title: msg.title,
ctx.add("body", &msg.body.map(|s| escape_html(&s))); post: msg.post,
Ok(self.tera.render("new-thread.html", &ctx)?) ..Default::default()
};
Ok(self.tera.render("post.html", &ctx)?)
} }
} }

View file

@ -1,56 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<title>Converse Index</title>
</head>
<body>
<header>
<nav class="navbar navbar-light bg-light justify-content-between mb-3">
<a class="navbar-brand" href="/">
<h2>Converse: New Thread</h2>
</a>
<form class="form-inline">
<a class="btn btn-outline-secondary my-2" href="/">Back to index</a>
</form>
</nav>
</header>
<div class="container border rounded">
<div class="col-8">
{% for alert in alerts %}
<div class="alert alert-warning m-3"><strong>{{ alert }}</strong></div>
{% endfor %}
<p class="mt-3">Make <i>your own thread</i> on these here forums!</p>
<p>Remember that you can use <strong>Markdown</strong> when
writing your posts.</p>
<form action="/thread/submit" method="post">
<div class="row">
<div class="col-8 input-group m-3">
<div class="input-group-prepend">
<span class="input-group-text" id="title-text">Title:</span>
</div>
<input type="text" class="form-control" id="title" name="title" aria-describedby="title-text" {% if title %}value="{{ title }}"{% endif %}>
</div>
</div>
<div class="row">
<div class="col-8 input-group m-3">
<div class="input-group-prepend">
<span class="input-group-text" id="body-text">Body:</span>
</div>
<textarea class="form-control" id="body" name="body" aria-label="thread body">{%if body %}{{ body }}{% endif %}</textarea>
</div>
</div>
<div class="row">
<div class="col-2 m-3">
<button class="btn btn-outline-primary" type="submit">Post!</button>
</div>
</div>
</form>
</div>
</div>
</body>
</html>

103
templates/post.html Normal file
View file

@ -0,0 +1,103 @@
<!-- {#
This template is shared by the new thread, reply and post-editing pages.
The main display differences between the different editing styles are the
headline of the page ("Submit new thread", "Reply to thread", "Edit post")
and whether or not the subject line field is displayed in the input form.
Every one of these pages can have a variable length list of alerts submitted
into the template, which will be rendered as Boostrap alert boxes above the
user input form.
#} -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<title>Converse Index</title>
</head>
<body>
<header>
<nav class="navbar navbar-light bg-light justify-content-between mb-3">
<a class="navbar-brand" href="/">
{% if mode == "NewThread" %}
<h2>Converse: Submit new thread</h2>
{% elif mode == "PostReply" %}
<h2>Converse: Reply to thread</h2>
{% elif mode == "EditPost" %}
<h2>Converse: Edit post</h2>
{% endif %}
</a>
<form class="form-inline">
<a class="btn btn-outline-secondary my-2" href="/">Back to index</a>
</form>
</nav>
</header>
<div class="container border rounded">
<div class="d-flex flex-column mt-3 border-bottom">
{%- for alert in alerts %}
<div class="alert alert-warning m-3"><strong>{{ alert }}</strong></div>
{% endfor -%}
{%- if mode == "NewThread" %}
<h5>Create a new thread</h5>
{% elif mode == "PostReply" %}
<h5>Respond to thread '{{ title }}'</h5>
{% elif mode == "EditPost" %}
<h5>Edit your post</h5>
{% endif -%}
</div>
<div class="d-flex flex-column mt-3">
{% if mode == "NewThread" %}
<form action="/thread/submit" method="post">
{% elif mode == "PostReply" %}
<form action="/thread/reply" method="post">
{% elif mode == "EditPost" %}
<form action="/post/edit" method="post">
{% endif %}
{% if mode == "PostReply" %}
<input type="hidden" id="thread_id" name="thread_id" value="{{ id }}">
{% elif mode == "EditPost" %}
<input type="hidden" id="thread_id" name="post_id" value="{{ id }}">
{% endif %}
{% if mode == "NewThread" %}
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="title-text">Title:</span>
</div>
<input type="text" class="form-control" id="title" name="title" aria-label="thread title" {% if title %}value="{{ title }}"{% endif %}>
</div>
{% endif %}
<div class="d-flex flex-row">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="post-text">Post:</span>
</div>
<textarea class="form-control" id="post" name="post" rows="15" aria-label="post content">{% if body %}{{ body }}{% endif %}</textarea>
</div>
<div class="d-flex flex-column flex-wrap-reverse border rounded">
<p class="m-2 pb-2 border-bottom border-dark">
Remember that you can use <a href="https://daringfireball.net/projects/markdown/basics"><strong>Markdown</strong></a> when
writing your posts:
</p>
<p class="ml-4 m-2"><i>*italic text*</i></p>
<p class="ml-4 m-2"><strong>**bold text**</strong></p>
<p class="ml-4 m-2"><s>~strikethrough text~</s></p>
<p class="ml-4 m-2"><code>[link text](https://some.link.com/)</code></p>
<p class="ml-4 m-2"><code>![image text](https://foo.com/thing.jpg)</code></p>
<p class="ml-4 m-2">Use <code>*</code> or <code>-</code> to enumerate lists.</p>
<p class="ml-4 m-2">See Markdown documentation for more information!</p>
</div>
</div>
<button class="btn btn-primary mt-3 mb-3" type="submit">Post!</button>
</form>
</div>
</div>
</body>
</html>

View file

@ -55,7 +55,7 @@
<input type="hidden" id="thread_id" name="thread_id" value="{{ id }}"> <input type="hidden" id="thread_id" name="thread_id" value="{{ id }}">
<label for="body">You can use <strong>Markdown</strong>!</label> <label for="body">You can use <strong>Markdown</strong>!</label>
<div class="input-group"> <div class="input-group">
<textarea class="form-control" id="body" name="body" aria-label="thread response" rows="10"></textarea> <textarea class="form-control" id="post" name="post" aria-label="thread response" rows="10"></textarea>
<div class="input-group-append"> <div class="input-group-append">
<button class="btn btn-primary" type="submit">Post!</button> <button class="btn btn-primary" type="submit">Post!</button>
</div> </div>