use std::{borrow::Borrow, cmp::Ordering, fmt::Display};
use axum::extract::State;
use convert_case::{Case, Casing};
use i18n_embed::fluent::FluentLanguageLoader;
use i18n_embed_fl::fl;
use maud::{html, Markup, PreEscaped, DOCTYPE};
use uuid::Uuid;
use crate::{
context::ContextTrait, entity::EntityBase, input::InputInfo, property::EnumVariant, Entity,
};
#[non_exhaustive]
pub struct FormRenderContext<'a, S: ContextTrait> {
pub form_id: &'a str,
pub ctx: S,
}
pub fn document(body: Markup) -> Markup {
html! {
(DOCTYPE)
html {
head {
link rel="stylesheet" href="" {}
meta charset="utf-8" {}
link rel="icon" href="/favicon.png" {}
link rel="stylesheet" type="text/css" href="/css/main.css" {}
meta name="viewport" content="width=device-width, initial-scale=1" {}
}
body {
(body)
}
}
}
}
pub fn sidebar(
_i18n: &FluentLanguageLoader,
names: impl IntoIterator<Item = impl AsRef<str>>,
active: &str,
) -> Markup {
html! {
nav class="cms-sidebar" {
@for name in names {
@let name = name.as_ref();
a href=(&format!("/{}", name.to_case(Case::Kebab))) class=[(name == active).then_some("active")] {
(name.to_case(Case::Title))
}
}
}
}
}
pub fn entity_inputs<E: Entity<S>, S: ContextTrait>(
ctx: S,
i18n: &FluentLanguageLoader,
value: Option<&E>,
) -> Markup {
let form_id = &Uuid::new_v4().to_string();
let ctx = FormRenderContext { form_id, ctx };
html! {
form id=(form_id) class="cms-entity-form cms-add-form" method="post" enctype="multipart/form-data" {
(inputs(&ctx, i18n, EntityBase::inputs(value)))
button class="cms-button" type="submit" {
(fl!(i18n, "entity-inputs-submit"))
}
script src="/js/callOnMountRecursive.js" {}
script {
(PreEscaped(format!(r#"callOnMountRecursive(document.getElementById("{form_id}"));"#)))
}
}
}
}
pub fn struct_input<'a, S: ContextTrait>(
ctx: &FormRenderContext<'_, S>,
i18n: &FluentLanguageLoader,
fields: impl IntoIterator<Item = InputInfo<'a, S>>,
) -> Markup {
html! {
fieldset class="cms-struct-container" {
(inputs(ctx, i18n, fields))
}
}
}
pub fn inputs<'a, S: ContextTrait>(
ctx: &FormRenderContext<'_, S>,
i18n: &FluentLanguageLoader,
inputs: impl IntoIterator<Item = InputInfo<'a, S>>,
) -> Markup {
html! {
@for f in inputs {
@let id = f.help.map(|_| Uuid::new_v4());
div class="cms-prop-container" {
label class="cms-prop-label" aria-describedby=[id] {(f.title)}
@if let Some(help) = f.help {
div role="tooltip" id=[id] {(help)}
}
(f.value.render_input(f.name, f.title, true, &ctx, i18n))
}
}
}
}
pub fn entity_list_page<E: Entity<S>, S: ContextTrait>(
ctx: State<S>,
i18n: &FluentLanguageLoader,
entities: impl IntoIterator<Item = impl Borrow<E>>,
) -> Markup {
document(html! {
(sidebar(&i18n, ctx.names_plural(), E::name_plural()))
main {
header class="cms-header" {
h1 {(E::name_plural().to_case(Case::Title))}
a href=(format!("/{}/add", (E::name_plural().to_case(Case::Kebab)))) class="cms-button" {
(fl!(i18n, "enitity-list-add"))
}
}
@for (i, c) in E::columns().iter().enumerate() {
@let i = i + 1;
@let id = format!("cms-list-column-filter-input-{i}");
input id=(id) class=("cms-list-column-filter-input") type="checkbox" checked[!c.hidden] {}
label for=(id) {
(c.name)
}
style {(PreEscaped(format!(r#"
#{id}:not(:checked) ~ .cms-entity-list .cms-list-column:nth-child({i}) {{
display: none;
}}
"#).trim()))}
}
table class="cms-entity-list" {
tr {
@for c in E::columns() {
th class="cms-list-column" {(c.name)}
}
th {}
}
@for e in entities {
@let e = e.borrow();
@let name = E::name().to_case(Case::Kebab);
@let id = e.id().to_string();
@let id = urlencoding::encode(&id);
@let row_id = Uuid::new_v4();
@let dialog_id = Uuid::new_v4();
tr id=(row_id) {
@for c in e.column_values() {
td class="cms-list-column" onclick=(format!(
"window.location = \"/{name}/{id}\"",
)) {
(c.render(i18n))
}
}
td
class="cms-list-column cms-list-delete-button"
onclick=(format!(r#"document.getElementById("{dialog_id}").showModal()"#))
{
"X"
}
(confirm_delete_modal(
i18n,
dialog_id,
&E::name().to_case(Case::Title),
format!(r#"
fetch("/api/v1/{name}/{id}", {{ method: "DELETE" }})
.then((r) => {{
if (!r.ok) return;
document.getElementById("{row_id}").remove();
document.getElementById("{dialog_id}").remove();
}})
"#).trim()
))
}
}
}
}
})
}
pub fn confirm_delete_modal(
i18n: &FluentLanguageLoader,
dialog_id: impl Display,
name: &str,
on_submit: impl Display,
) -> Markup {
html! {
dialog id=(dialog_id) class="cms-confirm-delete-modal" {
p {(fl!(i18n, "confirm-delete-modal", "title", name = name))}
form method="dialog" {
button {
(fl!(i18n, "confirm-delete-modal", "cancel"))
}
button onclick=(on_submit) {
(fl!(i18n, "confirm-delete-modal", "confirm"))
}
}
}
}
}
pub fn entity_page<E: Entity<S>, S: ContextTrait>(
State(ctx): State<S>,
i18n: &FluentLanguageLoader,
entity: Option<&E>,
) -> Markup {
document(html! {
(sidebar(i18n, ctx.names_plural(), E::name_plural()))
main {
h1 {(fl!(i18n, "edit-entity-title", name = E::name().to_case(Case::Title)))}
(entity_inputs::<E, S>(ctx, i18n, entity))
}
})
}
pub fn add_entity_page<E: Entity<S>, S: ContextTrait>(
State(ctx): State<S>,
i18n: &FluentLanguageLoader,
entity: Option<&E>,
) -> Markup {
document(html! {
(sidebar(i18n, ctx.names_plural(), E::name_plural()))
main {
h1 {(fl!(i18n, "create-entity-title", name = E::name().to_case(Case::Title)))}
(entity_inputs::<E, S>(ctx, i18n, entity))
}
})
}
pub fn input_enum<S: ContextTrait>(
ctx: &FormRenderContext<'_, S>,
i18n: &FluentLanguageLoader,
variants: &[EnumVariant<'_, S>],
selected: usize,
required: bool,
) -> Markup {
let id_type = Uuid::new_v4();
let id_data = Uuid::new_v4();
html! {
div class="cms-enum-type" id=(id_type) {
@for (i, variant) in variants.iter().enumerate() {
@let id = &format!("{}_radio-button_{}", variant.name, variant.value);
input
type="radio"
name=(variant.name)
value=(variant.value)
id=(id)
checked[i == selected]
onchange="cmsEnumInputOnchange(this)" {}
label for=(id) {(variant.title)}
}
}
div class="cms-enum-data" id=(id_data) {
@for (i, variant) in variants.iter().enumerate() {
@let class = match i.cmp(&selected) {
Ordering::Less => "cms-enum-container cms-enum-hidden cms-enum-hidden-left",
Ordering::Greater => "cms-enum-container cms-enum-hidden cms-enum-hidden-right",
Ordering::Equal => "cms-enum-container",
};
fieldset class=(class) disabled[i != selected] {
@if let Some(ref data) = variant.content {
(data.value.render_input(data.name, &variant.value.to_case(Case::Title), required, ctx, i18n))
}
}
}
}
script src="/js/enum.js" {}
}
}
pub fn error_page(title: &str, description: &str) -> Markup {
document(html! {
main {
h1 {(title)}
p {
@for line in description.split('\n') {
(line)
br;
}
}
a href="javascript:history.back()" {"Go Back"}
}
})
}