forked from Plume/Plume
Use Ructe (#327)
All the template are now compiled at compile-time with the `ructe` crate. I preferred to use it instead of askama because it allows more complex Rust expressions, where askama only supports a small subset of expressions and doesn't allow them everywhere (for instance, `{{ macro!() | filter }}` would result in a parsing error). The diff is quite huge, but there is normally no changes in functionality. Fixes #161 and unblocks #110 and #273fix-mobile-margin
parent
5f059c3e98
commit
70af57c6e1
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@
|
||||
extern crate ructe;
|
||||
extern crate rocket_i18n;
|
||||
use ructe::*;
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("templates");
|
||||
compile_templates(&in_dir, &out_dir).expect("compile templates");
|
||||
|
||||
println!("cargo:rerun-if-changed=po");
|
||||
rocket_i18n::update_po("plume", &["de", "en", "fr", "gl", "it", "ja", "nb", "pl", "ru"]);
|
||||
rocket_i18n::compile_po("plume", &["de", "en", "fr", "gl", "it", "ja", "nb", "pl", "ru"]);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
en
|
||||
fr
|
||||
pl
|
||||
de
|
||||
nb
|
||||
gl
|
||||
it
|
||||
ru
|
||||
ja
|
@ -1 +1 @@
|
||||
nightly-2018-07-17
|
||||
nightly-2018-10-06
|
||||
|
@ -1,40 +1,36 @@
|
||||
use rocket_contrib::Template;
|
||||
use rocket::Request;
|
||||
use rocket::request::FromRequest;
|
||||
use rocket_i18n::I18n;
|
||||
use plume_models::db_conn::DbConn;
|
||||
use plume_models::users::User;
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[catch(404)]
|
||||
fn not_found(req: &Request) -> Template {
|
||||
pub fn not_found(req: &Request) -> Ructe {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
Template::render("errors/404", json!({
|
||||
"error_message": "Page not found",
|
||||
"account": user.and_then(|u| conn.map(|conn| u.to_json(&*conn)))
|
||||
}))
|
||||
render!(errors::not_found(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
))
|
||||
}
|
||||
|
||||
#[catch(500)]
|
||||
fn server_error(req: &Request) -> Template {
|
||||
pub fn server_error(req: &Request) -> Ructe {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
Template::render("errors/500", json!({
|
||||
"error_message": "Server error",
|
||||
"account": user.and_then(|u| conn.map(|conn| u.to_json(&*conn)))
|
||||
}))
|
||||
render!(errors::server_error(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct Uri {
|
||||
target: String,
|
||||
}
|
||||
|
||||
#[post("/csrf-violation?<uri>")]
|
||||
fn csrf_violation(uri: Option<Uri>) -> Template {
|
||||
if let Some(uri) = uri {
|
||||
eprintln!("Csrf violation while acceding \"{}\"", uri.target)
|
||||
#[post("/csrf-violation?<target>")]
|
||||
pub fn csrf_violation(target: Option<String>, conn: DbConn, intl: I18n, user: Option<User>) -> Ructe {
|
||||
if let Some(uri) = target {
|
||||
eprintln!("Csrf violation while acceding \"{}\"", uri)
|
||||
}
|
||||
Template::render("errors/csrf", json!({
|
||||
"error_message":""
|
||||
}))
|
||||
render!(errors::csrf(
|
||||
&(&*conn, &intl.catalog, user)
|
||||
))
|
||||
}
|
||||
|
@ -1,29 +1,30 @@
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket_contrib::Template;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::utils;
|
||||
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
|
||||
use routes::Page;
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[get("/notifications?<page>")]
|
||||
fn paginated_notifications(conn: DbConn, user: User, page: Page) -> Template {
|
||||
Template::render("notifications/index", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"notifications": Notification::page_for_user(&*conn, &user, page.limits()).into_iter().map(|n| n.to_json(&*conn)).collect::<Vec<_>>(),
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(Notification::find_for_user(&*conn, &user).len() as i32)
|
||||
}))
|
||||
pub fn paginated_notifications(conn: DbConn, user: User, page: Page, intl: I18n) -> Ructe {
|
||||
render!(notifications::index(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
Notification::page_for_user(&*conn, &user, page.limits()),
|
||||
page.0,
|
||||
Page::total(Notification::find_for_user(&*conn, &user).len() as i32)
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/notifications")]
|
||||
fn notifications(conn: DbConn, user: User) -> Template {
|
||||
paginated_notifications(conn, user, Page::first())
|
||||
pub fn notifications(conn: DbConn, user: User, intl: I18n) -> Ructe {
|
||||
paginated_notifications(conn, user, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/notifications", rank = 2)]
|
||||
fn notifications_auth() -> Flash<Redirect>{
|
||||
pub fn notifications_auth(i18n: I18n) -> Flash<Redirect>{
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to see your notifications",
|
||||
i18n!(i18n.catalog, "You need to be logged in order to see your notifications"),
|
||||
uri!(notifications)
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,160 @@
|
||||
use plume_models::{Connection, users::User};
|
||||
use rocket::response::Content;
|
||||
use rocket_i18n::Catalog;
|
||||
use templates::Html;
|
||||
|
||||
pub use askama_escape::escape;
|
||||
|
||||
pub type BaseContext<'a> = &'a(&'a Connection, &'a Catalog, Option<User>);
|
||||
|
||||
pub type Ructe = Content<Vec<u8>>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! render {
|
||||
($group:tt :: $page:tt ( $( $param:expr ),* ) ) => {
|
||||
{
|
||||
use rocket::{http::ContentType, response::Content};
|
||||
use templates;
|
||||
|
||||
let mut res = vec![];
|
||||
templates::$group::$page(
|
||||
&mut res,
|
||||
$(
|
||||
$param
|
||||
),*
|
||||
).unwrap();
|
||||
Content(ContentType::HTML, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Size {
|
||||
Small,
|
||||
Medium,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Size::Small => "small",
|
||||
Size::Medium => "medium",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn avatar(conn: &Connection, user: &User, size: Size, pad: bool, catalog: &Catalog) -> Html<String> {
|
||||
let name = escape(&user.name(conn)).to_string();
|
||||
Html(format!(
|
||||
r#"<div
|
||||
class="avatar {size} {padded}"
|
||||
style="background-image: url('{url}');"
|
||||
title="{title}"
|
||||
aria-label="{title}"
|
||||
></div>"#,
|
||||
size = size.as_str(),
|
||||
padded = if pad { "padded" } else { "" },
|
||||
url = user.avatar_url(conn),
|
||||
title = i18n!(catalog, "{0}'s avatar"; name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn tabs(links: &[(&str, &str, bool)]) -> Html<String> {
|
||||
let mut res = String::from(r#"<div class="tabs">"#);
|
||||
for (url, title, selected) in links {
|
||||
res.push_str(r#"<a href=""#);
|
||||
res.push_str(url);
|
||||
if *selected {
|
||||
res.push_str(r#"" class="selected">"#);
|
||||
} else {
|
||||
res.push_str("\">");
|
||||
}
|
||||
res.push_str(title);
|
||||
res.push_str("</a>");
|
||||
}
|
||||
res.push_str("</div>");
|
||||
Html(res)
|
||||
}
|
||||
|
||||
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
|
||||
let mut res = String::new();
|
||||
res.push_str(r#"<div class="pagination">"#);
|
||||
if page != 1 {
|
||||
res.push_str(format!(r#"<a href="?page={}">{}</a>"#, page - 1, catalog.gettext("Previous page")).as_str());
|
||||
}
|
||||
if page < total {
|
||||
res.push_str(format!(r#"<a href="?page={}">{}</a>"#, page + 1, catalog.gettext("Next page")).as_str());
|
||||
}
|
||||
res.push_str("</div>");
|
||||
Html(res)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! icon {
|
||||
($name:expr) => {
|
||||
Html(concat!(r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#, $name, "\"/></svg>"))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! input {
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
{
|
||||
use validator::ValidationErrorsKind;
|
||||
use std::borrow::Cow;
|
||||
|
||||
Html(format!(r#"
|
||||
<label for="{name}">
|
||||
{label}
|
||||
{optional}
|
||||
{details}
|
||||
</label>
|
||||
{error}
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!($catalog, $label),
|
||||
kind = stringify!($kind),
|
||||
optional = if $optional { format!("<small>{}</small>", i18n!($catalog, "Optional")) } else { String::new() },
|
||||
details = if $details.len() > 0 {
|
||||
format!("<small>{}</small>", i18n!($catalog, $details))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
error = if let Some(ValidationErrorsKind::Field(errs)) = $err.errors().get(stringify!($name)) {
|
||||
format!(r#"<p class="error">{}</p>"#, i18n!($catalog, &*errs[0].message.clone().unwrap_or(Cow::from("Unknown error"))))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
val = escape(&$form.$name),
|
||||
props = $props
|
||||
))
|
||||
}
|
||||
};
|
||||
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!($catalog, $name ($kind), $label, true, $details, $form, $err, $props)
|
||||
};
|
||||
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!($catalog, $name ($kind), $label, true, "", $form, $err, $props)
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!($catalog, $name ($kind), $label, false, $details, $form, $err, $props)
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!($catalog, $name ($kind), $label, false, "", $form, $err, $props)
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
|
||||
input!($catalog, $name ($kind), $label, false, "", $form, $err, "")
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {
|
||||
{
|
||||
Html(format!(r#"
|
||||
<label for="{name}">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!($catalog, $label),
|
||||
kind = stringify!($kind),
|
||||
props = $props
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
@ -1,77 +1,75 @@
|
||||
{% import "macros" as macros %}
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext, title: &str, head: Content, header: Content, content: Content)
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{% block title %}{% endblock title %} ⋅ {{ "Plume" | _ }}</title>
|
||||
<title>@i18n!(ctx.1, title) ⋅ @i18n!(ctx.1, "Plume")</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="/static/css/main.css" />
|
||||
<link rel="stylesheet" href="/static/css/feather.css" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" type="image/png" href="/static/icons/trwnh/feather-filled/plumeFeatherFilled64.png">
|
||||
{% block head %}{% endblock head %}
|
||||
@:head()
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav id="menu">
|
||||
<a href="#" aria-label="{{ "Menu" | _ }}" title="{{ "Menu" | _ }}"><i class="icon icon-menu"></i></a>
|
||||
<a href="#" aria-label="@i18n!(ctx.1, "Menu")" title="@i18n!(ctx.1, "Menu")"><i class="icon icon-menu"></i></a>
|
||||
</nav>
|
||||
<div id="content">
|
||||
<nav>
|
||||
<a href="/" class="title">
|
||||
<img src="/static/icons/trwnh/feather/plumeFeather256.png">
|
||||
<p>{{ "Plume" | _ }}</p>
|
||||
<p>@i18n!(ctx.1, "Plume")</p>
|
||||
</a>
|
||||
<hr/>
|
||||
{% block header %}
|
||||
{% endblock header %}
|
||||
@:header()
|
||||
</nav>
|
||||
<nav>
|
||||
{% if account %}
|
||||
@if ctx.2.is_some() {
|
||||
<a href="/dashboard">
|
||||
<i class="icon icon-home" aria-label="{{ "Dashboard" | _ }}"></i>
|
||||
<span class="mobile-label">{{ "Dashboard" | _ }}</span>
|
||||
<i class="icon icon-home" aria-label="@i18n!(ctx.1, "Dashboard")"></i>
|
||||
<span class="mobile-label">@i18n!(ctx.1, "Dashboard")</span>
|
||||
</a>
|
||||
<a href="/notifications">
|
||||
<i class="icon icon-bell" aria-label="{{ "Notifications" | _ }}"></i>
|
||||
<span class="mobile-label">{{ "Notifications" | _ }}</span>
|
||||
<i class="icon icon-bell" aria-label="@i18n!(ctx.1, "Notifications")"></i>
|
||||
<span class="mobile-label">@i18n!(ctx.1, "Notifications")</span>
|
||||
</a>
|
||||
<a href="/logout">
|
||||
<i class="icon icon-log-out" aria-label="{{ "Log Out" | _ }}"></i>
|
||||
<span class="mobile-label">{{ "Log Out" | _ }}</span>
|
||||
<i class="icon icon-log-out" aria-label="@i18n!(ctx.1, "Log Out")"></i>
|
||||
<span class="mobile-label">@i18n!(ctx.1, "Log Out")</span>
|
||||
</a>
|
||||
<a href="/me" title="{{ "My account" | _ }}">
|
||||
{{ macros::avatar(user=account) }}
|
||||
<span class="mobile-label">{{ "My account" | _ }}</span>
|
||||
<a href="/me" title="@i18n!(ctx.1, "My account")">
|
||||
@avatar(ctx.0, &ctx.2.clone().unwrap(), Size::Small, false, &ctx.1)
|
||||
<span class="mobile-label">@i18n!(ctx.1, "My account")</span>
|
||||
</a>
|
||||
{% else %}
|
||||
} else {
|
||||
<a href="/login">
|
||||
<i class="icon icon-log-in"></i>
|
||||
<span class="mobile-label">{{ "Log In" | _ }}</span>
|
||||
<span class="mobile-label">@i18n!(ctx.1, "Log In")</span>
|
||||
</a>
|
||||
<a href="/users/new">
|
||||
<i class="icon icon-user-plus"></i>
|
||||
<span class="mobile-label">{{ "Register" | _ }}</span>
|
||||
<span class="mobile-label">@i18n!(ctx.1, "Register")</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
@:content()
|
||||
</main>
|
||||
<footer>
|
||||
<span>Plume 0.2.0</span>
|
||||
<a href="/about">{{ "About this instance" | _ }}</a>
|
||||
<a href="https://github.com/Plume-org/Plume">{{ "Source code" | _ }}</a>
|
||||
<a href="https://riot.im/app/#/room/#plume:disroot.org">{{ "Matrix room" | _ }}</a>
|
||||
{% if account %}
|
||||
{% if account.is_admin %}
|
||||
<a href="/admin">{{ "Administration" | _ }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<span>@concat!("Plume ", env!("CARGO_PKG_VERSION"))</span>
|
||||
<a href="/about">@i18n!(ctx.1, "About this instance")</a>
|
||||
<a href="https://github.com/Plume-org/Plume">@i18n!(ctx.1, "Source code")</a>
|
||||
<a href="https://riot.im/app/#/room/#plume:disroot.org">@i18n!(ctx.1, "Matrix room")</a>
|
||||
@if ctx.2.clone().map(|a| a.is_admin).unwrap_or(false) {
|
||||
<a href="/admin">@i18n!(ctx.1, "Administration")</a>
|
||||
}
|
||||
</footer>
|
||||
<script src="/static/js/menu.js"></script>
|
||||
</body>
|
@ -1,50 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ blog.title }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block header %}
|
||||
<a href="/~/{{ blog.fqn }}">{{ blog.title }}</a>
|
||||
{% endblock header %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ blog.title }} <small>~{{ blog.fqn }}</small></h1>
|
||||
<p>{{ blog.summary }}</p>
|
||||
<p>
|
||||
{{ "{{ count }} authors in this blog: " | _n(singular="One author in this blog: ", count = n_authors) }}
|
||||
{% for author in authors %}
|
||||
<a class="author" href="/@/{{ author.fqn }}">{{ author.name }}</a>{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ "{{ count }} articles in this blog" | _n(singular="One article in this blog", count = n_articles) }}
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
{{ "Latest articles" | _ }}
|
||||
<small><a href="/~/{{ blog.fqn }}/atom.xml" title="Atom feed">{{ macros::feather(name="rss") }}</a></small>
|
||||
</h2>
|
||||
{% if posts | length < 1 %}
|
||||
<p>{{ "No posts to see here yet." | _ }}</p>
|
||||
{% endif %}
|
||||
{% if is_author %}
|
||||
<a href="/~/{{ blog.fqn }}/new/" class="button inline-block">{{ "New article" | _ }}</a>
|
||||
{% endif %}
|
||||
<div class="cards">
|
||||
{% for article in posts %}
|
||||
{{ macros::post_card(article=article) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ macros::paginate(page=page, total=n_pages) }}
|
||||
</section>
|
||||
{% if is_author %}
|
||||
<h2>{{ "Danger zone" | _ }}</h2>
|
||||
<p>{{ "Be very careful, any action taken here can't be cancelled." | _ }}
|
||||
<form method="post" action="/~/{{ blog.fqn }}/delete">
|
||||
<input type="submit" class="inline-block button destructive" value="{{ "Delete this blog" | _ }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
@ -0,0 +1,49 @@
|
||||
@use plume_models::blogs::Blog;
|
||||
@use plume_models::posts::Post;
|
||||
@use plume_models::users::User;
|
||||
@use templates::{base, partials::post_card};
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext, blog: Blog, fqn: String, authors: &Vec<User>, total_articles: usize, page: i32, n_pages: i32, is_author: bool, posts: Vec<Post>)
|
||||
|
||||
@:base(ctx, blog.title.as_ref(), {}, {
|
||||
<a href="/~/@fqn">@blog.title</a>
|
||||
}, {
|
||||
<h1>@blog.title <small>~@fqn</small></h1>
|
||||
<p>@blog.summary</p>
|
||||
<p>
|
||||
@i18n!(ctx.1, "One author in this blog: ", "{0} authors in this blog: ", authors.len())
|
||||
@for author in authors {
|
||||
<a class="author" href="/@@/@author.get_fqn(ctx.0)">@author.name(ctx.0)</a>
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
@i18n!(ctx.1, "One article in this blog", "{0} articles in this blog", total_articles)
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
@i18n!(ctx.1, "Latest articles")
|
||||
<small><a href="/~/@fqn/atom.xml" title="Atom feed">@icon!("rss")</a></small>
|
||||
</h2>
|
||||
@if posts.len() < 1 {
|
||||
<p>@i18n!(ctx.1, "No posts to see here yet.")</p>
|
||||
}
|
||||
@if is_author {
|
||||
<a href="/~/@fqn/new/" class="button inline-block">@i18n!(ctx.1, "New article")</a>
|
||||
}
|
||||
<div class="cards">
|
||||
@for article in posts {
|
||||
@:post_card(ctx, article)
|
||||
}
|
||||
</div>
|
||||
@paginate(ctx.1, page, n_pages)
|
||||
</section>
|
||||
@if is_author {
|
||||
<h2>@i18n!(ctx.1, "Danger zone")</h2>
|
||||
<p>@i18n!(ctx.1, "Be very careful, any action taken here can't be cancelled.")</p>
|
||||
<form method="post" action="/~/@fqn/delete">
|
||||
<input type="submit" class="inline-block button destructive" value="@i18n!(ctx.1, "Delete this blog")">
|
||||
</form>
|
||||
}
|
||||
})
|
@ -1,15 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "New blog" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Create a blog" | _ }}</h1>
|
||||
<form method="post">
|
||||
{{ macros::input(name="title", label="Title", errors=errors, form=form, props='required minlength="1"') }}
|
||||
|
||||
<input type="submit" value="{{ "Create blog" | _ }}"/>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,14 @@
|
||||
@use validator::ValidationErrors;
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use routes::blogs::NewBlogForm;
|
||||
|
||||
@(ctx: BaseContext, form: &NewBlogForm, errors: ValidationErrors)
|
||||
|
||||
@:base(ctx, "New Blog", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Create a blog")</h1>
|
||||
<form method="post">
|
||||
@input!(ctx.1, title (text), "Title", form, errors, "required minlength=\"1\"")
|
||||
<input type="submit" value="@i18n!(ctx.1, "Create blog")"/>
|
||||
</form>
|
||||
})
|
@ -1,5 +0,0 @@
|
||||
{% extends "errors/base" %}
|
||||
|
||||
{% block error %}
|
||||
<h1>{{ "You are not authorized." | _ }}</h1>
|
||||
{% endblock error %}
|
@ -1,6 +0,0 @@
|
||||
{% extends "errors/base" %}
|
||||
|
||||
{% block error %}
|
||||
<h1>{{ "We couldn't find this page." | _ }}</h1>
|
||||
<h2>{{ "The link that led you here may be broken." | _ }}</h2>
|
||||
{% endblock error %}
|
@ -1,6 +0,0 @@
|
||||
{% extends "errors/base" %}
|
||||
|
||||
{% block error %}
|
||||
<h1>{{ "Something broke on our side." | _ }}</h1>
|
||||
<p>{{ "Sorry about that. If you think this is a bug, please report it." | _ }}</p>
|
||||
{% endblock error %}
|
@ -1,15 +0,0 @@
|
||||
{% extends "base" %}
|
||||
|
||||
{% block title %}
|
||||
{{ error_message }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<main class="error">
|
||||
{% block error %}
|
||||
{% endblock error %}
|
||||
<p>
|
||||
{{ error_message }}
|
||||
</p>
|
||||
</main>
|
||||
{% endblock content %}
|
@ -0,0 +1,9 @@
|
||||
@use templates::base as base_template;
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext, error_message: &str, error: Content)
|
||||
|
||||
@:base_template(ctx, error_message, {}, {}, {
|
||||
@:error()
|
||||
<p>@error_message</p>
|
||||
})
|
@ -1,6 +0,0 @@
|
||||
{% extends "errors/base" %}
|
||||
|
||||
{% block error %}
|
||||
<h1>{{ "Invalid CSRF token." | _ }}</h1>
|
||||
<p>{{ "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." | _ }}</p>
|
||||
{% endblock error %}
|
@ -0,0 +1,12 @@
|
||||
@use templates::errors::base;
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext)
|
||||
|
||||
@:base(ctx, "", {
|
||||
<h1>@i18n!(ctx.1, "Invalid CSRF token.")</h1>
|
||||
<p>@i18n!(ctx.1, r#"Something is wrong with your CSRF token.
|
||||
Make sure cookies are enabled in you browser, and try reloading this page.
|
||||
If you continue to see this error message, please report it."#)
|
||||
</p>
|
||||
})
|
@ -0,0 +1,8 @@
|
||||
@use templates::errors::base;
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext, error_message: &str)
|
||||
|
||||
@:base(ctx, error_message, {
|
||||
<h1>@i18n!(ctx.1, "You are not authorized.")</h1>
|
||||
})
|
@ -0,0 +1,10 @@
|
||||
@use templates::errors::base;
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext)
|
||||
|
||||
@:base(ctx, "Page not found", {
|
||||
<h1>@i18n!(ctx.1, "We couldn't find this page.")</h1>
|
||||
<p>@i18n!(ctx.1, "The link that led you here may be broken.")</p>
|
||||
})
|
||||
|
@ -0,0 +1,9 @@
|
||||
@use templates::errors::base;
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext)
|
||||
|
||||
@:base(ctx, "Internal server error", {
|
||||
<h1>@i18n!(ctx.1, "Something broke on our side.")</h1>
|
||||
<p>@i18n!(ctx.1, "Sorry about that. If you think this is a bug, please report it.")</p>
|
||||
})
|
@ -1,41 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
About {{ instance.name }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "About {{ instance_name }}" | _(instance_name=instance.name) }}</h1>
|
||||
<section>
|
||||
{{ instance.short_description_html | safe }}
|
||||
</section>
|
||||
<div class="banner">
|
||||
<section class="stats">
|
||||
<div>
|
||||
<p>{{ "Home to" | _ }}</p>
|
||||
<em>{{ n_users }}</em>
|
||||
<p>{{ "people" | _ }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ "Who wrote" | _ }}</p>
|
||||
<em>{{ n_articles }}</em>
|
||||
<p>{{ "articles" | _ }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ "And connected to" | _ }}</p>
|
||||
<em>{{ n_instances }}</em>
|
||||
<p>{{ "other instances" | _ }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ "Administred by" | _ }}</p>
|
||||
{{ macros::avatar(user=admin) }}
|
||||
<p><a href="/@/{{ admin.fqn }}">{{ admin.name }}</a><small>(@{{ admin.fqn }})</small></p>
|
||||
</div>
|
||||
</section>
|
||||
<p>{{ "Runs Plume {{ version }}" | _(version=version) }}
|
||||
</div>
|
||||
<section>
|
||||
{{ instance.long_description_html | safe }}
|
||||
</section>
|
||||
{% endblock content %}
|
@ -0,0 +1,34 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use plume_models::{instance::Instance, users::User};
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, admin: User, n_users: usize, n_articles: usize, n_instances: i64)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "About {0}"; instance.name.clone()).as_str(), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "About {0}"; instance.name)</h1>
|
||||
<section>
|
||||
@Html(instance.short_description_html)
|
||||
</section>
|
||||
<div class="banner">
|
||||
<section class="stats">
|
||||
<div>
|
||||
<p>@Html(i18n!(ctx.1, "Home to <em>{0}</em> users"; n_users))</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>@Html(i18n!(ctx.1, "Who wrote <em>{0}</em> articles"; n_articles))</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>@Html(i18n!(ctx.1, "And connected to <em>{0}</em> other instances"; n_instances))</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>@i18n!(ctx.1, "Administred by")</p>
|
||||
@avatar(ctx.0, &admin, Size::Small, false, ctx.1)
|
||||
<p><a href="/@@/@admin.get_fqn(ctx.0)">@admin.name(ctx.0)</a><small>@@@admin.get_fqn(ctx.0)</small></p>
|
||||
</div>
|
||||
</section>
|
||||
<p>@i18n!(ctx.1, "Runs Plume {0}"; env!("CARGO_PKG_VERSION"))</p>
|
||||
</div>
|
||||
<section>
|
||||
@Html(instance.long_description_html)
|
||||
</section>
|
||||
})
|
@ -1,35 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Administration of {{ instance.name }}" | _(instance=instance) }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Administration" | _ }}</h1>
|
||||
|
||||
{{ macros::tabs(links=['/admin', '/admin/instances', '/admin/users'], titles=['Configuration', 'Instances', 'Users'], selected=1) }}
|
||||
|
||||
<form method="post">
|
||||
{{ macros::input(name="name", label="Name", errors=errors, form=form, props='minlenght="1"', default=instance) }}
|
||||
|
||||
<label for="open_registrations">
|
||||
{% if instance.open_registrations %}
|
||||
<input type="checkbox" name="open_registrations" id="open_registrations" checked>
|
||||
{% else %}
|
||||
<input type="checkbox" name="open_registrations" id="open_registrations">
|
||||
{% endif %}
|
||||
{{ "Allow anyone to register" | _ }}
|
||||
</label>
|
||||
|
||||
<label for="short_description">{{ "Short description" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
|
||||
<textarea id="short_description" name="short_description">{{ form.short_description | default(value=instance.short_description | safe) }}</textarea>
|
||||
|
||||
<label for="long_description">{{ "Long description" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
|
||||
<textarea id="long_description" name="long_description">{{ form.long_description | default(value=instance.long_description | safe) }}</textarea>
|
||||
|
||||
{{ macros::input(name="default_license", label="Default license", errors=errors, form=form, props='minlenght="1"', default=instance) }}
|
||||
|
||||
<input type="submit" value="{{ "Save settings" | _ }}"/>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,39 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use plume_models::instance::Instance;
|
||||
@use routes::instance::InstanceSettingsForm;
|
||||
@use validator::ValidationErrors;
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, form: InstanceSettingsForm, errors: ValidationErrors)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name.clone()).as_str(), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Administration")</h1>
|
||||
|
||||
@tabs(&[
|
||||
("/admin", i18n!(ctx.1, "Configuration"), true),
|
||||
("/admin/instances", i18n!(ctx.1, "Instances"), false),
|
||||
("/admin/users", i18n!(ctx.1, "Users"), false),
|
||||
])
|
||||
|
||||
<form method="post">
|
||||
@input!(ctx.1, name (text), "Name", form, errors.clone(), "props")
|
||||
|
||||
<label for="open_registrations">
|
||||
@if instance.open_registrations {
|
||||
<input type="checkbox" name="open_registrations" id="open_registrations" checked>
|
||||
} else {
|
||||
<input type="checkbox" name="open_registrations" id="open_registrations">
|
||||
}
|
||||
@i18n!(ctx.1, "Allow anyone to register")
|
||||
|
||||
<label for="short_description">@i18n!(ctx.1, "Short description")<small>@i18n!(ctx.1, "Markdown is supported")</small></label>
|
||||
<textarea id="short_description" name="short_description">@Html(form.short_description)</textarea>
|
||||
|
||||
<label for="long_description">@i18n!(ctx.1, "Long description")<small>@i18n!(ctx.1, "Markdown is supported")</small></label>
|
||||
<textarea id="long_description" name="long_description">@Html(form.long_description)</textarea>
|
||||
|
||||
@input!(ctx.1, default_license (text), "Default license", form, errors, "minlenght=\"1\"")
|
||||
|
||||
<input type="submit" value="@i18n!(ctx.1, "Save settings")"/>
|
||||
</form>
|
||||
})
|
@ -1,32 +0,0 @@
|
||||
<section class="spaced">
|
||||
<div class="cards">
|
||||
<div class="presentation card">
|
||||
<h2>{{ "What is Plume?" | _ }}</h2>
|
||||
<main>
|
||||
<p>{{ "Plume is a decentralized blogging engine." | _ }}</p>
|
||||
<p>{{ "Authors can manage various blogs from an unique website." | _ }}</p>
|
||||
<p>{{ "Articles are also visible on other Plume websites, and you can interact with them directly from other platforms like Mastodon." | _ }}</p>
|
||||
</main>
|
||||
<a href="/users/new">{{ "Create your account" | _ }}</a>
|
||||
</div>
|
||||
<div class="presentation card">
|
||||
<h2>{{ "About {{ instance_name }}" | _(instance_name=instance.name) }}</h2>
|
||||
<main>
|
||||
{{ instance.short_description_html | safe }}
|
||||
<section class="stats">
|
||||
<div>
|
||||
<p>{{ "Home to" | _ }}</p>
|
||||
<em>{{ n_users }}</em>
|
||||
<p>{{ "people" | _ }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ "Who wrote" | _ }}</p>
|
||||
<em>{{ n_articles }}</em>
|
||||
<p>{{ "articles" | _ }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<a href="/about">{{ "Read the detailed rules" | _ }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -1,23 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "All the articles of the Fediverse" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "All the articles of the Fediverse" | _ }}</h1>
|
||||
|
||||
{% if account %}
|
||||
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=3) }}
|
||||
{% else %}
|
||||
{{ macros::tabs(links=['/', '/federated', '/local'], titles=['Latest articles', 'Federated feed', 'Local feed'], selected=2) }}
|
||||
{% endif %}
|
||||
|
||||
<div class="cards">
|
||||
{% for article in articles %}
|
||||
{{ macros::post_card(article=article) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ macros::paginate(page=page, total=n_pages) }}
|
||||
{% endblock content %}
|
@ -0,0 +1,31 @@
|
||||
@use templates::{base, partials::post_card};
|
||||
@use template_utils::*;
|
||||
@use plume_models::posts::Post;
|
||||
|
||||
@(ctx: BaseContext, articles: Vec<Post>, page: i32, n_pages: i32)
|
||||
|
||||
@:base(ctx, "All the articles of the Fediverse", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "All the articles of the Fediverse")</h1>
|
||||
|
||||
@if let Some(_) = ctx.2 {
|
||||
@tabs(&[
|
||||
("/", i18n!(ctx.1, "Latest articles"), false),
|
||||
("/feed", i18n!(ctx.1, "Your feed"), false),
|
||||
("/federated", i18n!(ctx.1, "Federated feed"), true),
|
||||
("/local", i18n!(ctx.1, "Local feed"), false),
|
||||
])
|
||||
} else {
|
||||
@tabs(&[
|
||||
("/", i18n!(ctx.1, "Latest articles"), false),
|
||||
("/federated", i18n!(ctx.1, "Federated feed"), true),
|
||||
("/local", i18n!(ctx.1, "Local feed"), false),
|
||||
])
|
||||
}
|
||||
|
||||
<div class="cards">
|
||||
@for article in articles {
|
||||
@:post_card(ctx, article)
|
||||
}
|
||||
</div>
|
||||
@paginate(ctx.1, page, n_pages)
|
||||
})
|
@ -1,25 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Your feed" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{{ "Your feed" | _ }}
|
||||
</h1>
|
||||
|
||||
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=2) }}
|
||||
|
||||
{% if articles | length > 0 %}
|
||||
<div class="cards">
|
||||
{% for article in articles %}
|
||||
{{ macros::post_card(article=article) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ macros::paginate(page=page, total=n_pages) }}
|
||||
{% else %}
|
||||
<p class="center">{{ "Nothing to see here yet. Try to follow more people." | _ }}</p>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
@ -0,0 +1,27 @@
|
||||
@use templates::{base, partials::post_card};
|
||||
@use template_utils::*;
|
||||
@use plume_models::posts::Post;
|
||||
|
||||
@(ctx: BaseContext, articles: Vec<Post>, page: i32, n_pages: i32)
|
||||
|
||||
@:base(ctx, "Your feed", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Your feed")</h1>
|
||||
|
||||
@tabs(&[
|
||||
("/", i18n!(ctx.1, "Latest articles"), false),
|
||||
("/feed", i18n!(ctx.1, "Your feed"), true),
|
||||
("/federated", i18n!(ctx.1, "Federated feed"), false),
|
||||
("/local", i18n!(ctx.1, "Local feed"), false),
|
||||
])
|
||||
|
||||
@if !articles.is_empty() {
|
||||
<div class="cards">
|
||||
@for article in articles {
|
||||
@:post_card(ctx, article)
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<p class="center">@i18n!(ctx.1, "Nothing to see here yet. Try to follow more people.")</p>
|
||||
}
|
||||
@paginate(ctx.1, page, n_pages)
|
||||
})
|
@ -1,25 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ instance.name }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Welcome to {{ instance_name | escape }}" | _(instance_name=instance.name) }}</h1>
|
||||
|
||||
{% if account %}
|
||||
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=1) }}
|
||||
|
||||
{{ macros::home_feed(title='Your feed', link='/feed', articles=user_feed) }}
|
||||
{{ macros::home_feed(title='Federated feed', link='/federated', articles=federated) }}
|
||||
{{ macros::home_feed(title='Local feed', link='/local', articles=local) }}
|
||||
{% include "instance/description" %}
|
||||
{% else %}
|
||||
{{ macros::tabs(links=['/', '/federated', '/local'], titles=['Latest articles', 'Federated feed', 'Local feed'], selected=1) }}
|
||||
|
||||
{{ macros::home_feed(title='Federated feed', link='/federated', articles=federated) }}
|
||||
{% include "instance/description" %}
|
||||
{{ macros::home_feed(title='Local feed', link='/local', articles=local) }}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
@ -0,0 +1,34 @@
|
||||
@use templates::{base, partials::*};
|
||||
@use template_utils::*;
|
||||
@use plume_models::instance::Instance;
|
||||
@use plume_models::posts::Post;
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, n_users: i32, n_articles: i32, local: Vec<Post>, federated: Vec<Post>, user_feed: Option<Vec<Post>>)
|
||||
|
||||
@:base(ctx, instance.name.clone().as_ref(), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Welcome on {}"; instance.name.as_str())</h1>
|
||||
|
||||
@if ctx.2.is_some() {
|
||||
@tabs(&[
|
||||
("/", i18n!(ctx.1, "Latest articles"), true),
|
||||
("/feed", i18n!(ctx.1, "Your feed"), false),
|
||||
("/federated", i18n!(ctx.1, "Federated feed"), false),
|
||||
("/local", i18n!(ctx.1, "Local feed"), false),
|
||||
])
|
||||
|
||||
@:home_feed(ctx, user_feed.unwrap_or_default(), "/feed", "Your feed")
|
||||
@:home_feed(ctx, federated, "/federated", "Federated feed")
|
||||
@:home_feed(ctx, local, "/local", "Local feed")
|
||||
@:instance_description(ctx, instance, n_users, n_articles)
|
||||
} else {
|
||||
@tabs(&[
|
||||
("/", i18n!(ctx.1, "Latest articles"), true),
|
||||
("/federated", i18n!(ctx.1, "Federated feed"), false),
|
||||
("/local", i18n!(ctx.1, "Local feed"), false),
|
||||
])
|
||||
|
||||
@:home_feed(ctx, federated, "/federated", "Federated feed")
|
||||
@:home_feed(ctx, local, "/local", "Local feed")
|
||||
@:instance_description(ctx, instance, n_users, n_articles)
|
||||
}
|
||||
})
|
@ -1,33 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Administration of {{ instance.name }}" | _(instance=instance) }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Instances" | _ }}</h1>
|
||||
|
||||
{{ macros::tabs(links=['/admin', '/admin/instances', '/admin/users'], titles=['Configuration', 'Instances', 'Users'], selected=2) }}
|
||||
|
||||
<div class="list">
|
||||
{% for instance in instances %}
|
||||
<div class="flex">
|
||||
<p class="grow">
|
||||
<a href="https://{{ instance.public_domain }}">{{ instance.name }}</a>
|
||||
<small>{{ instance.public_domain }}</small>
|
||||
</p>
|
||||
{% if not instance.local %}
|
||||
<form class="inline" method="post" action="/admin/instances/{{ instance.id }}/block">
|
||||
{% if instance.blocked %}
|
||||
<input type="submit" value="{{ 'Unblock' | _ }}">
|
||||
{% else %}
|
||||
<input type="submit" value="{{ 'Block' | _ }}">
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ macros::paginate(page=page, total=n_pages) }}
|
||||
{% endblock content %}
|
@ -0,0 +1,32 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use plume_models::instance::Instance;
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, instances: Vec<Instance>, page: i32, n_pages: i32)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name.clone()).as_str(), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Instances")</h1>
|
||||
|
||||
@tabs(&[
|
||||
("/admin", i18n!(ctx.1, "Configuration"), false),
|
||||
("/admin/instances", i18n!(ctx.1, "Instances"), true),
|
||||
("/admin/users", i18n!(ctx.1, "Users"), false),
|
||||
])
|
||||
|
||||
<div class="list">
|
||||
@for instance in instances {
|
||||
<div class="flex">
|
||||
<p class="grow">
|
||||
<a href="https://@instance.public_domain">@instance.name</a>
|
||||
<small>@instance.public_domain</small>
|
||||
</p>
|
||||
@if !instance.local {
|
||||
<form class="inline" method="post" action="/admin/instances/@instance.id/block">
|
||||
<input type="submit" value="@i18n!(ctx.1, if instance.blocked { "Unblock" } else { "Block"})">
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@paginate(ctx.1, page, n_pages)
|
||||
})
|
@ -1,23 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Articles from {{ instance.name }}" | _(instance=instance) }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Articles from {{ instance.name }}" | _(instance=instance) }}</h1>
|
||||
|
||||
{% if account %}
|
||||
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=4) }}
|
||||
{% else %}
|
||||
{{ macros::tabs(links=['/', '/federated', '/local'], titles=['Latest articles', 'Federated feed', 'Local feed'], selected=3) }}
|
||||
{% endif %}
|
||||
|
||||
<div class="cards">
|
||||
{% for article in articles %}
|
||||
{{ macros::post_card(article=article) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ macros::paginate(page=page, total=n_pages) }}
|
||||
{% endblock content %}
|
@ -0,0 +1,32 @@
|
||||
@use templates::{base, partials::post_card};
|
||||
@use template_utils::*;
|
||||
@use plume_models::posts::Post;
|
||||
@use plume_models::instance::Instance;
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, articles: Vec<Post>, page: i32, n_pages: i32)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "Articles from {}"; instance.name.clone()).as_str(), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Articles from {}"; instance.name)</h1>
|
||||
|
||||
@if let Some(_) = ctx.2 {
|
||||
@tabs(&[
|
||||
("/", i18n!(ctx.1, "Latest articles"), false),
|
||||
("/feed", i18n!(ctx.1, "Your feed"), false),
|
||||
("/federated", i18n!(ctx.1, "Federated feed"), false),
|
||||
("/local", i18n!(ctx.1, "Local feed"), true),
|
||||
])
|
||||
} else {
|
||||
@tabs(&[
|
||||
("/", i18n!(ctx.1, "Latest articles"), false),
|
||||
("/federated", i18n!(ctx.1, "Federated feed"), false),
|
||||
("/local", i18n!(ctx.1, "Local feed"), true),
|
||||
])
|
||||
}
|
||||
|
||||
<div class="cards">
|
||||
@for article in articles {
|
||||
@:post_card(ctx, article)
|
||||
}
|
||||
</div>
|
||||
@paginate(ctx.1, page, n_pages)
|
||||
})
|
@ -1,30 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Users" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Users" | _ }}</h1>
|
||||
|
||||
{{ macros::tabs(links=['/admin', '/admin/instances', '/admin/users'], titles=['Configuration', 'Instances', 'Users'], selected=3) }}
|
||||
|
||||
<div class="list">
|
||||
{% for user in users %}
|
||||
<div class="flex">
|
||||
{{ macros::avatar(user=user) }}
|
||||
<p class="grow">
|
||||
<a href="/@/{{ user.fqn }}">{{ user.name }}</a>
|
||||
<small>@{{ user.username }}</small>
|
||||
</p>
|
||||
{% if not user.is_admin %}
|
||||
<form class="inline" method="post" href="/admin/users/{{ user.id }}/ban">
|
||||
<input type="submit" value="{{ 'Ban' | _ }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ macros::paginate(page=page, total=n_pages) }}
|
||||
{% endblock content %}
|
@ -0,0 +1,33 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use plume_models::users::User;
|
||||
|
||||
@(ctx: BaseContext, users: Vec<User>, page: i32, n_pages: i32)
|
||||
|
||||
@:base(ctx, "Users", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Users")</h1>
|
||||
|
||||
@tabs(&[
|
||||
("/admin", i18n!(ctx.1, "Configuration"), false),
|
||||
("/admin/instances", i18n!(ctx.1, "Instances"), false),
|
||||
("/admin/users", i18n!(ctx.1, "Users"), true),
|
||||
])
|
||||
|
||||
<div class="list">
|
||||
@for user in users {
|
||||
<div class="flex">
|
||||
@avatar(ctx.0, &user, Size::Small, false, ctx.1)
|
||||
<p class="grow">
|
||||
<a href="/@@/@user.get_fqn(ctx.0)">@user.name(ctx.0)</a>
|
||||
<small>@format!("@{}", user.username)</small>
|
||||
</p>
|
||||
@if !user.is_admin {
|
||||
<form class="inline" method="post" action="/admin/users/@user.id/ban">
|
||||
<input type="submit" value="@i18n!(ctx.1, "Ban")">
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@paginate(ctx.1, page, n_pages)
|
||||
})
|
@ -1,112 +0,0 @@
|
||||
{% macro post_card(article) %}
|
||||
<div class="card">
|
||||
{% if article.cover %}
|
||||
<div class="cover" style="background-image: url('{{ article.cover.url }}')"></div>
|
||||
{% endif %}
|
||||
<h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3>
|
||||
<main>
|
||||
<p>
|
||||
{% if article.post.subtitle | length > 0 %}
|
||||
{{ article.post.subtitle }}
|
||||
{% else %}
|
||||
{{ article.post.content | safe | striptags | truncate(length=200) }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</main>
|
||||
<p class="author">
|
||||
{{ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" | _(
|
||||
link_1='<a href="/@/',
|
||||
link_2=article.author.fqn,
|
||||
link_3='/">',
|
||||
name=article.author.name,
|
||||
link_4="</a>")
|
||||
}}
|
||||
{% if article.post.published %}⋅ {{ article.date | date(format="%B %e") }}{% endif %}
|
||||
⋅ <a href="/~/{{ article.blog.fqn }}/">{{ article.blog.title }}</a>
|
||||
{% if not article.post.published %}⋅ {{ "Draft" | _ }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endmacro post_card %}
|
||||
{% macro input(name, label, errors="", form="", type="text", props="", optional=false, default='', details=' ') %}
|
||||
<label for="{{ name }}">
|
||||
{{ label | _ }}
|
||||
{% if optional %}
|
||||
<small>{{ "Optional" | _ }}</small>
|
||||
{% endif %}
|
||||
<small>{{ details | _ }}</small>
|
||||
</label>
|
||||
{% if errors is defined and errors[name] %}
|
||||
{% for err in errors[name] %}
|
||||
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% set default = default[name] | default(value="") %}
|
||||
<input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ form[name] | default(value=default) }}" {{ props | safe }}/>
|
||||
{% endmacro input %}
|
||||
{% macro paginate(page, total, previous="Previous page", next="Next page", query="") %}
|
||||
{% if query %}
|
||||
{% set query = query ~ "&" %}
|
||||
{% endif %}
|
||||
<div class="pagination">
|
||||
{% if page != 1 %}
|
||||
<a href="?{{ query }}page={{ page - 1 }}">{{ previous | _ }}</a>
|
||||
{% endif %}
|
||||
{% if page < total %}
|
||||
<a href="?{{ query }}page={{ page + 1 }}">{{ next | _ }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% macro comment(comm) %}
|
||||
<div class="comment" id="comment-{{ comm.id }}">
|
||||
<a class="author" href="/@/{{ comm.author.fqn }}/">
|
||||
{{ macros::avatar(user=comm.author, pad=true) }}
|
||||
<span class="display-name">{{ comm.author.name }}</span>
|
||||
<small>@{{ comm.author.fqn }}</small>
|
||||
</a>
|
||||
<div class="text">
|
||||
{% if comm.sensitive %}
|
||||
<details>
|
||||
<summary>{{ comm.spoiler_text }}</summary>
|
||||
{% endif %}
|
||||
{{ comm.content | safe }}
|
||||
{% if comm.sensitive %}
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a class="button icon icon-message-circle" href="?responding_to={{ comm.id }}">{{ "Respond" | _ }}</a>
|
||||
{% for res in comm.responses %}
|
||||
{{ self::comment(comm=res) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% macro tabs(links, titles, selected) %}
|
||||
<div class="tabs">
|
||||
{% for link in links %}
|
||||
{% set idx = loop.index0 %}
|
||||
<a href="{{ link }}" {% if loop.index == selected %}class="selected"{% endif %}>{{ titles[idx] | _ }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% macro feather(name) %}
|
||||
<svg class="feather">
|
||||
<use xlink:href="/static/images/feather-sprite.svg#{{ name }}"/>
|
||||
</svg>
|
||||
{% endmacro %}
|
||||
{% macro home_feed(title, link, articles) %}
|
||||
{% if articles | length > 0 %}
|
||||
<h2>{{ title | _ }} — <a href="{{ link }}">{{ "View all" | _ }}</a></h2>
|
||||
<div class="cards spaced">
|
||||
{% for article in articles %}
|
||||
{{ macros::post_card(article=article) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% macro avatar(user, size="small", pad=true) %}
|
||||
<div
|
||||
class="avatar {{ size }} {% if pad %}padded{% endif %}"
|
||||
style="background-image: url('{{ user.avatar }}');"
|
||||
title="{{ "{{ name }}'s avatar" | _(name=user.name) }}"
|
||||
aria-label="{{ "{{ name }}'s avatar" | _(name=user.name) }}"
|
||||
></div>
|
||||
{% endmacro %}
|
@ -1,35 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Media details" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Media details" }}</h1>
|
||||
<section>
|
||||
<a href="/medias">{{ "Go back to the gallery" | _ }}</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<figure class="media">
|
||||
{{ media.html | safe }}
|
||||
<figcaption>{{ media.alt_text }}</figcaption>
|
||||
</figure>
|
||||
<div>
|
||||
<p>
|
||||
{{ "Markdown code" | _ }}
|
||||
<small>{{ "Copy it in your articles to insert this media." }}</small>
|
||||
</p>
|
||||
<code>{{ media.md }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<form class="inline" method="post" action="/medias/{{ media.id }}/avatar">
|
||||
<input class="button" type="submit" value="{{ 'Use as avatar' | _ }}">
|
||||
</form>
|
||||
<form class="inline" method="post" action="/medias/{{ media.id }}/delete">
|
||||
<input class="button" type="submit" value="{{ 'Delete' | _ }}">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
@ -0,0 +1,36 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use plume_models::medias::{Media, MediaCategory};
|
||||
|
||||
@(ctx: BaseContext, media: Media)
|
||||
|
||||
@:base(ctx, "Media details", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Media details")</h1>
|
||||
<section>
|
||||
<a href="/medias">@i18n!(ctx.1, "Go back to the gallery")</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<figure class="media">
|
||||
@Html(media.html(ctx.0))
|
||||
<figcaption>@media.alt_text</figcaption>
|
||||
</figure>
|
||||
<div>
|
||||
<p>
|
||||
@i18n!(ctx.1, "Markdown code")
|
||||
<small>@i18n!(ctx.1, "Copy it in your articles to insert this media.")</small>
|
||||
</p>
|
||||
<code>@media.markdown(ctx.0)</code>
|
||||
</div>
|
||||
<div>
|
||||
@if media.category() == MediaCategory::Image {
|
||||
<form class="inline" method="post" action="/medias/@media.id/avatar">
|
||||
<input class="button" type="submit" value="@i18n!(ctx.1, "Use as avatar")">
|
||||
</form>
|
||||
}
|
||||
<form class="inline" method="post" action="/medias/@media.id/delete">
|
||||
<input class="button" type="submit" value="@i18n!(ctx.1, "Delete")">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
})
|
@ -1,31 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Your media" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Your media" | _ }}</h1>
|
||||
<div>
|
||||
<a href="/medias/new" class="inline-block button">Upload</a>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
{% if medias | length < 1 %}
|
||||
<p>{{ "You don't have any media yet." | _ }}</p>
|
||||
{% endif %}
|
||||
<div class="list">
|
||||
{% for media in medias %}
|
||||
<div class="card flex">
|
||||
{{ media.html_preview | safe }}
|
||||
<main class="grow">
|
||||
<p><a href="/medias/{{ media.id }}">{{ media.alt_text }}</a></p>
|
||||
</main>
|
||||
<a href="/medias/{{ media.id }}/delete">{{ "Delete" | _ }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{# TODO: macros::paginate(page=page, total=n_pages) #}
|
||||
</section>
|
||||
{% endblock content %}
|
@ -0,0 +1,29 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use plume_models::medias::Media;
|
||||
|
||||
@(ctx: BaseContext, medias: Vec<Media>)
|
||||
|
||||
@:base(ctx, "Your media", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Your media")</h1>
|
||||
<div>
|
||||
<a href="/medias/new" class="inline-block button">@i18n!(ctx.1, "Upload")</a>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
@if medias.is_empty() {
|
||||
<p>@i18n!(ctx.1, "You don't have any media yet.")</p>
|
||||
}
|
||||
<div class="list">
|
||||
@for media in medias {
|
||||
<div class="card flex">
|
||||
@Html(media.preview_html(ctx.0))
|
||||
<main class="grow">
|
||||
<p><a href="/medias/@media.id">@media.alt_text</a></p>
|
||||
</main>
|
||||
<a href="/medias/@media.id/delete">@i18n!(ctx.1, "Delete")</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
})
|
@ -1,17 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Media upload" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Media upload" | _ }}</h1>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{{ macros::input(name="alt", label="Description", errors=errors, form=form, props='required minlength="1"', details='Useful for visually impaired people and licensing') }}
|
||||
{{ macros::input(name="cw", label="Content warning", errors=errors, form=form, details='Let it empty if there is none') }}
|
||||
{{ macros::input(name="file", type='file', label="File", errors=errors, form=form, props='required') }}
|
||||
|
||||
<input type="submit" value="{{ "Send" | _ }}"/>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,28 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext)
|
||||
|
||||
@:base(ctx, "Media upload", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Media upload")</h1>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<label for="alt">
|
||||
@i18n!(ctx.1, "Description")
|
||||
<small>@i18n!(ctx.1, "Useful for visually impaired people and licensing")</small>
|
||||
</label>
|
||||
<input type="text" id="alt" name="alt" required minlenght="1"/>
|
||||
|
||||
<label for="cw">
|
||||
@i18n!(ctx.1, "Content warning")
|
||||
<small>@i18n!(ctx.1, "Let it empty if there is none")</small>
|
||||
</label>
|
||||
<input type="txt" id="cw" name="cw"/>
|
||||
|
||||
<label for="file">
|
||||
@i18n!(ctx.1, "File")
|
||||
</label>
|
||||
<input type="file" id="file" name="file" required/>
|
||||
|
||||
<input type="submit" value="@i18n!(ctx.1, "Send")"/>
|
||||
</form>
|
||||
})
|
@ -1,66 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{{ "Notifications" | _ }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ "Notifications" | _ }}</h1>
|
||||
<div class="list">
|
||||
{% for notification in notifications %}
|
||||
<div class="card flex">
|
||||
{% if notification.kind == "COMMENT" %}
|
||||
<i class="icon icon-message-circle left-icon"></i>
|
||||
<main class="grow">
|
||||
<h3><a href="{{ notification.object.post.url }}#comment-{{ notification.object.id }}">
|
||||
{{ "{{ user }} commented your article." | _(user=notification.object.user.name | escape) }}
|
||||
</a></h3>
|
||||
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
|
||||
</main>
|
||||
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
|
||||
|
||||
{% elif notification.kind == "FOLLOW" %}
|
||||
<i class="icon icon-user-plus left-icon"></i>
|
||||
<main class="grow">
|
||||
<h3><a href="/@/{{ notification.object.follower.fqn }}/">
|
||||
{{ "{{ user }} is now following you." | _(user=notification.object.follower.name | escape) }}
|
||||
</a></h3>
|
||||
</main>
|
||||
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
|
||||
|
||||
{% elif notification.kind == "LIKE" %}
|
||||
<i class="icon icon-heart left-icon"></i>
|
||||
<main class="grow">
|
||||
<h3>
|
||||
{{ "{{ user }} liked your article." | _(user=notification.object.user.name | escape) }}
|
||||
</h3>
|
||||
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
|
||||
</main>
|
||||
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
|
||||
|
||||
{% elif notification.kind == "MENTION" %}
|
||||
<i class="icon icon-at-sign left-icon"></i>
|
||||
<main class="grow">
|
||||
<h3><a href="{{ notification.object.url }}">
|
||||
{{ "{{ user }} mentioned you." | _(user=notification.object.user.name | escape) }}
|
||||
</a></h3>
|
||||
</main>
|
||||
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
|
||||
|
||||
{% elif notification.kind == "RESHARE" %}
|
||||
<i class="icon icon-repeat left-icon"></i>
|
||||
<main class="grow">
|
||||
<h3>
|
||||
{{ "{{ user }} boosted your article." | _(user=notification.object.user.name | escape) }}
|
||||
</h3>
|
||||
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
|
||||
</main>
|
||||
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ macros::paginate(page=page, total=n_pages) }}
|
||||
{% endblock content %}
|
@ -0,0 +1,33 @@
|
||||
@use templates::base;
|
||||
@use template_utils::*;
|
||||
@use plume_models::notifications::Notification;
|
||||
|
||||
@(ctx: BaseContext, notifications: Vec<Notification>, page: i32, n_pages: i32)
|
||||
|
||||
@:base(ctx, "Notifications", {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Notifications")</h1>
|
||||
|
||||
<div class="list">
|
||||
@for notification in notifications {
|
||||
<div class="card flex">
|
||||
<i class="icon @notification.icon_class() left-icon"></i>
|
||||
<main class="grow">
|
||||
<h3>
|
||||
@if let Some(url) = notification.get_url(ctx.0) {
|
||||
<a href="@url">
|
||||
@i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0))
|
||||
</a>
|
||||
} else {
|
||||
@i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0))
|
||||
}
|
||||
</h3>
|
||||
@if let Some(post) = notification.get_post(ctx.0) {
|
||||
<p><a href="@post.url(ctx.0)">@post.title</a></p>
|
||||
}
|
||||
</main>
|
||||
<p><small>@notification.creation_date.format("%B %e, %H:%M")</small></p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@paginate(ctx.1, page, n_pages)
|
||||
})
|
@ -0,0 +1,27 @@
|
||||
@use template_utils::*;
|
||||
@use plume_models::comments::Comment;
|
||||
@use plume_models::users::User;
|
||||
|
||||
@(ctx: BaseContext, comm: &Comment, author: User)
|
||||
|
||||
<div class="comment" id="comment-@comm.id">
|
||||
<a class="author" href="/@@/@author.get_fqn(ctx.0)/">
|
||||
@avatar(ctx.0, &author, Size::Small, true, ctx.1)
|
||||
<span class="display-name">@author.name(ctx.0)</span>
|
||||
<small>@author.get_fqn(ctx.0)</small>
|
||||
</a>
|
||||
<div class="text">
|
||||
@if comm.sensitive {
|
||||
<details>
|
||||
<summary>@comm.spoiler_text</summary>
|
||||
}
|
||||
@Html(&comm.content)
|
||||
@if comm.sensitive {
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
<a class="button icon icon-message-circle" href="?responding_to=@comm.id">@i18n!(ctx.1, "Respond")</a>
|
||||
@for res in comm.get_responses(ctx.0) {
|
||||
@:comment(ctx, &res, res.get_author(ctx.0))
|
||||
}
|
||||
</div>
|
@ -0,0 +1,14 @@
|
||||
@use templates::partials::post_card;
|
||||
@use plume_models::posts::Post;
|
||||
@use template_utils::*;
|
||||
|
||||
@(ctx: BaseContext, articles: Vec<Post>, link: &str, title: &str)
|
||||
|
||||
@if articles.len() > 0 {
|
||||
<h2>@i18n!(ctx.1, title) — <a href="@link">@i18n!(ctx.1, "View all")</a></h2>
|
||||
<div class="cards spaced">
|
||||
@for article in articles {
|
||||
@:post_card(ctx, article)
|
||||
}
|
||||
</div>
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
@use template_utils::*;
|
||||
@use plume_models::instance::Instance;
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, n_users: i32, n_articles: i32)
|
||||
|
||||
<section class="spaced">
|
||||
<div class="cards">
|
||||
<div class="presentation card">
|
||||
<h2>@i18n!(ctx.1, "What is Plume?")</h2>
|
||||
<main>
|
||||
<p>@i18n!(ctx.1, "Plume is a decentralized blogging engine.")</p>
|
||||
<p>@i18n!(ctx.1, "Authors can manage various blogs from an unique website.")</p>
|
||||
<p>@i18n!(ctx.1, "Articles are also visible on other Plume websites, and you can interact with them directly from other platforms like Mastodon.")</p>
|
||||
</main>
|
||||
<a href="/users/new">@i18n!(ctx.1, "Create your account")</a>
|
||||
</div>
|
||||
<div class="presentation card">
|
||||
<h2>@i18n!(ctx.1, "About {0}"; instance.name)</h2>
|
||||
<main>
|
||||
@Html(instance.short_description_html)
|
||||
<section class="stats">
|
||||
<div>
|
||||
<p>@Html(i18n!(ctx.1, "Home to <em>{0}</em> people"; n_users))</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>@Html(i18n!(ctx.1, "Who wrote <em>{0}</em> articles"; n_articles))</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<a href="/about">@i18n!(ctx.1, "Read the detailed rules")</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,29 @@
|
||||
@use template_utils::*;
|
||||
@use plume_models::posts::Post;
|
||||
|
||||
@(ctx: BaseContext, article: Post)
|
||||
|
||||
<div class="card">
|
||||
@if article.cover_id.is_some() {
|
||||
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
||||
}
|
||||
<h3><a href="@article.url(ctx.0)">@article.title</a></h3>
|
||||
<main>
|
||||
<p>@article.subtitle</p>
|
||||
</main>
|
||||
<p class="author">
|
||||
@Html(i18n!(ctx.1, "By {0}"; format!(
|
||||
"<a href=\"/@/{}/\">{}</a>",
|
||||
escape(&article.get_authors(ctx.0)[0].get_fqn(ctx.0)),
|
||||
escape(&article.get_authors(ctx.0)[0].name(ctx.0))
|
||||
)))
|
||||
@if article.published {
|
||||
⋅ @article.creation_date.format("%B %e, %Y")
|
||||
}
|
||||
⋅ <a href="/~/@article.get_blog(ctx.0).get_fqn(ctx.0)/">@article.get_blog(ctx.0).title</a>
|
||||
@if !article.published {
|
||||
⋅ @i18n!(ctx.1, "Draft")
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,152 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block head %}
|
||||
<meta property="og:title" content="{{ article.post.title }}"/>
|
||||
<meta property="og:type" content="article"/>
|
||||
{% if article.cover %}
|
||||
<meta property="og:image" content="{{ article.cover.url | safe }}"/>
|
||||
{% endif %}
|
||||
<meta property="og:url" content="{{ article.url | safe }}"/>
|
||||
{% if article.post.subtitle %}
|
||||
<meta property="og:description" content="{{ article.post.subtitle }}"/>
|
||||
{% endif %}
|
||||
{% endblock head %}
|
||||
|
||||
{% block title %}
|
||||
{{ article.post.title }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block header %}
|
||||
<a href="/~/{{ blog.fqn }}">{{ blog.title }}</a>
|
||||
{% endblock header %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="article">{{ article.post.title }}</h1>
|
||||
<h2 class="article">{{ article.post.subtitle }}</h2>
|
||||
<div class="article-info">
|
||||
<span class="author">{{ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" | _(
|
||||
link_1='<a href="/@/',
|
||||
url=author.fqn,
|
||||
link_2='/">',
|
||||
name=author.name,
|
||||
link_3="</a>"
|
||||
)
|
||||
}}</span>
|
||||
—
|
||||
<span class="date">{{ date | date(format="%B %e, %Y") }}</span>
|
||||
{% if is_author %}
|
||||
—
|
||||
<a href="{{ article.url}}edit">{{ "Edit" | _ }}</a>
|
||||
—
|
||||
<form class="inline" method="post" action="{{ article.url}}delete">
|
||||
<input onclick="return confirm('Are you sure you?')" type="submit" value="{{ 'Delete this article' | _ }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not article.post.published %}
|
||||
<span class="badge">{{ "Draft" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if article.cover %}
|
||||
<div class="cover" style="background-image: url('{{ article.cover.url }}')"></div>
|
||||
{% endif %}
|
||||
<article>
|
||||
{{ article.post.content | safe }}
|
||||
</article>
|
||||
|
||||
<div class="article-meta">
|
||||
<p>{{ "This article is under the {{ license }} license." | _(license=article.post.license) }}</p>
|
||||
<ul class="tags">
|
||||
{% for tag in article.tags %}
|
||||
{% if not tag.is_hashtag %}
|
||||
<li><a href="/tag/{{ tag.tag }}">{{ tag.tag }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="flex">
|
||||
{{ macros::avatar(user=author, pad=true, size="medium") }}
|
||||
<div class="grow">
|
||||
<h2><a href="/@/{{ author.fqn }}">{{ author.name }}</a></h2>
|
||||
<p>{{ author.summary | safe }}</h2>
|
||||
</div>
|
||||
<a href="/@/{{ author.fqn }}/follow" class="button">
|
||||
{% if is_following %}
|
||||
{{ "Unfollow" | _ }}
|
||||
{% else %}
|
||||
{{ "Follow" | _ }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if account %}
|
||||
<div class="actions">
|
||||
<form class="likes" action="{{ article.url }}like" method="POST">
|
||||
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
|
||||
|
||||
{% if has_liked %}
|
||||
<button type="submit" class="action liked">{{ macros::feather(name="heart") }}{{ "I don't like this anymore" | _ }}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<form class="reshares" action="{{ article.url }}reshare" method="POST">
|
||||
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
|
||||
|
||||
{% if has_reshared %}
|
||||
<button type="submit" class="action reshared"><i class="icon icon-repeat"></i>{{ "I don't want to boost this anymore" | _ }}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="center">{{ "Login or use your Fediverse account to interact with this article" | _ }}</p>
|
||||
<div class="actions">
|
||||
<div class="likes">
|
||||
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
|
||||
<a href="/login?m=Login%20to%20like" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</a>
|
||||
</div>
|
||||
|
||||
<div class="reshares">
|
||||
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
|
||||
<a href="/login?m=Login%20to%20boost" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="comments">
|
||||
<h2>{{ "Comments" | _ }}</h2>
|
||||
|
||||
{% if account %}
|
||||
<form method="post" action="{{ article.url }}comment">
|
||||
{{ macros::input(
|
||||
name="warning",
|
||||
label="Content warning",
|
||||
optional=true,
|
||||
form=comment_form,
|
||||
errors=comment_errors,
|
||||
default=default)
|
||||
}}
|
||||
|
||||
<label for="plume-editor">{{ "Your comment" | _ }}</label>
|
||||
{% if previous %}
|
||||
<input type="hidden" name="responding_to" value="{{ previous.id }}"/>
|
||||
{% endif %}
|
||||
{# Ugly, but we don't have the choice if we don't want weird paddings #}
|
||||
<textarea id="plume-editor" name="content">{% filter trim %}{% if previous %}{% if previous.author.fqn != user_fqn %}@{{ previous.author.fqn }} {% endif %}{% for mention in previous.mentions %}{% if mention != user_fqn %}@{{ mention }} {% endif %}{% endfor %}{% endif %}{% endfilter %}</textarea>
|
||||
<input type="submit" value="{{ "Submit comment" | _ }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if comments | length > 0 %}
|
||||
<div class="list">
|
||||
{% for comment in comments %}
|
||||
{{ macros::comment(comm=comment) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="center">{{ "No comments yet. Be the first to react!" | _ }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,146 @@
|
||||
@use templates::{base, partials::comment};
|
||||
@use template_utils::*;
|
||||
@use plume_models::blogs::Blog;
|
||||
@use plume_models::comments::Comment;
|
||||
@use plume_models::posts::Post;
|
||||
@use plume_models::tags::Tag;
|
||||
@use plume_models::users::User;
|
||||
@use validator::ValidationErrors;
|
||||
@use routes::comments::NewCommentForm;
|
||||
|
||||
@(ctx: BaseContext, article: Post, blog: Blog, comment_form: &NewCommentForm, comment_errors: ValidationErrors, tags: Vec<Tag>, comments: Vec<Comment>, previous_comment: Option<Comment>, n_likes: usize, n_reshares: usize, has_liked: bool, has_reshared: bool, is_following: bool, author: User)
|
||||
|
||||
@:base(ctx, &article.title.clone(), {
|
||||
<meta property="og:title" content="article.title"/>
|
||||
<meta property="og:type" content="article"/>
|
||||
@if article.cover_id.is_some() {
|
||||
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
|
||||
}
|
||||
<meta property="og:url" content="@Html(article.url(ctx.0))"/>
|
||||
<meta property="og:description" content="@article.subtitle"/>
|
||||
}, {
|
||||
<a href="/~/@blog.get_fqn(ctx.0)">@blog.title</a>
|
||||
}, {
|
||||
<h1 class="article">@&article.title</h1>
|
||||
<h2 class="article">@&article.subtitle</h2>
|
||||
<div class="article-info">
|
||||
<span class="author">
|
||||
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"/@/{}/\">{}</a>", escape(&author.get_fqn(ctx.0)), escape(&author.name(ctx.0)))))
|
||||
</span>
|
||||
—
|
||||
<span class="date">@article.creation_date.format("%B %e, %Y")</span>
|
||||
@if ctx.2.clone().map(|u| u.id == author.id).unwrap_or(false) {
|
||||
—
|
||||
<a href="@article.url(ctx.0)/edit">@i18n!(ctx.1, "Edit")</a>
|
||||
—
|
||||
<form class="inline" method="post" action="@article.url(ctx.0)/delete">
|
||||
<input onclick="return confirm('Are you sure you?')" type="submit" value="@i18n!(ctx.1, "Delete this article")">
|
||||
</form>
|
||||
}
|
||||
@if !article.published {
|
||||
<span class="badge">@i18n!(ctx.1, "Draft")</span>
|
||||
}
|
||||
</div>
|
||||
@if article.cover_id.is_some() {
|
||||
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
||||
}
|
||||
<article>
|
||||
@Html(&article.content)
|
||||
</article>
|
||||
|
||||
<div class="article-meta">
|
||||
<p>@i18n!(ctx.1, "This article is under the {0} license."; &article.license)</p>
|
||||
<ul class="tags">
|
||||
@for tag in tags {
|
||||
@if !tag.is_hashtag {
|
||||
<li><a href="/tag/@tag.tag">@tag.tag</a></li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
<div class="flex">
|
||||
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
|
||||
<div class="grow">
|
||||
<h2><a href="/@@/@author.get_fqn(ctx.0)">@author.name(ctx.0)</a></h2>
|
||||
<p>@Html(&author.summary)</h2>
|
||||
</div>
|
||||
<a href="/@@/@author.get_fqn(ctx.0)/follow" class="button">
|
||||
@if is_following {
|
||||
@i18n!(ctx.1, "Unfollow")
|
||||
} else {
|
||||
@i18n!(ctx.1, "Follow")
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if ctx.2.is_some() {
|
||||
<div class="actions">
|
||||
<form class="likes" action="@article.url(ctx.0)/like" method="POST">
|
||||
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes", &n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes", n_likes)">
|
||||
@n_likes
|
||||
</p>
|
||||
|
||||
@if has_liked {
|
||||
<button type="submit" class="action liked">@icon!("heart") @i18n!(ctx.1, "I don't like this anymore")</button>
|
||||
} else {
|
||||
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
|
||||
}
|
||||
</form>
|
||||
<form class="reshares" action="@article.url(ctx.0)/reshare" method="POST">
|
||||
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost", &n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts", n_reshares)">
|
||||
@n_reshares
|
||||
</p>
|
||||
|
||||
@if has_reshared {
|
||||
<button type="submit" class="action reshared">@icon!("repeat") @i18n!(ctx.1, "I don't want to boost this anymore")</button>
|
||||
} else {
|
||||
<button type="submit" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
} else {
|
||||
<p class="center">@i18n!(ctx.1, "Login or use your Fediverse account to interact with this article")</p>
|
||||
<div class="actions">
|
||||
<div class="likes">
|
||||
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes", &n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes", n_likes)">
|
||||
@n_likes
|
||||
</p>
|
||||
<a href="/login?m=Login%20to%20like" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
|
||||
</div>
|
||||
|
||||
<div class="reshares">
|
||||
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost", &n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts", n_reshares)">
|
||||
@n_reshares
|
||||
</p>
|
||||
<a href="/login?m=Login%20to%20boost" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments">
|
||||
<h2>@i18n!(ctx.1, "Comments")</h2>
|
||||
|
||||
@if ctx.2.is_some() {
|
||||
<form method="post" action="@article.url(ctx.0)/comment">
|
||||
@input!(ctx.1, warning (optional text), "Content warning", comment_form, comment_errors, "")
|
||||
|
||||
<label for="plume-editor">@i18n!(ctx.1, "Your comment")</label>
|
||||
@if let Some(ref prev) = previous_comment {
|
||||
<input type="hidden" name="responding_to" value="@prev.id"/>
|
||||
}
|
||||
<textarea id="plume-editor" name="content">@comment_form.content</textarea>
|
||||
<input type="submit" value="@i18n!(ctx.1, "Submit comment")" />
|
||||
</form>
|
||||
}
|
||||
|
||||
@if !comments.is_empty() {
|
||||
<div class="list">
|
||||
@for comm in comments {
|
||||
@:comment(ctx, &comm, comm.get_author(ctx.0))
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<p class="center">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
})
|
@ -1,68 +0,0 @@
|
||||
{% extends "base" %}
|
||||
{% import "macros" as macros %}
|
||||
|
||||
{% block title %}
|
||||
{% if editing %}
|
||||
{{ "Edit {{ post }}" | _(post=form.title) }}
|
||||
{% else %}
|
||||
{{ "New post" | _ }}
|
||||
{% endif %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% if editing %}
|
||||
{{ "Edit {{ post }}" | _(post=form.title) }}
|
||||
{% else %}
|
||||
{{ "Create a post" | _ }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
<form class="new-post" method="post">
|
||||
{{ macros::input(name="title", label="Title", errors=errors, form=form, props="required") }}
|
||||
{{ macros::input(name="subtitle", label="Subtitle", errors=errors, form=form, optional=true) }}
|
||||
|
||||
{% if errors is defined and errors.content %}
|
||||
{% for err in errors.content %}
|
||||
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<label for="plume-editor">{{ "Content" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
|
||||
<textarea id="plume-editor" name="content" rows="20">{{ form.content | default(value="") }}</textarea>
|
||||
|
||||
{{ macros::input(name="tags", label="Tags, separated by commas", errors=errors, form=form, optional=true) }}
|
||||
|
||||
{% set license_infos = "Default license will be {{ instance.default_license }}" | _(instance=instance) %}
|
||||
{{ macros::input(name="license", label="License", errors=errors, form=form, optional=true, details=license_infos) }}
|
||||
|
||||
<label for="cover">{{ "Illustration" | _ }}<small>{{ "Optional" | _ }}</small></label>
|
||||
<select id="cover" name="cover">
|
||||
<option value="none" {% if form is undefined or form.cover is undefined %}selected{% endif %}>{{ "None" | _ }}</option>
|
||||
{% for media in medias %}
|
||||
{% if media.category == "image" %}
|
||||
<option value="{{ media.id }}" {% if form is defined and form.cover is defined and form.cover == media.id %}selected{% endif %}>
|
||||
{{ media.alt_text | default(value=media.content_warning) }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% if is_draft %}
|
||||
<label for="draft">
|
||||
<input type="checkbox" name="draft" id="draft" checked>
|
||||
{{ "This is a draft, don't publish it yet." | _ }}
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
{% if editing %}
|
||||
<input type="submit" value="{{ "Update" | _ }}" />
|
||||
{% else %}
|
||||
{% if is_draft %}
|
||||
<input type="submit" value="{{ "Update or publish" | _ }}" />
|
||||
{% else %}
|
||||
<input type="submit" value="{{ "Publish" | _ }}" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</form>
|
||||
<script src="/static/js/autoExpand.js"></script>
|
||||
{% endblock content %}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue