Compare commits
15 Commits
main
...
create-tas
Author | SHA1 | Date | |
---|---|---|---|
f3fbf37e4d | |||
ade4a86a74 | |||
e1d575bce9 | |||
e79797fe5f | |||
5adba14aeb | |||
2b1ddc0715 | |||
0d61c21748 | |||
10ad94b17b | |||
b34dbde83e | |||
3fe5c4477b | |||
95fb2e093e | |||
251822698c | |||
02b3d94d45 | |||
50688c6fbe | |||
d8fb60f23c |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/public/styles
|
||||||
|
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
2015
Cargo.lock
generated
Normal file
2015
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@ -2,7 +2,24 @@
|
|||||||
name = "clego-app"
|
name = "clego-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
axum = "0.7.5"
|
||||||
|
fake = { version = "2.9.2", features = ["derive"] }
|
||||||
|
maud = { version = "0.26.0", features = ["axum"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
strum = "0.26.2"
|
||||||
|
strum_macros = "0.26.2"
|
||||||
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
|
tower = "0.4.13"
|
||||||
|
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
lightningcss = "1.0.0-alpha.55"
|
||||||
|
grass = "0.13.2"
|
||||||
|
30
README.md
30
README.md
@ -1 +1,31 @@
|
|||||||
# clego-app
|
# clego-app
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Rust: 🦀
|
||||||
|
- Axum: for serving static assets and powering the backend API
|
||||||
|
- htmx: for reactivity on the UI
|
||||||
|
- Maud: HTML templating (debatable, we may want to switch to askama)
|
||||||
|
- lightningcss: bundle and minify css
|
||||||
|
|
||||||
|
## Font choice
|
||||||
|
|
||||||
|
Use default system fonts to enhance native feel as well as lower size, following [Modern Font Stacks](https://github.com/system-fonts/modern-font-stacks)
|
||||||
|
|
||||||
|
## Commit messages
|
||||||
|
|
||||||
|
See [link](https://www.conventionalcommits.org/fr/v1.0.0/)
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
For automatic rebuilds on change install cargo-watch:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install cargo-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
Then to run it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo watch -c run
|
||||||
|
```
|
||||||
|
32
build.rs
Normal file
32
build.rs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use lightningcss::bundler::{Bundler, FileProvider};
|
||||||
|
use lightningcss::printer::PrinterOptions;
|
||||||
|
use lightningcss::stylesheet::{MinifyOptions, ParserOptions};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let input_path = Path::new("styles/global.css");
|
||||||
|
let output_path = Path::new("public/styles/global.css");
|
||||||
|
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fs = FileProvider::new();
|
||||||
|
let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
|
||||||
|
|
||||||
|
let mut stylesheet = bundler.bundle(input_path).unwrap();
|
||||||
|
let _ = stylesheet.minify(MinifyOptions::default());
|
||||||
|
let minified_css = stylesheet.to_css(PrinterOptions {
|
||||||
|
minify: true,
|
||||||
|
..PrinterOptions::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut output_file = File::create(output_path)?;
|
||||||
|
output_file.write_all(minified_css.unwrap().code.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
1
public/htmx.min.js
vendored
Normal file
1
public/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
src/base.rs
Normal file
20
src/base.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use maud::{html, Markup, DOCTYPE};
|
||||||
|
|
||||||
|
pub fn header() -> Markup {
|
||||||
|
html! {
|
||||||
|
(DOCTYPE)
|
||||||
|
head {
|
||||||
|
meta charset="utf-8";
|
||||||
|
title class="title" { "Clego" }
|
||||||
|
script src="/public/htmx.min.js" {};
|
||||||
|
link rel="stylesheet" href="/public/styles/global.css";
|
||||||
|
link rel="icon" href="/public/favicon.ico";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error_tmpl() -> Markup {
|
||||||
|
html! {
|
||||||
|
h1 { "Something went terribly wrong :(" }
|
||||||
|
}
|
||||||
|
}
|
45
src/main.rs
45
src/main.rs
@ -1,3 +1,44 @@
|
|||||||
fn main() {
|
use crate::tasks::tasks_controller;
|
||||||
println!("Hello, world!");
|
use axum::{routing::get, Router};
|
||||||
|
use maud::{html, Markup};
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use tracing::info;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
mod base;
|
||||||
|
mod tasks;
|
||||||
|
mod traits;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| "clego_app=debug".into()),
|
||||||
|
)
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("initializing router...");
|
||||||
|
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/", get(hello))
|
||||||
|
.nest("/tasks", tasks_controller::router())
|
||||||
|
.nest_service("/public", ServeDir::new("public"));
|
||||||
|
let port = 3000_u16;
|
||||||
|
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
info!("router initialized, now listening on port {}", port);
|
||||||
|
axum::serve(listener, router).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn hello() -> Markup {
|
||||||
|
html! {
|
||||||
|
(base::header())
|
||||||
|
main class="content" #content {
|
||||||
|
p hx-get="/tasks" hx-trigger="load" hx-swap="outerHTML" { "Loading..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
src/tasks/mod.rs
Normal file
3
src/tasks/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod rendering;
|
||||||
|
pub mod tasks;
|
||||||
|
pub mod tasks_controller;
|
30
src/tasks/rendering.rs
Normal file
30
src/tasks/rendering.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use crate::traits::html_renderable::HtmlRenderable;
|
||||||
|
use maud::{html, Markup};
|
||||||
|
|
||||||
|
use super::tasks::{TaskPriority, TaskStatus};
|
||||||
|
|
||||||
|
impl HtmlRenderable for TaskPriority {
|
||||||
|
fn to_html(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
i class=(format!("priority-icon {}", match self {
|
||||||
|
TaskPriority::Low => "icon-low",
|
||||||
|
TaskPriority::Medium => "icon-medium",
|
||||||
|
TaskPriority::High => "icon-high",
|
||||||
|
})) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlRenderable for TaskStatus {
|
||||||
|
fn to_html(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
i class=(format!("status-icon {}", match self {
|
||||||
|
TaskStatus::Backlog => "icon-backlog",
|
||||||
|
TaskStatus::Todo => "icon-todo",
|
||||||
|
TaskStatus::InProgress => "icon-in-progress",
|
||||||
|
TaskStatus::Done => "icon-done",
|
||||||
|
TaskStatus::Canceled => "icon-canceled",
|
||||||
|
})) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
src/tasks/tasks.rs
Normal file
111
src/tasks/tasks.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use fake::{faker::lorem::fr_fr::Sentence, Fake};
|
||||||
|
use rand::{
|
||||||
|
distributions::{Distribution, Standard},
|
||||||
|
Rng,
|
||||||
|
};
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use strum_macros::{self, EnumIter, EnumString};
|
||||||
|
|
||||||
|
#[allow(unused_imports)] // trait into scope
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Task {
|
||||||
|
pub id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub label: TaskLabel,
|
||||||
|
pub status: TaskStatus,
|
||||||
|
pub priority: TaskPriority,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, EnumString, EnumIter, strum_macros::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum TaskLabel {
|
||||||
|
Bug,
|
||||||
|
Feature,
|
||||||
|
Documentation,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, EnumString, EnumIter, strum_macros::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum TaskStatus {
|
||||||
|
Backlog,
|
||||||
|
Todo,
|
||||||
|
InProgress,
|
||||||
|
Done,
|
||||||
|
Canceled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, EnumString, EnumIter, strum_macros::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum TaskPriority {
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<TaskLabel> for Standard {
|
||||||
|
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> TaskLabel {
|
||||||
|
match rng.gen_range(0..3) {
|
||||||
|
0 => TaskLabel::Bug,
|
||||||
|
1 => TaskLabel::Feature,
|
||||||
|
_ => TaskLabel::Documentation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<TaskStatus> for Standard {
|
||||||
|
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> TaskStatus {
|
||||||
|
match rng.gen_range(0..5) {
|
||||||
|
0 => TaskStatus::Backlog,
|
||||||
|
1 => TaskStatus::Todo,
|
||||||
|
2 => TaskStatus::InProgress,
|
||||||
|
3 => TaskStatus::Done,
|
||||||
|
_ => TaskStatus::Canceled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<TaskPriority> for Standard {
|
||||||
|
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> TaskPriority {
|
||||||
|
match rng.gen_range(0..3) {
|
||||||
|
0 => TaskPriority::Low,
|
||||||
|
1 => TaskPriority::Medium,
|
||||||
|
_ => TaskPriority::High,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<Task> for Standard {
|
||||||
|
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Task {
|
||||||
|
Task {
|
||||||
|
id: (1000..2000).fake(),
|
||||||
|
title: Sentence(10..20).fake(),
|
||||||
|
label: rng.gen(),
|
||||||
|
status: rng.gen(),
|
||||||
|
priority: rng.gen(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TASKS: OnceLock<Mutex<Vec<Task>>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub async fn all() -> Option<Vec<Task>> {
|
||||||
|
Some(
|
||||||
|
TASKS
|
||||||
|
.get_or_init(|| Mutex::new((0..100).map(|_| rand::random::<Task>()).collect()))
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn by_id(id: i32) -> Option<Task> {
|
||||||
|
TASKS
|
||||||
|
.get_or_init(|| Mutex::new((0..100).map(|_| rand::random::<Task>()).collect()))
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|task| task.id == id)
|
||||||
|
.cloned()
|
||||||
|
}
|
69
src/tasks/tasks_controller.rs
Normal file
69
src/tasks/tasks_controller.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use crate::base;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::{extract::Path, routing::get};
|
||||||
|
use maud::{html, Markup};
|
||||||
|
|
||||||
|
use super::tasks::{self, Task};
|
||||||
|
|
||||||
|
pub fn router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(index))
|
||||||
|
.route("/:id", get(task))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index() -> Markup {
|
||||||
|
let tasks = tasks::all().await;
|
||||||
|
|
||||||
|
match tasks {
|
||||||
|
Some(tasks) => index_tmpl(tasks),
|
||||||
|
None => base::error_tmpl(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_tmpl(tasks: Vec<Task>) -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="table-container" {
|
||||||
|
table class="table is-hoverable is-fullwidth" {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { "Task" }
|
||||||
|
th { "Title" }
|
||||||
|
th { "Status" }
|
||||||
|
th { "Priority" }
|
||||||
|
th {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for task in &tasks{
|
||||||
|
tr {
|
||||||
|
th { (task.id) }
|
||||||
|
td { span class="tag is-black" { (task.label) } (task.title) }
|
||||||
|
td { (task.status) }
|
||||||
|
td { (task.priority) }
|
||||||
|
td { button hx-get={ (format!("/tasks/{}", task.id)) } hx-push-url="true" hx-target="closest main" { "Read more" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn task(Path(id): Path<i32>) -> Markup {
|
||||||
|
let task = tasks::by_id(id).await;
|
||||||
|
|
||||||
|
match task {
|
||||||
|
Some(task) => task_tmpl(task),
|
||||||
|
None => base::error_tmpl(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn task_tmpl(task: Task) -> Markup {
|
||||||
|
html! {
|
||||||
|
h2 { (task.title) }
|
||||||
|
button hx-get="/tasks" hx-target="closest main" {
|
||||||
|
"Go back"
|
||||||
|
}
|
||||||
|
hr;
|
||||||
|
}
|
||||||
|
}
|
5
src/traits/html_renderable.rs
Normal file
5
src/traits/html_renderable.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
use maud::Markup;
|
||||||
|
|
||||||
|
pub trait HtmlRenderable {
|
||||||
|
fn to_html(&self) -> Markup;
|
||||||
|
}
|
1
src/traits/mod.rs
Normal file
1
src/traits/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod html_renderable;
|
33
styles/global.css
Normal file
33
styles/global.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@import "missing.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--main-font: "system-ui, sans-serif";
|
||||||
|
--secondary-font: var(--main-font);
|
||||||
|
--mono-font: "ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
@use "pico" with (
|
||||||
|
$theme-color: "sand",
|
||||||
|
$enable-semantic-container: true,
|
||||||
|
$enable-viewport: false,
|
||||||
|
$enable-classes: false
|
||||||
|
);
|
||||||
|
|
||||||
|
@use "bulma/sass/utilities" with (
|
||||||
|
$family-sans-serif: "system-ui, sans-serif",
|
||||||
|
$family-monospace:
|
||||||
|
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace"
|
||||||
|
);
|
||||||
|
|
||||||
|
@forward "bulma/sass/base";
|
||||||
|
@forward "bulma/sass/components/navbar";
|
||||||
|
@forward "bulma/sass/elements/title";
|
||||||
|
@forward "bulma/sass/elements/content";
|
||||||
|
@forward "bulma/sass/elements/table";
|
||||||
|
@forward "bulma/sass/elements/tag";
|
||||||
|
@forward "bulma/sass/layout/hero";
|
||||||
|
@forward "bulma/sass/layout/container";
|
||||||
|
|
||||||
|
@forward "bulma/sass/themes";
|
||||||
|
*/
|
4225
styles/missing.css
Normal file
4225
styles/missing.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user