feat: ability to create a project (#9)
This commit is contained in:
commit
e0a6dcd2ff
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
/.dioxus/
|
||||
/.git/
|
||||
/.github/
|
||||
/dist/
|
||||
/debug/
|
||||
/node_modules/
|
||||
/static/
|
||||
/target/
|
||||
/docker-compose-dev.yml
|
||||
/docker-compose-prod.yml
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/target/
|
||||
/dist/
|
||||
/static/
|
||||
/.dioxus/
|
||||
/node_modules/
|
||||
|
||||
**/*.rs.bk
|
||||
|
||||
.env
|
||||
.env.prod
|
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
20
.idea/dataSources.local.xml
generated
Normal file
20
.idea/dataSources.local.xml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="dataSourceStorageLocal" created-in="RR-242.20224.309">
|
||||
<data-source name="todo_baggins@localhost" uuid="1658668c-c2b8-426d-a22f-16fbad9eff0b">
|
||||
<database-info product="PostgreSQL" version="16.4 (Debian 16.4-1.pgdg120+1)" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.6.0" dbms="POSTGRES" exact-version="16.4" exact-driver-version="42.6">
|
||||
<identifier-quote-string>"</identifier-quote-string>
|
||||
</database-info>
|
||||
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
|
||||
<secret-storage>master_key</secret-storage>
|
||||
<user-name>app</user-name>
|
||||
<schema-mapping>
|
||||
<introspection-scope>
|
||||
<node kind="database" qname="@">
|
||||
<node kind="schema" qname="@" />
|
||||
</node>
|
||||
</introspection-scope>
|
||||
</schema-mapping>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="todo_baggins@localhost" uuid="1658668c-c2b8-426d-a22f-16fbad9eff0b">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://localhost:5432/todo_baggins</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
4959
.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b.xml
generated
Normal file
4959
.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b.xml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
#n:todo_baggins
|
@ -0,0 +1,2 @@
|
||||
#n:information_schema
|
||||
!<md> [null, 0, null, null, -2147483648, -2147483648]
|
@ -0,0 +1,2 @@
|
||||
#n:pg_catalog
|
||||
!<md> [null, 0, null, null, -2147483648, -2147483648]
|
@ -0,0 +1,2 @@
|
||||
#n:public
|
||||
!<md> [767, 0, null, null, -2147483648, -2147483648]
|
Binary file not shown.
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/todo-baggins.iml" filepath="$PROJECT_DIR$/.idea/todo-baggins.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
11
.idea/runConfigurations/dev.xml
generated
Normal file
11
.idea/runConfigurations/dev.xml
generated
Normal file
@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="dev" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="envFilePath" value="" />
|
||||
<option name="sourceFilePath" value="docker-compose-dev.yml" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
8
.idea/sqldialects.xml
generated
Normal file
8
.idea/sqldialects.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/migrations/00000000000000_diesel_initial_setup/down.sql" dialect="GenericSQL" />
|
||||
<file url="file://$PROJECT_DIR$/migrations/00000000000000_diesel_initial_setup/up.sql" dialect="PostgreSQL" />
|
||||
<file url="PROJECT" dialect="PostgreSQL" />
|
||||
</component>
|
||||
</project>
|
11
.idea/todo-baggins.iml
generated
Normal file
11
.idea/todo-baggins.iml
generated
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
12
.idea/vcs.xml
generated
Normal file
12
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CommitMessageInspectionProfile">
|
||||
<profile version="1.0">
|
||||
<inspection_tool class="CommitFormat" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
3041
Cargo.lock
generated
Normal file
3041
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "todo-baggins"
|
||||
version = "0.1.0"
|
||||
authors = ["Matouš Volf <66163112+matous-volf@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
diesel = { version = "2.2.2", features = ["postgres"] }
|
||||
|
||||
dioxus = { version = "0.5", features = ["fullstack", "router"] }
|
||||
|
||||
# Debug
|
||||
dioxus-logger = "0.5.1"
|
||||
dotenvy = "0.15.7"
|
||||
serde = "1.0.208"
|
||||
validator = { version = "0.18.1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
43
Dioxus.toml
Normal file
43
Dioxus.toml
Normal file
@ -0,0 +1,43 @@
|
||||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "todo-baggins"
|
||||
|
||||
# Dioxus App Default Platform
|
||||
# web, desktop, fullstack
|
||||
default_platform = "fullstack"
|
||||
|
||||
# `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (assets) file folder
|
||||
asset_dir = "assets"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "Todo Baggins"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher trigger, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be watcher monitoring
|
||||
watch_path = ["src", "assets"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
|
||||
style = ["/styles/tailwind_output.css"]
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
1
assets/styles/.gitignore
vendored
Normal file
1
assets/styles/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/tailwind_output.css
|
6
diesel.toml
Normal file
6
diesel.toml
Normal file
@ -0,0 +1,6 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema/mod.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
31
docker-compose-dev.yml
Normal file
31
docker-compose-dev.yml
Normal file
@ -0,0 +1,31 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
dockerfile: docker/dev/app/Dockerfile
|
||||
volumes:
|
||||
- ./.env.dev:/srv/app/.env
|
||||
- ./assets:/srv/app/assets
|
||||
- ./src:/srv/app/src
|
||||
- ./migrations:/srv/app/migrations
|
||||
- ./Cargo.lock:/srv/app/Cargo.lock
|
||||
- ./Cargo.toml:/srv/app/Cargo.toml
|
||||
- ./diesel.toml:/srv/app/diesel.toml
|
||||
- ./Dioxus.toml:/srv/app/Dioxus.toml
|
||||
- ./package.json:/srv/app/package.json
|
||||
- ./package-lock.json:/srv/app/package-lock.json
|
||||
restart: always
|
||||
ports: [ "8000:8000" ]
|
||||
depends_on: [ "db" ]
|
||||
|
||||
db:
|
||||
image: postgres:16.4-bookworm
|
||||
volumes: [ "db_data:/var/lib/postgresql/data" ]
|
||||
ports: [ "5432:5432" ]
|
||||
environment:
|
||||
POSTGRES_DB: todo_baggins
|
||||
POSTGRES_USER: app
|
||||
POSTGRES_PASSWORD: app
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
db_data:
|
0
docker-compose-prod.yml
Executable file
0
docker-compose-prod.yml
Executable file
23
docker/dev/app/Dockerfile
Normal file
23
docker/dev/app/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
FROM rust:1.80-bookworm
|
||||
|
||||
RUN rustup target add wasm32-unknown-unknown && \
|
||||
cargo install dioxus-cli diesel_cli && \
|
||||
apt-get update && apt-get install -y nodejs=18.19.0+dfsg-6~deb12u2 npm=9.2.0~ds1-1 supervisor=4.2.5-1
|
||||
|
||||
COPY . /srv/app
|
||||
WORKDIR /srv/app
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY docker/dev/app/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
RUN chown -R 1000:1000 /srv/app && \
|
||||
chown -R 1000:1000 /usr/local/cargo && \
|
||||
mkdir -p /.local/share/dioxus && \
|
||||
chown -R 1000:1000 /.local/share/dioxus
|
||||
|
||||
HEALTHCHECK CMD curl --fail http://localhost:8000 || exit 1
|
||||
|
||||
USER 1000:1000
|
||||
|
||||
CMD ["sh", "docker/dev/app/entrypoint.sh"]
|
5
docker/dev/app/entrypoint.sh
Executable file
5
docker/dev/app/entrypoint.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
diesel migration run
|
||||
|
||||
supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
23
docker/dev/app/supervisord.conf
Normal file
23
docker/dev/app/supervisord.conf
Normal file
@ -0,0 +1,23 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/dev/null
|
||||
|
||||
[program:npm]
|
||||
command=npm run watch
|
||||
directory=/srv/app
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
[program:dx]
|
||||
command=dx serve
|
||||
directory=/srv/app
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal file
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal 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();
|
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal 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;
|
2
migrations/2024-08-16-221326_create_projects/down.sql
Normal file
2
migrations/2024-08-16-221326_create_projects/down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE IF EXISTS "projects";
|
6
migrations/2024-08-16-221326_create_projects/up.sql
Normal file
6
migrations/2024-08-16-221326_create_projects/up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Your SQL goes here
|
||||
CREATE TABLE "projects"(
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"title" TEXT NOT NULL
|
||||
);
|
||||
|
1386
package-lock.json
generated
Normal file
1386
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
package.json
Normal file
9
package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3.4.6"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tailwindcss -c src/styles/tailwind.config.js -i src/styles/tailwind.css -o assets/styles/tailwind_output.css",
|
||||
"watch": "npm run build -- --watch"
|
||||
}
|
||||
}
|
11
src/components/app.rs
Normal file
11
src/components/app.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use crate::route::Route;
|
||||
use dioxus::core_macro::rsx;
|
||||
use dioxus::dioxus_core::Element;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub(crate) fn App() -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
11
src/components/home.rs
Normal file
11
src/components/home.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use crate::components::project_form::ProjectForm;
|
||||
use dioxus::core_macro::rsx;
|
||||
use dioxus::dioxus_core::Element;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub(crate) fn Home() -> Element {
|
||||
rsx! {
|
||||
ProjectForm {}
|
||||
}
|
||||
}
|
3
src/components/mod.rs
Normal file
3
src/components/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub(crate) mod app;
|
||||
pub(crate) mod home;
|
||||
pub(crate) mod project_form;
|
30
src/components/project_form.rs
Normal file
30
src/components/project_form.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use crate::models::project::NewProject;
|
||||
use crate::server::projects::create_project;
|
||||
use dioxus::core_macro::{component, rsx};
|
||||
use dioxus::dioxus_core::Element;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub(crate) fn ProjectForm() -> Element {
|
||||
rsx! {
|
||||
form {
|
||||
onsubmit: move |event| {
|
||||
async move {
|
||||
let new_project = NewProject::new(
|
||||
event.values().get("title").unwrap().as_value()
|
||||
);
|
||||
let _ = create_project(new_project).await;
|
||||
}
|
||||
},
|
||||
input {
|
||||
r#type: "text",
|
||||
name: "title",
|
||||
placeholder: "title"
|
||||
}
|
||||
button {
|
||||
r#type: "submit",
|
||||
"create"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
src/errors/error.rs
Normal file
29
src/errors/error.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Error {
|
||||
ServerInternal,
|
||||
}
|
||||
|
||||
// has to be implemented for Dioxus server functions
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::ServerInternal => write!(f, "internal server error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// has to be implemented for Dioxus server functions
|
||||
impl FromStr for Error {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"internal server error" => Error::ServerInternal,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
43
src/errors/error_vec.rs
Normal file
43
src/errors/error_vec.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorVec<T> {
|
||||
errors: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> From<ErrorVec<T>> for Vec<T> {
|
||||
fn from(e: ErrorVec<T>) -> Self {
|
||||
e.errors
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for ErrorVec<T> {
|
||||
fn from(e: Vec<T>) -> Self {
|
||||
ErrorVec { errors: e }
|
||||
}
|
||||
}
|
||||
|
||||
// has to be implemented for Dioxus server functions
|
||||
impl<T: Display> Display for ErrorVec<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// has to be implemented for Dioxus server functions
|
||||
impl<T> FromStr for ErrorVec<T> {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ErrorVec { errors: Vec::new() })
|
||||
}
|
||||
}
|
3
src/errors/mod.rs
Normal file
3
src/errors/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod error_vec;
|
||||
pub(crate) mod project_create_error;
|
51
src/errors/project_create_error.rs
Normal file
51
src/errors/project_create_error.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use crate::errors::error::Error;
|
||||
use crate::errors::error_vec::ErrorVec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use validator::{ValidationErrors, ValidationErrorsKind};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ProjectCreateError {
|
||||
TitleLengthInvalid,
|
||||
Error(Error),
|
||||
}
|
||||
|
||||
impl From<ValidationErrors> for ErrorVec<ProjectCreateError> {
|
||||
fn from(e: ValidationErrors) -> Self {
|
||||
e.errors()
|
||||
.iter()
|
||||
.flat_map(|(&field, error_kind)| match field {
|
||||
"title" => match error_kind {
|
||||
ValidationErrorsKind::Field(validation_errors) => validation_errors
|
||||
.iter()
|
||||
.map(|validation_error| validation_error.code.as_ref())
|
||||
.map(|code| match code {
|
||||
"title_length" => ProjectCreateError::TitleLengthInvalid,
|
||||
_ => panic!("unexpected validation error code: {code}"),
|
||||
})
|
||||
.collect::<Vec<ProjectCreateError>>(),
|
||||
_ => panic!("unexpected validation error kind"),
|
||||
},
|
||||
_ => panic!("unexpected validation field name: {field}"),
|
||||
})
|
||||
.collect::<Vec<ProjectCreateError>>()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
// has to be implemented for Dioxus server functions
|
||||
impl Display for ProjectCreateError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// has to be implemented for Dioxus server functions
|
||||
impl FromStr for ProjectCreateError {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(_: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ProjectCreateError::TitleLengthInvalid)
|
||||
}
|
||||
}
|
21
src/main.rs
Normal file
21
src/main.rs
Normal file
@ -0,0 +1,21 @@
|
||||
mod components;
|
||||
mod errors;
|
||||
mod models;
|
||||
mod route;
|
||||
mod schema;
|
||||
mod server;
|
||||
|
||||
use components::app::App;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_logger::tracing::{info, Level};
|
||||
|
||||
fn main() {
|
||||
dioxus_logger::init(Level::INFO).expect("failed to initialize logger");
|
||||
info!("starting app");
|
||||
|
||||
let cfg = server_only!(
|
||||
dioxus::fullstack::Config::new().addr(std::net::SocketAddr::from(([0, 0, 0, 0], 8000)))
|
||||
);
|
||||
|
||||
LaunchBuilder::fullstack().with_cfg(cfg).launch(App);
|
||||
}
|
1
src/models/mod.rs
Normal file
1
src/models/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub(crate) mod project;
|
38
src/models/project.rs
Normal file
38
src/models/project.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use crate::schema::projects;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
const TITLE_LENGTH_MIN: u64 = 1;
|
||||
const TITLE_LENGTH_MAX: u64 = 255;
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize, Debug)]
|
||||
#[diesel(table_name = crate::schema::projects)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct Project {
|
||||
id: i32,
|
||||
title: String,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)]
|
||||
#[diesel(table_name = projects)]
|
||||
pub struct NewProject {
|
||||
#[validate(length(min = "TITLE_LENGTH_MIN", max = "TITLE_LENGTH_MAX", code = "title_length"))]
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl NewProject {
|
||||
pub fn new(title: String) -> Self {
|
||||
Self { title }
|
||||
}
|
||||
}
|
8
src/route/mod.rs
Normal file
8
src/route/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use crate::components::home::Home;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||
pub(crate) enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
}
|
8
src/schema/mod.rs
Normal file
8
src/schema/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
projects (id) {
|
||||
id -> Int4,
|
||||
title -> Text,
|
||||
}
|
||||
}
|
12
src/server/database_connection.rs
Normal file
12
src/server/database_connection.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use dotenvy::dotenv;
|
||||
use std::env;
|
||||
|
||||
pub(crate) fn establish_database_connection() -> ConnectionResult<PgConnection> {
|
||||
dotenv().ok();
|
||||
|
||||
let database_url =
|
||||
env::var("DATABASE_URL").expect("The environment variable DATABASE_URL must be set.");
|
||||
PgConnection::establish(&database_url)
|
||||
}
|
2
src/server/mod.rs
Normal file
2
src/server/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod database_connection;
|
||||
pub(crate) mod projects;
|
34
src/server/projects.rs
Normal file
34
src/server/projects.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use crate::errors::error::Error;
|
||||
use crate::errors::error_vec::ErrorVec;
|
||||
use crate::errors::project_create_error::ProjectCreateError;
|
||||
use crate::models::project::{NewProject, Project};
|
||||
use crate::server::database_connection::establish_database_connection;
|
||||
use diesel::{RunQueryDsl, SelectableHelper};
|
||||
use dioxus::prelude::*;
|
||||
use validator::Validate;
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn create_project(
|
||||
new_project: NewProject,
|
||||
) -> Result<Project, ServerFnError<ErrorVec<ProjectCreateError>>> {
|
||||
use crate::schema::projects;
|
||||
|
||||
new_project
|
||||
.validate()
|
||||
.map_err::<ErrorVec<ProjectCreateError>, _>(|errors| errors.into())?;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<ProjectCreateError>, _>(
|
||||
|_| vec![ProjectCreateError::Error(Error::ServerInternal), ].into()
|
||||
)?;
|
||||
|
||||
let new_project = diesel::insert_into(projects::table)
|
||||
.values(&new_project)
|
||||
.returning(Project::as_returning())
|
||||
.get_result(&mut connection)
|
||||
.map_err::<ErrorVec<ProjectCreateError>, _>(
|
||||
|_| vec![ProjectCreateError::Error(Error::ServerInternal), ].into()
|
||||
)?;
|
||||
|
||||
Ok(new_project)
|
||||
}
|
9
src/styles/tailwind.config.js
Normal file
9
src/styles/tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
mode: "all",
|
||||
content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
8
src/styles/tailwind.css
Normal file
8
src/styles/tailwind.css
Normal file
@ -0,0 +1,8 @@
|
||||
/* stylelint-disable */
|
||||
/* noinspection CssInvalidAtRule */
|
||||
@tailwind base;
|
||||
/* noinspection CssInvalidAtRule */
|
||||
@tailwind components;
|
||||
/* noinspection CssInvalidAtRule */
|
||||
@tailwind utilities;
|
||||
/* stylelint-enable */
|
Loading…
x
Reference in New Issue
Block a user