Initial State

This commit is contained in:
Andrey Tkachenko 2023-05-12 11:12:46 +04:00
commit 261577bfc7
40 changed files with 4896 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgres://postgres@localhost/kttd

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/ui/target

1565
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "kttd"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = { version = "0.6.18", features = ["tracing", "http2"] }
chrono = { version = "0.4.24", features = ["serde"] }
ctxerr = "0.2.5"
diesel = { version = "2.0.4", features = ["chrono"] }
diesel-async = { version = "0.2.2", features = ["postgres", "tokio-postgres", "deadpool"] }
serde = { version = "1.0.163", features = ["derive"] }
tokio = { version = "1.28.0", features = ["parking_lot", "macros", "rt", "rt-multi-thread"] }

8
diesel.toml Normal file
View File

@ -0,0 +1,8 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
[migrations_directory]
dir = "migrations"

0
migrations/.keep Normal file
View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1,10 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
image_id INTEGER,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1 @@
-- Your SQL goes here

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1 @@
-- Your SQL goes here

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1 @@
-- Your SQL goes here

35
src/api/mod.rs Normal file
View File

@ -0,0 +1,35 @@
mod user;
use axum::{
routing::{get, post},
Router,
};
use crate::db::DbPool;
#[derive(Clone)]
pub struct ApiState {
pub pool: DbPool,
}
pub async fn start_server(state: ApiState) {
// initialize tracing
// tracing_subscriber::fmt::init();
// build our application with a route
let app = Router::new()
.route("/", get(root))
.route("/users", post(user::create_user))
.route("/users", get(user::list_user))
.with_state(state);
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// basic handler that responds with a static string
async fn root() -> &'static str {
"Hello, World!"
}

47
src/api/user.rs Normal file
View File

@ -0,0 +1,47 @@
use axum::{extract::State, http::StatusCode, Json};
use serde::Deserialize;
use crate::db::{
models::user::{NewUser, User},
repos,
};
use super::ApiState;
#[derive(Deserialize)]
pub struct ApiUser {
first_name: String,
last_name: String,
email: String,
}
pub async fn create_user(
State(s): State<ApiState>,
Json(payload): Json<ApiUser>,
) -> Result<Json<User>, (StatusCode, String)> {
let user = repos::user::create_user(
&s.pool,
NewUser {
first_name: &payload.first_name,
last_name: &payload.last_name,
email: &payload.email,
},
)
.await
.map_err(internal_error)?;
Ok(Json(user))
}
pub async fn list_user(State(s): State<ApiState>) -> (StatusCode, Json<Vec<User>>) {
let user = repos::user::list_users(&s.pool).await.unwrap();
(StatusCode::CREATED, Json(user))
}
fn internal_error<E>(err: E) -> (StatusCode, String)
where
E: std::error::Error,
{
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
}

13
src/db/mod.rs Normal file
View File

@ -0,0 +1,13 @@
pub mod models;
pub mod repos;
pub mod schema;
use diesel_async::pg::AsyncPgConnection;
use diesel_async::pooled_connection::deadpool::Pool;
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
pub type DbPool = Pool<AsyncPgConnection>;
pub async fn connect<S: Into<String>>(durl: S) -> Result<DbPool, crate::error::Error> {
Ok(DbPool::builder(AsyncDieselConnectionManager::new(durl)).build()?)
}

0
src/db/models/image.rs Normal file
View File

0
src/db/models/item.rs Normal file
View File

View File

6
src/db/models/mod.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod image;
pub mod item;
pub mod location;
pub mod role;
pub mod tag;
pub mod user;

0
src/db/models/role.rs Normal file
View File

0
src/db/models/tag.rs Normal file
View File

24
src/db/models/user.rs Normal file
View File

@ -0,0 +1,24 @@
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use crate::db::schema::users;
#[derive(Debug, Serialize, Clone, Queryable, Selectable, Identifiable)]
#[diesel(table_name = users)]
pub struct User {
pub id: i32,
pub image_id: Option<i32>,
pub first_name: String,
pub last_name: String,
pub email: String,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}
#[derive(Debug, Deserialize, Clone, Insertable)]
#[diesel(table_name = users)]
pub struct NewUser<'a> {
pub first_name: &'a str,
pub last_name: &'a str,
pub email: &'a str,
}

0
src/db/repos/items.rs Normal file
View File

0
src/db/repos/location.rs Normal file
View File

3
src/db/repos/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod items;
pub mod location;
pub mod user;

25
src/db/repos/user.rs Normal file
View File

@ -0,0 +1,25 @@
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use crate::db::models::user::NewUser;
use crate::db::schema::users;
use crate::db::{models::user::User, DbPool};
use crate::Error;
pub async fn create_user<'a>(pool: &DbPool, user: NewUser<'a>) -> Result<User, Error> {
let mut conn = pool.get().await?;
Ok(diesel::insert_into(users::table)
.values(&user)
.get_result(&mut conn)
.await?)
}
pub async fn list_users(pool: &DbPool) -> Result<Vec<User>, Error> {
let mut conn = pool.get().await?;
Ok(users::table
.select(User::as_select())
.load(&mut conn)
.await?)
}

108
src/db/schema.rs Normal file
View File

@ -0,0 +1,108 @@
diesel::table! {
users {
id -> Int4,
image_id -> Nullable<Int4>,
first_name -> VarChar,
last_name -> VarChar,
email -> VarChar,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
user_schedule {
id -> Int4,
user_id -> Int4,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
items {
id -> Int4,
title -> VarChar,
image_id -> Nullable<Int4>,
location_id -> Nullable<Int4>,
description -> Text,
created_by -> Int4,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
images {
id -> Int4,
title -> VarChar,
url -> VarChar,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
item_image {
id -> Int4,
item_id -> Int4,
image_id -> Int4,
}
}
diesel::table! {
locations {
id -> Int4,
parent_id -> Nullable<Int4>,
image_id -> Nullable<Int4>,
title -> VarChar,
description -> Text,
created_by -> Int4,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
location_image {
id -> Int4,
location_id -> Int4,
image_id -> Int4,
}
}
diesel::table! {
tasks {
id -> Int4,
active -> Bool,
title -> Varchar,
description -> Text,
assigned_to -> Nullable<Int4>,
estimate_time -> Int4,
estimate_period -> Int4,
estimate_start_date -> Nullable<Timestamp>,
estimate_end_date -> Nullable<Timestamp>,
created_by -> Int4,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
item_task {
id -> Int4,
location_id -> Int4,
image_id -> Int4,
}
}
diesel::joinable!(users -> images (image_id));
diesel::joinable!(item_image -> images (image_id));
diesel::joinable!(item_image -> items (item_id));
diesel::joinable!(location_image -> images (image_id));
diesel::joinable!(location_image -> locations (location_id));
diesel::allow_tables_to_appear_in_same_query!(items, item_image, images,);
diesel::allow_tables_to_appear_in_same_query!(items, location_image, locations,);

16
src/error.rs Normal file
View File

@ -0,0 +1,16 @@
use ctxerr::ctxerr;
use diesel::result;
use diesel_async::pooled_connection::deadpool::{BuildError, PoolError};
// use diesel_async::pooled_connection::PoolError;
#[ctxerr]
pub enum ErrorKind {
#[error("PoolError: {0}")]
Pool(#[from] PoolError),
#[error("PoolBuildError: {0}")]
PoolBuild(#[from] BuildError),
#[error("DieselResultError: {0}")]
DieselResult(#[from] result::Error),
}

27
src/main.rs Normal file
View File

@ -0,0 +1,27 @@
mod api;
mod db;
mod error;
pub use error::{Error, ErrorKind};
#[tokio::main]
async fn main() -> Result<(), Error> {
let durl = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres@127.0.0.1:5432/kttd".to_string());
let pool = db::connect(durl).await?;
// repos::user::create_user(
// &pool,
// db::models::user::NewUser {
// first_name: "Andrey",
// last_name: "Tkachenko",
// email: "andrey@aidev.ru",
// },
// )
// .await?;
api::start_server(api::ApiState { pool }).await;
Ok(())
}

1658
ui/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

10
ui/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "ui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = "0.3.2"
dioxus-web = "0.3.1"

1029
ui/dist/assets/dioxus/dioxus.js vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
ui/dist/assets/dioxus/dioxus_bg.wasm vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,180 @@
let m,p,ls,lss,sp,d,t,c,s,sl,op,i,e,z,index,bubbles,n,len,field,root,text,ns,value,ptr,many,event_name,tmpl_id,id;const ns_cache = [];const evt = [];const attr = [];
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
this.global = {};
// non bubbling events listen at the element the listener was created at
this.local = {};
this.root = null;
this.handler = null;
}
create(event_name, element, bubbles) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = {};
this.global[event_name].active = 1;
this.root.addEventListener(event_name, this.handler);
} else {
this.global[event_name].active++;
}
}
else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
element.addEventListener(event_name, this.handler);
}
}
remove(element, event_name, bubbles) {
if (bubbles) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
else {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (this.local[id].length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, this.handler);
}
}
removeAllNonBubbling(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
function SetAttributeInner(node, field, value, ns) {
const name = field;
if (ns === "style") {
// ????? why do we need to do this
if (node.style === undefined) {
node.style = {};
}
node.style[name] = value;
} else if (ns !== null && ns !== undefined && ns !== "") {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value !== node.value) {
node.value = value;
}
break;
case "checked":
node.checked = value === "true";
break;
case "selected":
node.selected = value === "true";
break;
case "dangerous_inner_html":
node.innerHTML = value;
break;
default:
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
}
}
function LoadChild(ptr, len) {
// iterate through each number and get that child
node = stack[stack.length - 1];
ptr_end = ptr + len;
for (; ptr < ptr_end; ptr++) {
end = m.getUint8(ptr);
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
const listeners = new ListenerMap();
let nodes = [];
let stack = [];
const templates = {};
let node, els, end, ptr_end, k;
export function save_template(nodes, tmpl_id) {
templates[tmpl_id] = nodes;
}
export function set_node(id, node) {
nodes[id] = node;
}
export function initilize(root, handler) {
listeners.handler = handler;
nodes = [root];
stack = [root];
listeners.root = root;
}
function AppendChildren(id, many){
root = nodes[id];
els = stack.splice(stack.length-many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
async: true,
autofocus: true,
autoplay: true,
checked: true,
controls: true,
default: true,
defer: true,
disabled: true,
formnovalidate: true,
hidden: true,
ismap: true,
itemscope: true,
loop: true,
multiple: true,
muted: true,
nomodule: true,
novalidate: true,
open: true,
playsinline: true,
readonly: true,
required: true,
reversed: true,
selected: true,
truespeed: true,
};
export function create(r){d=r;c=new TextDecoder('utf-8',{fatal:true})}export function update_memory(r){m=new DataView(r.buffer)}export function set_buffer(b){m=new DataView(b)}export function run(){t=m.getUint8(d,true);if(t&1){ls=m.getUint32(d+1,true)}p=ls;if(t&2){lss=m.getUint32(d+5,true)}if(t&4){sl=m.getUint32(d+9,true);if(t&8){sp=lss;s="";e=sp+(sl/4|0)*4;while(sp<e){t=m.getUint32(sp,true);s+=String.fromCharCode(t&255,(t&65280)>>8,(t&16711680)>>16,t>>24);sp+=4}while(sp<lss+sl){s+=String.fromCharCode(m.getUint8(sp++));}}else{s=c.decode(new DataView(m.buffer,lss,sl))}sp=0}for(;;){op=m.getUint32(p,true);p+=4;z=0;while(z++<4){switch(op&255){case 0:{AppendChildren(root, stack.length-1);}break;case 1:{stack.push(nodes[m.getUint32(p,true)]);}p+=4;break;case 2:id=m.getUint32(p,true);p += 4;{AppendChildren(id, m.getUint32(p,true));}p+=4;break;case 3:{stack.pop();}break;case 4:id=m.getUint32(p,true);p += 4;{root = nodes[id]; els = stack.splice(stack.length-m.getUint32(p,true)); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}p+=4;break;case 5:id=m.getUint32(p,true);p += 4;{nodes[id].after(...stack.splice(stack.length-m.getUint32(p,true)));}p+=4;break;case 6:id=m.getUint32(p,true);p += 4;{nodes[id].before(...stack.splice(stack.length-m.getUint32(p,true)));}p+=4;break;case 7:{node = nodes[m.getUint32(p,true)]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}p+=4;break;case 8:{stack.push(document.createTextNode(s.substring(sp,sp+=m.getUint32(p,true))));}p+=4;break;case 9:text=s.substring(sp,sp+=m.getUint32(p,true));p += 4;{node = document.createTextNode(text); nodes[m.getUint32(p,true)] = node; stack.push(node);}p+=4;break;case 10:{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[m.getUint32(p,true)] = node;}p+=4;break;case 11:id=m.getUint32(p,true);p += 4;i=m.getUint32(p,true);if((i&128)!=0){event_name=s.substring(sp,sp+=(i>>>8)&255);evt[i&127]=event_name;}else{event_name=evt[i&127];}node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `${id}`); listeners.create(event_name, node, (i>>>16)&255);p+=3;break;case 12:i=m.getUint32(p,true);p += 3;if((i&128)!=0){event_name=s.substring(sp,sp+=(i>>>8)&255);evt[i&127]=event_name;}else{event_name=evt[i&127];}bubbles=(i>>>16)&255;{node = nodes[m.getUint32(p,true)]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, event_name, bubbles);}p+=4;break;case 13:id=m.getUint32(p,true);p += 4;{nodes[id].textContent = s.substring(sp,sp+=m.getUint32(p,true));}p+=4;break;case 14:i=m.getUint32(p,true);p += 4;if((i&128)!=0){ns=s.substring(sp,sp+=(i>>>8)&255);ns_cache[i&127]=ns;}else{ns=ns_cache[i&127];}if((i&8388608)!=0){field=s.substring(sp,sp+=i>>>24);attr[(i>>>16)&127]=field;}else{field=attr[(i>>>16)&127];}id=m.getUint32(p,true);p += 4;{node = nodes[id]; SetAttributeInner(node, field, s.substring(sp,sp+=m.getUint32(p,true)), ns);}p+=4;break;case 15:i=m.getUint32(p,true);p += 4;if((i&128)!=0){ns=s.substring(sp,sp+=(i>>>8)&255);ns_cache[i&127]=ns;}else{ns=ns_cache[i&127];}if((i&8388608)!=0){field=s.substring(sp,sp+=i>>>24);attr[(i>>>16)&127]=field;}else{field=attr[(i>>>16)&127];}{name = field;
node = nodes[m.getUint32(p,true)];
if (ns == "style") {
node.style.removeProperty(name);
} else if (ns !== null && ns !== undefined && ns !== "") {
node.removeAttributeNS(ns, name);
} else if (name === "value") {
node.value = "";
} else if (name === "checked") {
node.checked = false;
} else if (name === "selected") {
node.selected = false;
} else if (name === "dangerous_inner_html") {
node.innerHTML = "";
} else {
node.removeAttribute(name);
}}p+=4;break;case 16:len=m.getUint8(p,true);p += 1;ptr=m.getUint32(p,true);p += 4;{nodes[m.getUint32(p,true)] = LoadChild(ptr, len);}p+=4;break;case 17:len=m.getUint8(p,true);p += 1;value=s.substring(sp,sp+=m.getUint32(p,true));p += 4;ptr=m.getUint32(p,true);p += 4;{
node = LoadChild(ptr, len);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
nodes[m.getUint32(p,true)] = node;
}p+=4;break;case 18:len=m.getUint8(p,true);p += 1;ptr=m.getUint32(p,true);p += 4;{els = stack.splice(stack.length - m.getUint32(p,true)); node = LoadChild(ptr, len); node.replaceWith(...els);}p+=4;break;case 19:tmpl_id=m.getUint32(p,true);p += 4;index=m.getUint32(p,true);p += 4;{node = templates[tmpl_id][index].cloneNode(true); nodes[m.getUint32(p,true)] = node; stack.push(node);}p+=4;break;case 20:return true;}op>>>=8;}}}

46
ui/dist/index.html vendored Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>dioxus | ⛺</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
</head>
<body>
<div id="main"></div>
<script type="module">
import init from "/./assets/dioxus/dioxus.js";
init("/./assets/dioxus/dioxus_bg.wasm").then(wasm => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
</script>
</body>
</html><script>// Dioxus-CLI
// https://github.com/DioxusLabs/cli
(function () {
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = protocol + '//' + window.location.host + '/_dioxus/ws';
var poll_interval = 8080;
var reload_upon_connect = () => {
window.setTimeout(
() => {
var ws = new WebSocket(url);
ws.onopen = () => window.location.reload();
ws.onclose = reload_upon_connect;
},
poll_interval);
};
var ws = new WebSocket(url);
ws.onmessage = (ev) => {
if (ev.data == "reload") {
window.location.reload();
}
};
ws.onclose = reload_upon_connect;
})()</script>

19
ui/src/main.rs Normal file
View File

@ -0,0 +1,19 @@
#![allow(non_snake_case)]
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
use dioxus::prelude::*;
fn main() {
// launch the web app
dioxus_web::launch(App);
}
// create a component that renders a div with the text "Hello, world!"
fn App(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
})
}