feat: UI overhaul
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 6s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 3s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 5s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 12s
GitLeaks check / GitLeaks check (pull_request) Successful in 11s
hadolint check / hadolint check (pull_request) Successful in 28s
markdownlint check / markdownlint check (pull_request) Successful in 52s
Prettier check / Prettier check (pull_request) Successful in 36s
htmlhint check / htmlhint check (pull_request) Successful in 55s
checkov check / checkov check (pull_request) Successful in 1m15s
Stylelint check / Stylelint check (pull_request) Successful in 31s
yamllint check / yamllint check (pull_request) Successful in 29s
ShellCheck check / ShellCheck check (pull_request) Successful in 1m9s
Rust check / Rust check (pull_request) Successful in 22m2s

This commit is contained in:
2026-01-28 19:36:00 +01:00
parent be1a21b746
commit b5e7ab5c48
41 changed files with 842 additions and 994 deletions

View File

@@ -1,11 +1,14 @@
use crate::components::button_primary::ButtonPrimary;
use crate::components::button_secondary::ButtonSecondary;
use crate::components::category_input::CategoryInput;
use crate::components::input::Input;
use crate::components::input_label::InputLabel;
use crate::components::project_select::ProjectSelect;
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
use crate::components::reoccurrence_interval_input::ReoccurrenceIntervalInput;
use crate::components::subtasks_form::SubtasksForm;
use crate::models::category::{CalendarTime, Category, Reoccurrence};
use crate::models::task::NewTask;
use crate::models::task::Task;
use crate::route::Route;
use crate::server::tasks::{create_task, delete_task, edit_task};
use chrono::Duration;
use dioxus::core_macro::{component, rsx};
@@ -13,8 +16,8 @@ use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::{
FaBell, FaBomb, FaClock, FaFloppyDisk, FaHourglassEnd, FaLayerGroup, FaList, FaPenClip,
FaRepeat, FaTrashCan,
FaBell, FaBomb, FaClock, FaFeatherPointed, FaHourglassEnd, FaList, FaRepeat, FaScroll, FaStamp,
FaTrashCan, FaXmark,
};
use dioxus_i18n::t;
use serde::{Deserialize, Serialize};
@@ -51,24 +54,19 @@ struct InputData {
project_id: Option<String>,
}
pub(crate) static TASK_BEING_EDITED: GlobalSignal<Option<Task>> = Signal::global(|| None);
pub(crate) static LATEST_VISITED_CATEGORY: GlobalSignal<Category> =
Signal::global(|| Category::Inbox);
#[component]
pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()>) -> Element {
let route = use_route::<Route>();
pub(crate) fn TaskForm() -> Element {
let navigator = use_navigator();
let task = TASK_BEING_EDITED();
let selected_category = use_signal(|| {
if let Some(task) = &task {
task.category.clone()
} else {
match route {
Route::CategorySomedayMaybePage => Category::SomedayMaybe,
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
Route::CategoryNextStepsPage => Category::NextSteps,
Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
date: chrono::Local::now().date_naive(),
reoccurrence: None,
time: None,
},
_ => Category::Inbox,
}
LATEST_VISITED_CATEGORY()
}
});
let category_calendar_reoccurrence_interval = use_signal(|| {
@@ -108,9 +106,9 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
rsx! {
div {
class: "p-4 flex flex-col gap-4",
class: "grow px-4 flex flex-col gap-6.5",
form {
class: "flex flex-col gap-4",
class: "flex flex-col gap-8",
id: "form_task",
onsubmit: move |event| {
event.prevent_default();
@@ -152,56 +150,45 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
project_id: input_data.project_id
.and_then(|deadline| deadline.parse().ok()).filter(|&id| id > 0),
};
if let Some(task) = task {
let _ = edit_task(task.id, new_task).await;
let result = if let Some(task) = task {
edit_task(task.id, new_task).await
} else {
let _ = create_task(new_task).await;
create_task(new_task).await
};
if result.is_ok() {
navigator.go_back();
}
on_successful_submit.call(());
}
},
div {
class: "flex flex-row items-center gap-3",
label {
InputLabel {
r#for: "input_title",
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaPenClip,
height: 16,
width: 16
}
icon: FaFeatherPointed
},
input {
Input {
class: "grow",
name: "title",
required: true,
initial_value: task.as_ref().map(|task| task.title.clone()),
r#type: "text",
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
},
autofocus: true
}
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_project",
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaList,
height: 16,
width: 16
}
InputLabel {
r#for: "input_project_id",
icon: FaList
},
SuspenseBoundary {
fallback: |_| {
rsx ! {
select {
class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow cursor-pointer",
class: "px-4 pt-3 pb-2.25 bg-gray-800-muted drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl grow cursor-pointer",
option {
value: 0,
{t!("none")}
},
value: 0
}
}
}
},
@@ -212,98 +199,69 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaBomb,
height: 16,
width: 16
}
InputLabel {
icon: FaBomb,
r#for: "input_deadline"
},
input {
Input {
name: "deadline",
initial_value: task.as_ref().and_then(|task| task.deadline)
.map(|deadline| deadline.format("%Y-%m-%d").to_string()),
r#type: "date",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0 cursor-pointer",
id: "input_deadline"
class: "grow basis-0"
}
},
div {
class: "flex flex-row items-center gap-3",
label {
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaLayerGroup,
height: 16,
width: 16
}
InputLabel {
icon: FaScroll
},
CategoryInput {
selected_category: selected_category,
class: "grow"
class: "grow",
selected_category: selected_category
}
}
match selected_category() {
Category::WaitingFor(waiting_for) => rsx! {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_waiting_for",
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaHourglassEnd,
height: 16,
width: 16
}
InputLabel {
icon: FaHourglassEnd,
r#for: "input_category_waiting_for",
},
input {
Input {
class: "grow",
name: "category_waiting_for",
required: true,
initial_value: waiting_for,
r#type: "text",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_waiting_for"
},
}
}
},
Category::Calendar { date, reoccurrence, time } => rsx! {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_category_calendar_date",
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaClock,
height: 16,
width: 16
}
InputLabel {
icon: FaClock,
r#for: "input_category_calendar_date"
},
div {
class: "grow flex flex-row gap-2",
input {
r#type: "date",
class: "grow grid grid-cols-2 gap-3",
Input {
class: "grow",
name: "category_calendar_date",
r#type: "date",
required: true,
initial_value: date.format("%Y-%m-%d").to_string(),
class:
"py-2 px-3 bg-zinc-800/50 rounded-lg grow cursor-pointer",
id: "input_category_calendar_date"
},
input {
r#type: "time",
Input {
class: "grow",
name: "category_calendar_time",
r#type: "time",
initial_value: time.map(|calendar_time|
calendar_time.time.format("%H:%M").to_string()
),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow cursor-pointer",
id: "input_category_calendar_time",
oninput: move |event| {
oninput: move |event: Event<FormData>| {
category_calendar_has_time.set(!event.value().is_empty());
}
}
@@ -311,60 +269,46 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reoccurrence_length",
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaRepeat,
height: 16,
width: 16
}
InputLabel {
icon: FaRepeat,
r#for: "category_calendar_reoccurrence_length"
},
div {
class: "grow grid grid-cols-6 gap-2",
class: "grow grid grid-cols-5 items-end gap-3",
ReoccurrenceIntervalInput {
reoccurrence_interval: category_calendar_reoccurrence_interval
},
input {
Input {
class: "text-right",
r#type: "number",
inputmode: "numeric",
name: "category_calendar_reoccurrence_length",
disabled: category_calendar_reoccurrence_interval().is_none(),
required: true,
min: 1,
min: "1",
initial_value: category_calendar_reoccurrence_interval().map_or(
String::new(),
|_| reoccurrence.map_or(1, |reoccurrence|
reoccurrence.length).to_string()
),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right",
id: "category_calendar_reoccurrence_length"
)
}
}
},
if category_calendar_has_time() {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reminder_offset_index",
class: "min-w-6 flex flex-row justify-center items-center",
Icon {
class: "text-zinc-400/50",
icon: FaBell,
height: 16,
width: 16
}
InputLabel {
r#for: "input_category_calendar_reminder_offset_index",
icon: FaBell
},
input {
r#type: "range",
class: "grow",
name: "category_calendar_reminder_offset_index",
r#type: "range",
min: 0,
max: REMINDER_OFFSETS.len() as i64 - 1,
initial_value: category_calendar_reminder_offset_index()
.to_string(),
class: "grow input-range-reverse cursor-pointer",
id: "category_calendar_has_reminder",
oninput: move |event| {
category_calendar_reminder_offset_index.set(
event.value().parse().unwrap()
@@ -372,8 +316,8 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
}
},
label {
r#for: "category_calendar_reminder_offset_index",
class: "pr-3 min-w-16 text-right",
r#for: "category_calendar_reminder_offset_index",
{REMINDER_OFFSETS[category_calendar_reminder_offset_index()]
.map(
|offset| if offset.num_hours() < 1 {
@@ -399,17 +343,20 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
}
}
}
div {
class: "flex flex-row justify-between mt-auto",
button {
r#type: "button",
class: "py-3 px-4 bg-zinc-300/50 rounded-lg cursor-pointer",
onclick: move |_| {
}
div {
class: "px-4 grid grid-cols-3 gap-3 mt-auto",
ButtonSecondary {
r#type: "button",
class: "grow",
onclick: {
let task = task.clone();
move |_| {
let task = task.clone();
async move {
if let Some(task) = task {
if let Category::Trash = task.category {
let _ = delete_task(task.id).await;
let result = if let Category::Trash = task.category {
delete_task(task.id).await
} else {
let new_task = NewTask {
title: task.title.to_owned(),
@@ -417,27 +364,53 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
category: Category::Trash,
project_id: task.project_id
};
let _ = edit_task(task.id, new_task).await;
edit_task(task.id, new_task).await.map(|_| ())
};
if result.is_ok() {
/* TODO: Might not work on mobile due to
https://dioxuslabs.com/learn/0.7/essentials/router/navigation#history-buttons.
*/
navigator.go_back();
}
} else {
navigator.go_back();
}
}
}
},
Icon {
icon: FaTrashCan,
height: 16,
width: 16
}
}
if task.is_some() {
div {
class: "grow flex flex-col items-stretch",
GoBackButton {
ButtonSecondary {
/* TODO: Replace w-full` with proper flexbox styling once
https://github.com/DioxusLabs/dioxus/issues/5269 is solved. */
class: "w-full",
r#type: "button",
Icon {
icon: FaXmark,
height: 16,
width: 16
}
on_successful_submit.call(());
}
},
Icon {
icon: FaTrashCan,
height: 16,
width: 16
}
}
button {
form: "form_task",
r#type: "submit",
class: "py-3 px-4 flex flex-row justify-center items-center bg-zinc-300/50 rounded-lg cursor-pointer",
Icon {
icon: FaFloppyDisk,
height: 16,
width: 16
}
} else {
div {}
}
ButtonPrimary {
form: "form_task",
r#type: "submit",
Icon {
icon: FaStamp,
height: 16,
width: 16
}
}
}