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
|
||||
/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"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[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
|
||||
|
||||
## 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() {
|
||||
println!("Hello, world!");
|
||||
use crate::tasks::tasks_controller;
|
||||
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