Author SHA1 Message Date
schn33fuchs b404ff5bc9 Path
Fixed layout issues and joke message
2026-06-06 18:31:05 +02:00
schn33fuchs c3b7321005 Styling
The ticket backgrounds have color depending on background and some
improvements
2026-06-06 18:18:22 +02:00
TheDiluTek dfb031949b view tickets 2026-06-06 17:25:12 +02:00
TheDiluTek b030e133a2 Merge branch 'main' of https://git.raffauf-clan.de/schn33fuchs/ticketsystem 2026-06-06 17:01:58 +02:00
schn33fuchs 810d554cb9 Fixed bug
You could set the password to be an empty string
2026-06-05 21:35:54 +02:00
TheDiluTek 2e6bd2a781 bisschen sidebar schön. problem gefunden: es reicht wenn man benutzernamen eingibt. man logt sich auch ohne passwort ein 2026-06-04 21:48:47 +02:00
schn33fuchs 390630886e Merge pull request 'Icon' (#42) from Icon into main
Reviewed-on: #42
2026-06-03 16:57:13 +02:00
schn33fuchs 0ee20e2833 Icon in the corner 2026-06-03 16:10:37 +02:00
schn33fuchs b7f8df624c Favion
Its there now
2026-06-03 15:53:57 +02:00
schn33fuchs 8a9c9efbcc Icons 2026-06-03 15:52:10 +02:00
schn33fuchs c31375573f .gitignore
update
2026-06-03 15:48:52 +02:00
TheDiluTek b2daed8e99 bug fixes 2026-06-03 15:13:09 +02:00
TheDiluTek 076bd6c545 mehr von login seite gemacht, sah voll gut aus aber hab irgendwas kaputt gemacht und finds nicht 2026-05-30 23:45:39 +02:00
TheDiluTek a42897de9f anmeldung überarbeitet, login braucht noch mehr gap als die anderen inputs haben (command?) 2026-05-30 14:03:25 +02:00
TheDiluTek 3154e3de44 login schön gemacht 2026-05-30 13:29:41 +02:00
schn33fuchs bf0197adae .gitignore 2026-05-13 16:58:44 +02:00
schn33fuchs c6a4a24fb6 Sam did something 2026-05-13 13:31:56 +02:00
schn33fuchs 52387f7333 Fun times 2026-05-13 12:18:55 +02:00
schn33fuchs 6eb84d24e0 Archive
There is now an archive for tickets
2026-05-11 20:46:06 +02:00
schn33fuchs d9ef5746a2 Home page and sidebar update
Home page now shows who is logged in and the sidebar has a button to
home
2026-05-11 13:03:00 +02:00
schn33fuchs 535d940857 Removed unneeded imports 2026-05-10 10:26:16 +02:00
schn33fuchs 33df619c8d Cargo doc fixes
It's good now
2026-05-10 10:18:45 +02:00
28 changed files with 444 additions and 115 deletions
+2 -1
View File
@@ -20,9 +20,10 @@ frontend/node_modules/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
# Added by cargo
/target
.antigravitycli/
Generated
+4
View File
@@ -2777,6 +2777,10 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "ticketsystem"
version = "0.1.0"
[[package]]
name = "time"
version = "0.3.47"
+8
View File
@@ -1,3 +1,11 @@
[package]
name = "ticketsystem"
version = "0.1.0"
edition = "2021"
readme = "README.md"
description = "A ticket system with backend and frontend components"
publish = false
[workspace]
members = ["backend", "frontend"]
resolver = "2"
+6
View File
@@ -1,2 +1,8 @@
# Ticketsystem
A ticket system with backend and frontend components.
## Components
- **[Backend](../backend/index.html)** - The server-side API and business logic
- **[Frontend](../frontend/index.html)** - The client-side user interface
+1 -1
View File
@@ -1,5 +1,5 @@
use axum::{Json, http::StatusCode};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, decode, encode};
use serde::{Deserialize, Serialize};
use crate::models::Claims;
+1 -6
View File
@@ -12,12 +12,7 @@ use axum_extra::extract::CookieJar;
use jsonwebtoken::DecodingKey;
use serde_json::json;
use crate::{
AppState,
cookie::jwt::decode_token,
handlers::auth::filter_user,
models::{LoginScheme, User},
};
use crate::{AppState, cookie::jwt::decode_token, handlers::auth::filter_user, models::User};
/// Axum middleware to validate a JWT token present in cookies or Authorization header.
///
+23 -9
View File
@@ -1,4 +1,4 @@
use std::{sync::Arc, usize};
use std::sync::Arc;
use argon2::{
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
@@ -11,7 +11,6 @@ use axum::{
response::IntoResponse,
};
use axum_extra::extract::cookie::{Cookie, SameSite};
use chrono::format;
use jsonwebtoken::{EncodingKey, Header};
use serde_json::json;
@@ -276,7 +275,7 @@ pub async fn get_current_user(
/// - `404 Not Found` if user doesn't exist
/// - `500 Internal Server Error` if database error occurs
pub async fn delete_user(
Path(id): Path<i32>,
Path(id): Path<i16>,
State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let query = sqlx::query(r#"DELETE FROM users WHERE id = $1"#)
@@ -415,10 +414,11 @@ pub async fn get_user_by_id(
/// - Passwords are hashed using Argon2 before storage.
/// - This endpoint typically requires admin privileges (enforced by middleware).
pub async fn update_user(
Path(id): Path<i32>,
Path(id): Path<i16>,
State(data): State<Arc<AppState>>,
Json(body): Json<UserUpdateScheme>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let update_result = if !body.new_pwd.is_empty() {
let argon = Argon2::default();
let salt = SaltString::generate(&mut OsRng);
let hashed_pwd = match argon.hash_password(body.new_pwd.clone().as_bytes(), &salt) {
@@ -426,15 +426,25 @@ pub async fn update_user(
Err(e) => panic!("Error hashing {:}", e),
};
let update_result = sqlx::query(r#"UPDATE users SET first_name = $1, last_name = $2, username = $3, pwd = $4, is_admin = $5 WHERE id = $6"#)
sqlx::query(r#"UPDATE users SET first_name = $1, last_name = $2, username = $3, pwd = $4, is_admin = $5 WHERE id = $6"#)
.bind(body.first_name.to_owned())
.bind(body.last_name.to_owned())
.bind(body.username.to_owned())
.bind(&hashed_pwd)
.bind(hashed_pwd)
.bind(body.make_admin.to_owned())
.bind(id)
.execute(&data.db)
.await
} else {
sqlx::query(r#"UPDATE users SET first_name = $1, last_name = $2, username = $3, is_admin = $4 WHERE id = $5"#)
.bind(body.first_name.to_owned())
.bind(body.last_name.to_owned())
.bind(body.username.to_owned())
.bind(body.make_admin.to_owned())
.bind(id)
.execute(&data.db)
.await
}
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -485,7 +495,8 @@ pub async fn update_user(
pub async fn check_admin_exists(
State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let admin_count = sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM users WHERE is_admin = true"#)
let admin_count =
sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM users WHERE is_admin = true"#)
.fetch_one(&data.db)
.await
.map_err(|e| {
@@ -528,7 +539,8 @@ pub async fn setup_initial_admin(
Json(request): Json<UserCreateScheme>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
// Check if any admin already exists
let admin_count = sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM users WHERE is_admin = true"#)
let admin_count =
sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM users WHERE is_admin = true"#)
.fetch_one(&data.db)
.await
.map_err(|e| {
@@ -580,7 +592,9 @@ pub async fn setup_initial_admin(
Json(json!({"status": "error", "message": "Error creating admin user"})),
));
} else {
Ok(Json(json!({"status": "success", "result": "Admin user created"})))
Ok(Json(
json!({"status": "success", "result": "Admin user created"}),
))
}
}
+1 -1
View File
@@ -133,7 +133,7 @@ pub async fn get_tickets(
r#"SELECT t.id, t.category, t.betreff, t.description, t.room, t.status, t.date, t.user_id, u.first_name, u.last_name
FROM tickets t
LEFT JOIN users u ON t.user_id = u.id
WHERE t.status <> 'Archived' ORDER BY t.date DESC"#,
ORDER BY t.date DESC"#,
)
.fetch_all(&data.db)
.await
+1 -9
View File
@@ -1,5 +1,3 @@
#![allow(unused_imports)]
/// Cookie and JWT authentication utilities
mod cookie;
/// Environment configuration loading
@@ -13,18 +11,12 @@ mod router;
use std::sync::Arc;
use axum::{
Router,
http::{
use axum::http::{
HeaderValue, Method,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
},
routing,
};
use dotenv::dotenv;
use models::*;
use router::create_router;
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, postgres::PgPoolOptions};
use tower_http::cors::CorsLayer;
-3
View File
@@ -1,7 +1,4 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use sqlx::{Decode, prelude::Type};
/// API response for a ticket with user information.
///
+2
View File
@@ -4,6 +4,8 @@
<head>
<link data-trunk rel="rust" data-bin="bin" />
<link data-trunk rel="scss" href="src/styles/main.scss" />
<link data-trunk rel="icon" href="src/assets/favicon.ico" type="image/x-icon" />
<link data-trunk rel="copy-dir" href="src/assets" />
<meta charset="utf-8" />
<title>Yew App</title>
</head>
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

+11
View File
@@ -25,6 +25,9 @@ enum Route {
/// Route for viewing all tickets.
#[at("/tickets")]
AllTickets,
/// Route for viewing archived tickets.
#[at("/tickets/archive")]
ArchivedTickets,
/// Route for user registration.
#[at("/register")]
Register,
@@ -206,6 +209,13 @@ fn switch(route: Route) -> Html {
</SidebarShell>
</ProtectedRoute>
},
Route::ArchivedTickets => html! {
<ProtectedRoute admin_page={true}>
<SidebarShell>
<ticket::ArchivedTickets/>
</SidebarShell>
</ProtectedRoute>
},
Route::Register => html! {
<ProtectedRoute admin_page={true}>
<SidebarShell>
@@ -259,6 +269,7 @@ fn switch(route: Route) -> Html {
pub fn app() -> Html {
html! {
<BrowserRouter>
<basic_pages::Icon/>
<Switch<Route> render={switch} />
</BrowserRouter>
}
+27 -16
View File
@@ -3,6 +3,12 @@ use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
macro_rules! dequote {
($str:expr) => {
$str.trim_matches('"').to_string()
};
}
/// The main home page component of the application.
///
/// This component displays different content based on whether the logged-in user
@@ -22,10 +28,9 @@ use yew_router::prelude::*;
/// ```
#[component(Home)]
pub fn home_component() -> Html {
let is_admin = use_state(|| None::<bool>);
let name = use_state(|| "".to_string());
{
let is_admin = is_admin.clone();
let name = name.clone();
use_effect_with((), move |_| {
spawn_local(async move {
let response = Request::get("/api/users/current")
@@ -36,28 +41,27 @@ pub fn home_component() -> Html {
match response {
Ok(resp) if resp.status() == 200 => {
let user_data: serde_json::Value = resp.json().await.unwrap_or_default();
let admin_value = user_data["data"]["is_admin"].as_bool();
is_admin.set(admin_value);
let name_value = format!(
"{} {}",
dequote!(user_data["data"]["first_name"].to_string()),
dequote!(user_data["data"]["last_name"].to_string())
);
name.set(name_value);
}
_ => is_admin.set(Some(false)),
_ => name.set("Unknown".to_string()),
}
});
|| ()
});
}
match *is_admin {
None => html! { <div>{ "Loading..." }</div> },
Some(true) => html! {
<div>
html! {
<div class="rundbg">
<crate::utilities::TicketCount/>
<p>{ "You are logged in as: " }</p>
<p>{ &*name }</p>
</div>
},
Some(false) => html! {
<div>
<crate::utilities::TicketCount/>
</div>
},
}
}
@@ -105,3 +109,10 @@ pub fn denied_component() -> Html {
</div>
}
}
#[component(Icon)]
pub fn icon_component() -> Html {
html! {
<img src="assets/csg.png" class="csg-icon" />
}
}
-1
View File
@@ -11,4 +11,3 @@ pub mod sidebar;
pub mod ticket;
pub mod user;
pub mod utilities;
+3
View File
@@ -381,6 +381,7 @@ pub fn sidebar() -> Html {
<SidebarStateProvider>
<nav class="sidebar user">
<ul>
<Link<crate::Route> to={crate::Route::Home}>{ "󰟒" }</Link<crate::Route>>
<TicketMenu/>
<li class="logout-item">
<button
@@ -400,9 +401,11 @@ pub fn sidebar() -> Html {
<SidebarStateProvider>
<nav class="sidebar admin">
<ul>
<Link<crate::Route> to={crate::Route::Home}>{ "󰟒" }</Link<crate::Route>>
<TicketMenu/>
<UsersMenu/>
<Link<crate::Route> to={crate::Route::Diagnostics}>{ "Statistiken" }</Link<crate::Route>>
<Link<crate::Route> to={crate::Route::ArchivedTickets}>{ "Archiv" }</Link<crate::Route>>
<li class="logout-item">
<button
class="logout-button"
+145 -3
View File
@@ -287,7 +287,7 @@ pub fn submit_ticket_component() -> Html {
let room_valid = (*room).is_some();
html! {
<form {onsubmit}>
<form class="rundbg" {onsubmit}>
<label>{ "Betreff:" }
<input type="text" value={(*betreff).clone()} oninput={betreff_change}/>
</label>
@@ -658,6 +658,149 @@ pub fn all_tickets_component() -> Html {
});
}
if *loading {
html! {<p>{ "Loading" }</p>}
} else if let Some(e) = &*error {
html! { <p>{ format!("Error: {}", e) }</p> }
} else {
html! {
<ul class= "postits">
{ for tickets.iter().filter(|t| t.status != "Archived" && (if user.is_admin { true } else if let Some(uid) = user.id { t.user_id == uid } else { false })).map(|t| {
let status_class = match t.status.as_str() {
"ToDo" => "To-Do",
"InProgress" => "InProgress",
"Completed" => "Completed",
"Archived" => "Archived",
_ => "To-Do"
};
html! {
<div class={classes!{status_class, "listbox"}}>
<li key={t.id.to_string()}>
<Link<crate::Route> to={crate::Route::TicketById{id: t.id}}><h3>{ format!("{} - #{}", t.betreff, t.id) }</h3></Link<crate::Route>>
<p>{ &t.description }</p>
<p>{ match t.status.as_str() {
"ToDo" => "Zu tun",
"InProgress" => "In Bearbeitung",
"Completed" => "Erledigt",
"Archived" => "Archiviert",
_ => "Ungültiger Status"
}}</p>
</li>
</div>
}})}
<Link<crate::Route> to={crate::Route::Ticket}>{ "Zurück zur Startseite" }</Link<crate::Route>>
</ul>
}
}
}
/// A component for fetching and displaying a list of archived tickets.
///
/// This component retrieves all tickets from the backend and presents them as a list,
/// filtered to only show those with "Archived" status.
///
/// # State
/// Uses `use_state` hooks to manage:
/// - `tickets`: A vector of `Ticket` structs to store the fetched tickets.
/// - `error`: Any error message encountered during API calls.
/// - `loading`: A boolean indicating if data is currently being fetched.
/// - `user`: An `ActiveUser` struct holding the current user's ID and admin status.
///
/// # Functionality
/// - **Fetch Tickets**: On component mount, fetches all tickets from `/api/tickets`.
/// - **Fetch Current User**: Concurrently fetches the current user's details from
/// `/api/users/current` to determine their `user_id` and `is_admin` status.
/// - **Conditional Display**:
/// - If `loading` is true, displays "Loading...".
/// - If an `error` occurs, displays the error message.
/// - Otherwise, renders a list of tickets.
/// - **Filtering**:
/// - Only tickets with `t.status == "Archived"` are displayed.
/// - If the user is an admin, all archived tickets are shown.
/// - If the user is not an admin, only their own archived tickets are shown.
/// - **Navigation**: Each ticket in the list is a link to [`crate::Route::TicketById`]
/// for viewing individual ticket details.
///
/// # Example
/// ```rust
/// html! {
/// <ArchivedTickets />
/// }
/// ```
#[component(ArchivedTickets)]
pub fn archived_tickets_component() -> Html {
let tickets = use_state(|| Vec::<Ticket>::new());
let error = use_state(|| None::<String>);
let loading = use_state(|| false);
let user = use_state(|| ActiveUser {
id: None,
is_admin: false,
});
{
let tickets = tickets.clone();
let error = error.clone();
let loading = loading.clone();
use_effect_with((), move |_| {
loading.set(true);
spawn_local(async move {
let url = format!("/api/tickets");
match Request::get(&url).send().await {
Ok(response) if response.status() == 200 => {
match response.json::<Vec<Ticket>>().await {
Ok(t) => tickets.set(t),
Err(e) => error.set(Some(format!("parse error: {}", e))),
}
}
Ok(response) => {
if let Ok(text) = response.text().await {
error.set(Some(text));
} else {
error.set(Some(format!("status {}", response.status())));
}
}
Err(err) => error.set(Some(format!("Network error: {}", err))),
}
loading.set(false);
});
|| ()
});
}
{
let user = user.clone();
use_effect_with((), move |_| {
let user = user.clone();
spawn_local(async move {
if let Ok(response) = Request::get("/api/users/current")
.credentials(web_sys::RequestCredentials::Include)
.send()
.await
{
if response.status() == 200 {
if let Ok(json) = response.json::<serde_json::Value>().await {
let id = json
.get("data")
.and_then(|d| d.get("id"))
.and_then(|v| v.as_i64())
.and_then(|n| i16::try_from(n).ok());
let is_admin = json
.get("data")
.and_then(|d| d.get("is_admin"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
user.set(ActiveUser { id, is_admin });
}
}
}
});
|| ()
});
}
if *loading {
html! {<p>{ "Loading" }</p>}
} else if let Some(e) = &*error {
@@ -665,7 +808,7 @@ pub fn all_tickets_component() -> Html {
} else {
html! {
<ul>
{ for tickets.iter().filter(|t| if user.is_admin { true } else if let Some(uid) = user.id { t.user_id == uid } else { false }).map(|t| html! {
{ for tickets.iter().filter(|t| t.status == "Archived" && (user.is_admin || if let Some(uid) = user.id { t.user_id == uid } else { false })).map(|t| html! {
<div>
<li key={t.id.to_string()}>
<Link<crate::Route> to={crate::Route::TicketById{id: t.id}}><h3>{ format!("{} - #{}", t.betreff, t.id) }</h3></Link<crate::Route>>
@@ -681,7 +824,6 @@ pub fn all_tickets_component() -> Html {
</div>
})}
<Link<crate::Route> to={crate::Route::Ticket}>{ "Zurück zur Startseite" }</Link<crate::Route>>
</ul>
}
}
+9 -4
View File
@@ -222,7 +222,7 @@ pub fn register_component() -> Html {
};
html! {
<form {onsubmit}>
<form {onsubmit} class="rundbg">
<label>{ "Vorname:" }
<input type="text" value={(*first_name).clone()} oninput={fn_change}/>
</label>
@@ -328,7 +328,9 @@ pub fn login_component() -> Html {
};
html! {
<form {onsubmit}>
<div>
<h1 class="headline">{ "Anmeldung" }</h1>
<form {onsubmit} class="rundbg login">
<input
placeholder="username"
value={(*username).clone()}
@@ -349,6 +351,7 @@ pub fn login_component() -> Html {
<button type="submit" disabled={*loading}>{ if *loading { "Logging in..." } else { "Login" } }</button>
if !error.is_empty() { <p style="color:red">{(*error).clone()}</p> }
</form>
</div>
}
}
@@ -422,9 +425,9 @@ pub fn all_users_component() -> Html {
html! { <p>{ format!("Error: {}", e) }</p> }
} else {
html! {
<ul>
<ul class="posti8s">
{ for users.iter().map(|t| html! {
<li key={t.id.to_string()}>
<li key={t.id.to_string()} class="listbox">
<Link<crate::Route> to={crate::Route::UserByID{id: t.id}}><h3>{ format!("{} {}- #{}", t.first_name, t.last_name, t.id) }</h3></Link<crate::Route>>
</li>
})}
@@ -563,6 +566,7 @@ pub fn user_by_id_component(props: &UserProps) -> Html {
let last_name = (*last_name).clone();
let username = (*username).clone();
let make_admin = *make_admin;
let new_pwd_state = new_pwd.clone();
let new_pwd = (*new_pwd).clone();
saving.set(true);
@@ -597,6 +601,7 @@ pub fn user_by_id_component(props: &UserProps) -> Html {
if let Ok(updated) = resp.json::<FilteredUser>().await {
user_state.set(Some(updated));
}
new_pwd_state.set(String::new());
save_success.set(true);
}
Ok(resp) => {
+2 -2
View File
@@ -1,8 +1,8 @@
$color-bg: #ffffff;
$color-sidebar: #0f172a;
$color-sidebar: #570000;
$color-accent: #2563eb;
$color-muted: #6b7280;
$spacing-sm: 8px;
$spacing-md: 16px;
$border-radius: 6px;
$rundbgbackgroundcolor: #a0a0a0;
@@ -130,7 +130,7 @@
.room-bar {
height: 100%;
background-color: #f97316;
background-color: #570000;
transition: width 0.3s ease;
}
}
@@ -0,0 +1,7 @@
.headline {
font-size: 40px;
font-weight: bold;
text-align: center;
color: #570000;
margin-top: 150px;
}
+17
View File
@@ -0,0 +1,17 @@
.csg-icon {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
width: auto;
height: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
img {
width: auto;
height: auto;
}
}
+9 -7
View File
@@ -4,24 +4,26 @@
.sidebar {
width: 260px;
background: $color-sidebar;
color: #fff;
color: #fffefe;
min-height: 100%;
padding: $spacing-md;
display: flex;
flex-direction: column;
ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: $spacing-sm; flex: 1; }
a, .menu-toggle {
color: #fff;
color: #ffffff;
text-decoration: none;
display: block;
padding: 8px 12px;
border-radius: 4px;
&:hover { background: rgba(255,255,255,0.04); }
&:hover { background: rgba(158, 45, 1, 0.842); }
}
.menu-toggle { background: transparent; border: none; text-align: left; width: 100%; cursor: pointer; }
.menu-toggle { background: #ff5a316c; border: none; text-align: left; width: 100%; cursor: pointer; }
.submenu {
margin-left: 8px;
@@ -38,9 +40,9 @@
}
.logout-button {
background: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
background: rgba(255, 93, 43, 0.527);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.801);
padding: 8px 12px;
border-radius: 4px;
width: 100%;
@@ -7,7 +7,48 @@
display: flex;
flex-direction: column;
gap: $spacing-sm;
.meta { color: $color-muted; font-size: 0.9rem; }
.title { font-weight: 600; }
}
.listbox {
max-width: auto;
margin:0 0;
background-color: $rundbgbackgroundcolor;
padding-bottom: 96px;
color: white;
padding-left: 32px;
padding-right: 32px;
padding-top: 96px;
border-radius: 20px;
border-color: #000000;
border-style: double;
box-shadow: 10px 10px 10px rgba(43, 43, 43, 0.8);
font-size: 26px;
list-style-type: none;
&.To-Do {
background-color: #b1003b;
}
&.InProgress {
background-color: #fff385;
}
&.Completed {
background-color: #90ff82;
}
&.Archived {
background-color: #af69ee;
}
}
.postits {
display: grid;
grid-template-columns: auto auto auto;
gap: 8px;
}
+75 -4
View File
@@ -4,6 +4,9 @@
@use "components/sidebar";
@use "components/tickets";
@use "components/diagnostics";
@use "components/frontpage";
@use "components/home";
@use "components/icon";
body {
background: variables.$color-bg;
@@ -13,26 +16,94 @@ body {
}
.admin { display: flex; }
.content { flex: 1; padding: variables.$spacing-md; }
.layout {
display: flex;
min-height: 100vh;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
}
.sidebar {
width: 260px;
flex-shrink: 0;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.content {
flex: 1;
min-width: 0;
height: 100%;
overflow-y: auto;
padding: variables.$spacing-md;
box-sizing: border-box;
}
@media (max-width: 768px) {
.sidebar { position: fixed; left: -100%; transition: left .2s; }
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: -100%;
transition: left .2s;
z-index: 1000;
height: 100%;
box-sizing: border-box;
}
.sidebar.open { left: 0; }
.content { margin-left: 0; }
.content { margin-left: 0; padding: variables.$spacing-md; box-sizing: border-box; }
}
.rundbg {
display: flex;
justify-content: center;
flex-direction: column;
max-width: 40%;
margin:0 auto;
background-color: variables.$rundbgbackgroundcolor;
padding-bottom: 96px;
padding-left: 32px;
padding-right: 32px;
padding-top: 96px;
border-radius: 20px;
gap: 8px;
border-color: #000000;
border-style: double;
box-shadow: 20px 20px 10px rgb(43, 43, 43);
font-size: 26px;
label {
display: block;
width: 100%;
box-sizing: border-box;
}
input, select, button {
width: 100%;
box-sizing: border-box;
padding: 16px;
font-size: 1.8rem;
background-color: #f0f0f0;
box-shadow: #6d6d6d;
box-shadow: 6px 6px 8px;
}
input[type="checkbox"] {
width: auto;
}
button {
margin: 16px 0;
}
&.login {
margin-top: 96px;
}
}
.setup-form input {
width: 100%;
box-sizing: border-box;
}
+1
View File
@@ -0,0 +1 @@
#![doc = include_str!("../README.md")]