Compare commits

...
This repository has been archived on 2024-10-09. You can view files and clone it, but cannot push or open issues or pull requests.

15 Commits

18 changed files with 6639 additions and 2 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
/public/styles

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

2015
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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
View 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 :(" }
}
}

View File

@ -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
View File

@ -0,0 +1,3 @@
mod rendering;
pub mod tasks;
pub mod tasks_controller;

30
src/tasks/rendering.rs Normal file
View 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
View 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()
}

View 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;
}
}

View File

@ -0,0 +1,5 @@
use maud::Markup;
pub trait HtmlRenderable {
fn to_html(&self) -> Markup;
}

1
src/traits/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod html_renderable;

33
styles/global.css Normal file
View 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

File diff suppressed because it is too large Load Diff