* Create a minimalistic toast
* Profile editing (WIP)
This commit is contained in:
parent
327b2d0a5b
commit
1c79cc890d
25 changed files with 1133 additions and 575 deletions
604
Cargo.lock
generated
604
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -28,7 +28,7 @@ fn main() {
|
||||||
|
|
||||||
fn run_sass(command: &mut Command) -> Output {
|
fn run_sass(command: &mut Command) -> Output {
|
||||||
command
|
command
|
||||||
.arg("style.scss")
|
.arg("scss/style.scss")
|
||||||
.arg("static/style.css")
|
.arg("static/style.css")
|
||||||
.output()
|
.output()
|
||||||
.expect("Unable to compile SASS file, install SASS, see https://sass-lang.com/")
|
.expect("Unable to compile SASS file, install SASS, see https://sass-lang.com/")
|
||||||
|
|
|
||||||
159
backend/scss/style.scss
Normal file
159
backend/scss/style.scss
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
@use 'toast.scss';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Fira Code;
|
||||||
|
font-weight: 200;
|
||||||
|
src: url(FiraCode-Light.woff2) format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Fira Code;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(FiraCode-Regular.woff2) format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Fira Code;
|
||||||
|
font-weight: 600;
|
||||||
|
src: url(FiraCode-SemiBold.woff2) format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Fira Code;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(FiraCode-Bold.woff2) format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
$primary: #182430;
|
||||||
|
$background: darken($primary, 5%);
|
||||||
|
$background-container: lighten($primary, 10%);
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 80%
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: lighten($primary, 40%);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: lighten($primary, 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
font-family: Fira Code, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
|
text-shadow: 2px 2px 2px rgb(0, 0, 0);
|
||||||
|
// line-height: 18px;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
background-color: $background;
|
||||||
|
margin: 0px;
|
||||||
|
|
||||||
|
.recipe-item {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item-current {
|
||||||
|
padding: 3px;
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
// .recipes-list {
|
||||||
|
// text-align: left;
|
||||||
|
// }
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
background-color: $background-container;
|
||||||
|
border: 0.1em solid white;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
background-color: rgb(52, 40, 85);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: white;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #user-edit {
|
||||||
|
// .label-name {
|
||||||
|
// grid-column: 1;
|
||||||
|
// grid-row: 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .input-name {
|
||||||
|
// grid-column: 2;
|
||||||
|
// grid-row: 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .label-password-1 {
|
||||||
|
// grid-column: 1;
|
||||||
|
// grid-row: 2;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .input-password-1 {
|
||||||
|
// grid-column: 2;
|
||||||
|
// grid-row: 2;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .label-password-2 {
|
||||||
|
// grid-column: 1;
|
||||||
|
// grid-row: 3;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .input-password-2 {
|
||||||
|
// grid-column: 2;
|
||||||
|
// grid-row: 3;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .button-save {
|
||||||
|
// grid-column: 2;
|
||||||
|
// grid-row: 4;
|
||||||
|
// width: fit-content;
|
||||||
|
// justify-self: flex-end;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #sign-in {
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
backend/scss/toast.scss
Normal file
44
backend/scss/toast.scss
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#toast {
|
||||||
|
visibility: hidden;
|
||||||
|
max-width: 300px;
|
||||||
|
margin-left: -125px;
|
||||||
|
background-color: black;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 16px; // TODO: 'rem' better?
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 30px;
|
||||||
|
box-shadow: -1px 1px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toast.show {
|
||||||
|
visibility: visible;
|
||||||
|
animation: fadein 0.5s, fadeout 0.5s 3.6s;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
bottom: 30px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
bottom: 30px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
70
backend/src/html_templates.rs
Normal file
70
backend/src/html_templates.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
use crate::model;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "home.html")]
|
||||||
|
pub struct HomeTemplate {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
pub recipes: Vec<(i64, String)>,
|
||||||
|
pub current_recipe_id: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "view_recipe.html")]
|
||||||
|
pub struct ViewRecipeTemplate {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
pub recipes: Vec<(i64, String)>,
|
||||||
|
pub current_recipe_id: Option<i64>,
|
||||||
|
pub current_recipe: model::Recipe,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "message.html")]
|
||||||
|
pub struct MessageTemplate<'a> {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
pub message: &'a str,
|
||||||
|
pub as_code: bool, // Display the message in <pre> markup.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "sign_up_form.html")]
|
||||||
|
pub struct SignUpFormTemplate {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
pub email: String,
|
||||||
|
pub message: String,
|
||||||
|
pub message_email: String,
|
||||||
|
pub message_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "sign_in_form.html")]
|
||||||
|
pub struct SignInFormTemplate {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
pub email: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "ask_reset_password.html")]
|
||||||
|
pub struct AskResetPasswordTemplate {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
pub email: String,
|
||||||
|
pub message: String,
|
||||||
|
pub message_email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "reset_password.html")]
|
||||||
|
pub struct ResetPasswordTemplate {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
pub reset_token: String,
|
||||||
|
pub message: String,
|
||||||
|
pub message_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "profile.html")]
|
||||||
|
pub struct ProfileTemplate {
|
||||||
|
pub user: Option<model::User>,
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,10 @@ use std::{net::SocketAddr, path::Path};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{ConnectInfo, FromRef, Request, State},
|
extract::{ConnectInfo, FromRef, Request, State},
|
||||||
|
http::StatusCode,
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::{Response, Result},
|
response::{Response, Result},
|
||||||
routing::{get, post, put},
|
routing::{get, put},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::cookie::CookieJar;
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
|
|
@ -21,8 +22,10 @@ mod consts;
|
||||||
mod data;
|
mod data;
|
||||||
mod email;
|
mod email;
|
||||||
mod hash;
|
mod hash;
|
||||||
|
mod html_templates;
|
||||||
mod model;
|
mod model;
|
||||||
mod ron_extractor;
|
mod ron_extractor;
|
||||||
|
mod ron_utils;
|
||||||
mod services;
|
mod services;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
|
@ -44,6 +47,12 @@ impl FromRef<AppState> for db::Connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl axum::response::IntoResponse for db::DBError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
ron_utils::ron_error(StatusCode::INTERNAL_SERVER_ERROR, &self.to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
const TRACING_LEVEL: tracing::Level = tracing::Level::DEBUG;
|
const TRACING_LEVEL: tracing::Level = tracing::Level::DEBUG;
|
||||||
|
|
||||||
|
|
@ -75,7 +84,12 @@ async fn main() {
|
||||||
db_connection,
|
db_connection,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
// TODO: Add fallback fo ron_api_routes.
|
||||||
|
let ron_api_routes = Router::new()
|
||||||
|
.route("/user/update", put(services::ron::update_user))
|
||||||
|
.fallback(services::ron::not_found);
|
||||||
|
|
||||||
|
let html_routes = Router::new()
|
||||||
.route("/", get(services::home_page))
|
.route("/", get(services::home_page))
|
||||||
.route(
|
.route(
|
||||||
"/signup",
|
"/signup",
|
||||||
|
|
@ -99,15 +113,19 @@ async fn main() {
|
||||||
.route("/recipe/view/:id", get(services::view_recipe))
|
.route("/recipe/view/:id", get(services::view_recipe))
|
||||||
// User.
|
// User.
|
||||||
.route("/user/edit", get(services::edit_user))
|
.route("/user/edit", get(services::edit_user))
|
||||||
// RON API.
|
.route_layer(middleware::from_fn(services::ron_error_to_html));
|
||||||
.route("/user/set_name", put(services::ron::update_user))
|
|
||||||
|
let app = Router::new()
|
||||||
|
.merge(html_routes)
|
||||||
|
.nest("/ron-api", ron_api_routes)
|
||||||
|
.fallback(services::not_found)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.route_layer(middleware::from_fn_with_state(
|
// FIXME: Should be 'route_layer' but it doesn't work for 'fallback(..)'.
|
||||||
|
.layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
user_authentication,
|
user_authentication,
|
||||||
))
|
))
|
||||||
.nest_service("/static", ServeDir::new("static"))
|
.nest_service("/static", ServeDir::new("static"))
|
||||||
.fallback(services::not_found)
|
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.into_make_service_with_connect_info::<SocketAddr>();
|
.into_make_service_with_connect_info::<SocketAddr>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,63 +5,9 @@ use axum::{
|
||||||
http::{header, StatusCode},
|
http::{header, StatusCode},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use ron::{
|
use serde::de::DeserializeOwned;
|
||||||
de::from_bytes,
|
|
||||||
ser::{to_string_pretty, PrettyConfig},
|
|
||||||
};
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
|
|
||||||
const RON_CONTENT_TYPE: &'static str = "application/ron";
|
use crate::ron_utils;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
|
||||||
pub struct RonError {
|
|
||||||
pub error: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl axum::response::IntoResponse for RonError {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
let ron_as_str = to_string_pretty(&self, PrettyConfig::new()).unwrap();
|
|
||||||
ron_as_str.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RonError> for Response {
|
|
||||||
fn from(value: RonError) -> Self {
|
|
||||||
value.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ron_error(status: StatusCode, message: &str) -> impl IntoResponse {
|
|
||||||
(
|
|
||||||
status,
|
|
||||||
[(header::CONTENT_TYPE, RON_CONTENT_TYPE)],
|
|
||||||
RonError {
|
|
||||||
error: message.to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ron_response<T>(ron: T) -> impl IntoResponse
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
let ron_as_str = to_string_pretty(&ron, PrettyConfig::new()).unwrap();
|
|
||||||
([(header::CONTENT_TYPE, RON_CONTENT_TYPE)], ron_as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_body<T>(body: Bytes) -> Result<T, RonError>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned,
|
|
||||||
{
|
|
||||||
match from_bytes::<T>(&body) {
|
|
||||||
Ok(ron) => Ok(ron),
|
|
||||||
Err(error) => {
|
|
||||||
return Err(RonError {
|
|
||||||
error: format!("Ron parsing error: {}", error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ExtractRon<T: DeserializeOwned>(pub T);
|
pub struct ExtractRon<T: DeserializeOwned>(pub T);
|
||||||
|
|
||||||
|
|
@ -71,22 +17,26 @@ where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
{
|
{
|
||||||
type Rejection = Response; // axum::Error::ErrorResponse;
|
type Rejection = Response;
|
||||||
|
|
||||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
match req.headers().get(header::CONTENT_TYPE) {
|
match req.headers().get(header::CONTENT_TYPE) {
|
||||||
Some(content_type) => {
|
Some(content_type) => {
|
||||||
if content_type != RON_CONTENT_TYPE {
|
if content_type != ron_utils::RON_CONTENT_TYPE {
|
||||||
return Err(ron_error(
|
return Err(ron_utils::ron_error(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
&format!("Invalid content type, must be {}", RON_CONTENT_TYPE),
|
&format!(
|
||||||
|
"Invalid content type, must be {:?}",
|
||||||
|
ron_utils::RON_CONTENT_TYPE
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(
|
return Err(
|
||||||
ron_error(StatusCode::BAD_REQUEST, "No content type given").into_response()
|
ron_utils::ron_error(StatusCode::BAD_REQUEST, "No content type given")
|
||||||
|
.into_response(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +45,7 @@ where
|
||||||
.await
|
.await
|
||||||
.map_err(IntoResponse::into_response)?;
|
.map_err(IntoResponse::into_response)?;
|
||||||
|
|
||||||
let ron = parse_body(body)?;
|
let ron = ron_utils::parse_body(body)?;
|
||||||
|
|
||||||
Ok(Self(ron))
|
Ok(Self(ron))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
71
backend/src/ron_utils.rs
Normal file
71
backend/src/ron_utils.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
body::Bytes,
|
||||||
|
extract::{FromRequest, Request},
|
||||||
|
http::{header, HeaderValue, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use common::ron_api;
|
||||||
|
use ron::de::from_bytes;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
|
pub const RON_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/ron");
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
pub struct RonError {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl axum::response::IntoResponse for RonError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let ron_as_str = ron_api::to_string(&self);
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
[(header::CONTENT_TYPE, RON_CONTENT_TYPE)],
|
||||||
|
ron_as_str,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RonError> for Response {
|
||||||
|
fn from(value: RonError) -> Self {
|
||||||
|
value.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ron_error(status: StatusCode, message: &str) -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
[(header::CONTENT_TYPE, RON_CONTENT_TYPE)],
|
||||||
|
RonError {
|
||||||
|
error: message.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ron_response<T>(status: StatusCode, ron: T) -> impl IntoResponse
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
let ron_as_str = ron_api::to_string(&ron);
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
[(header::CONTENT_TYPE, RON_CONTENT_TYPE)],
|
||||||
|
ron_as_str,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_body<T>(body: Bytes) -> Result<T, RonError>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
match from_bytes::<T>(&body) {
|
||||||
|
Ok(ron) => Ok(ron),
|
||||||
|
Err(error) => {
|
||||||
|
return Err(RonError {
|
||||||
|
error: format!("Ron parsing error: {}", error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{collections::HashMap, net::SocketAddr};
|
use std::{collections::HashMap, net::SocketAddr};
|
||||||
|
|
||||||
use askama::Template;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::{self, Body},
|
||||||
debug_handler,
|
debug_handler,
|
||||||
extract::{ConnectInfo, Extension, Host, Path, Query, Request, State},
|
extract::{ConnectInfo, Extension, Host, Path, Query, Request, State},
|
||||||
http::{HeaderMap, StatusCode},
|
http::{header, HeaderMap},
|
||||||
|
middleware::Next,
|
||||||
response::{IntoResponse, Redirect, Response, Result},
|
response::{IntoResponse, Redirect, Response, Result},
|
||||||
Form,
|
Form,
|
||||||
};
|
};
|
||||||
|
|
@ -14,30 +14,36 @@ use chrono::Duration;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
||||||
use crate::{config::Config, consts, data::db, email, model, utils, AppState};
|
use crate::{
|
||||||
|
config::Config, consts, data::db, email, html_templates::*, model, ron_utils, utils, AppState,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod ron;
|
pub mod ron;
|
||||||
|
|
||||||
impl axum::response::IntoResponse for db::DBError {
|
// Will embed RON error in HTML page.
|
||||||
fn into_response(self) -> Response {
|
pub async fn ron_error_to_html(req: Request, next: Next) -> Result<Response> {
|
||||||
let body = MessageTemplate {
|
let response = next.run(req).await;
|
||||||
user: None,
|
|
||||||
message: &self.to_string(),
|
if let Some(content_type) = response.headers().get(header::CONTENT_TYPE) {
|
||||||
};
|
if content_type == ron_utils::RON_CONTENT_TYPE {
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
|
let message = match body::to_bytes(response.into_body(), usize::MAX).await {
|
||||||
|
Ok(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(),
|
||||||
|
Err(error) => error.to_string(),
|
||||||
|
};
|
||||||
|
return Ok(MessageTemplate {
|
||||||
|
user: None,
|
||||||
|
message: &message,
|
||||||
|
as_code: true,
|
||||||
|
}
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
///// HOME /////
|
///// HOME /////
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "home.html")]
|
|
||||||
struct HomeTemplate {
|
|
||||||
user: Option<model::User>,
|
|
||||||
recipes: Vec<(i64, String)>,
|
|
||||||
current_recipe_id: Option<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn home_page(
|
pub async fn home_page(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
|
|
@ -54,15 +60,6 @@ pub async fn home_page(
|
||||||
|
|
||||||
///// VIEW RECIPE /////
|
///// VIEW RECIPE /////
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "view_recipe.html")]
|
|
||||||
struct ViewRecipeTemplate {
|
|
||||||
user: Option<model::User>,
|
|
||||||
recipes: Vec<(i64, String)>,
|
|
||||||
current_recipe_id: Option<i64>,
|
|
||||||
current_recipe: model::Recipe,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn view_recipe(
|
pub async fn view_recipe(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
|
|
@ -78,121 +75,36 @@ pub async fn view_recipe(
|
||||||
current_recipe: recipe,
|
current_recipe: recipe,
|
||||||
}
|
}
|
||||||
.into_response()),
|
.into_response()),
|
||||||
None => Ok(MessageTemplate {
|
None => Ok(MessageTemplate::new_with_user(
|
||||||
|
&format!("Cannot find the recipe {}", recipe_id),
|
||||||
user,
|
user,
|
||||||
message: &format!("Cannot find the recipe {}", recipe_id),
|
)
|
||||||
}
|
|
||||||
.into_response()),
|
.into_response()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///// EDIT/NEW RECIPE /////
|
|
||||||
|
|
||||||
// #[derive(Template)]
|
|
||||||
// #[template(path = "edit_recipe.html")]
|
|
||||||
// struct EditRecipeTemplate {
|
|
||||||
// user: Option<model::User>,
|
|
||||||
// recipes: Vec<(i64, String)>,
|
|
||||||
// current_recipe_id: Option<i64>,
|
|
||||||
|
|
||||||
// current_recipe: model::Recipe,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[get("/recipe/edit/{id}")]
|
|
||||||
// pub async fn edit_recipe(
|
|
||||||
// req: HttpRequest,
|
|
||||||
// path: web::Path<(i64,)>,
|
|
||||||
// connection: web::Data<db::Connection>,
|
|
||||||
// ) -> Result<HttpResponse> {
|
|
||||||
// let (id,) = path.into_inner();
|
|
||||||
// let user = match get_current_user(&req, connection.clone()).await {
|
|
||||||
// Some(u) => u,
|
|
||||||
// None => {
|
|
||||||
// return Ok(MessageTemplate {
|
|
||||||
// user: None,
|
|
||||||
// message: "Cannot edit a recipe without being logged in",
|
|
||||||
// }
|
|
||||||
// .to_response())
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let recipe = connection.get_recipe_async(id).await?;
|
|
||||||
|
|
||||||
// if recipe.user_id != user.id {
|
|
||||||
// return Ok(MessageTemplate {
|
|
||||||
// message: "Cannot edit a recipe you don't own",
|
|
||||||
// user: Some(user),
|
|
||||||
// }
|
|
||||||
// .to_response());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let recipes = connection.get_all_recipe_titles_async().await?;
|
|
||||||
|
|
||||||
// Ok(EditRecipeTemplate {
|
|
||||||
// user: Some(user),
|
|
||||||
// current_recipe_id: Some(recipe.id),
|
|
||||||
// recipes,
|
|
||||||
// current_recipe: recipe,
|
|
||||||
// }
|
|
||||||
// .to_response())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[get("/recipe/new")]
|
|
||||||
// pub async fn new_recipe(
|
|
||||||
// req: HttpRequest,
|
|
||||||
// connection: web::Data<db::Connection>,
|
|
||||||
// ) -> Result<HttpResponse> {
|
|
||||||
// let user = match get_current_user(&req, connection.clone()).await {
|
|
||||||
// Some(u) => u,
|
|
||||||
// None => {
|
|
||||||
// return Ok(MessageTemplate {
|
|
||||||
// message: "Cannot create a recipe without being logged in",
|
|
||||||
// user: None,
|
|
||||||
// }
|
|
||||||
// .to_response())
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let recipe_id = connection.create_recipe_async(user.id).await?;
|
|
||||||
// let recipes = connection.get_all_recipe_titles_async().await?;
|
|
||||||
// let user_id = user.id;
|
|
||||||
|
|
||||||
// Ok(EditRecipeTemplate {
|
|
||||||
// user: Some(user),
|
|
||||||
// current_recipe_id: Some(recipe_id),
|
|
||||||
// recipes,
|
|
||||||
// current_recipe: model::Recipe::empty(recipe_id, user_id),
|
|
||||||
// }
|
|
||||||
// .to_response())
|
|
||||||
// }
|
|
||||||
|
|
||||||
///// MESSAGE /////
|
///// MESSAGE /////
|
||||||
|
|
||||||
#[derive(Template)]
|
impl<'a> MessageTemplate<'a> {
|
||||||
#[template(path = "message_without_user.html")]
|
pub fn new(message: &'a str) -> MessageTemplate<'a> {
|
||||||
struct MessageWithoutUser<'a> {
|
MessageTemplate {
|
||||||
message: &'a str,
|
user: None,
|
||||||
}
|
message,
|
||||||
|
as_code: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
pub fn new_with_user(message: &'a str, user: Option<model::User>) -> MessageTemplate<'a> {
|
||||||
#[template(path = "message.html")]
|
MessageTemplate {
|
||||||
struct MessageTemplate<'a> {
|
user,
|
||||||
user: Option<model::User>,
|
message,
|
||||||
message: &'a str,
|
as_code: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//// SIGN UP /////
|
//// SIGN UP /////
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "sign_up_form.html")]
|
|
||||||
struct SignUpFormTemplate {
|
|
||||||
user: Option<model::User>,
|
|
||||||
email: String,
|
|
||||||
message: String,
|
|
||||||
message_email: String,
|
|
||||||
message_password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn sign_up_get(
|
pub async fn sign_up_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
|
@ -300,11 +212,10 @@ pub async fn sign_up_post(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(MessageTemplate {
|
Ok(()) => Ok(
|
||||||
user,
|
MessageTemplate::new_with_user(
|
||||||
message: "An email has been sent, follow the link to validate your account.",
|
"An email has been sent, follow the link to validate your account.",
|
||||||
}
|
user).into_response()),
|
||||||
.into_response()),
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// error!("Email validation error: {}", error); // TODO: log
|
// error!("Email validation error: {}", error); // TODO: log
|
||||||
error_response(SignUpError::UnableSendEmail, &form_data, user)
|
error_response(SignUpError::UnableSendEmail, &form_data, user)
|
||||||
|
|
@ -330,10 +241,7 @@ pub async fn sign_up_validation(
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
return Ok((
|
return Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate {
|
MessageTemplate::new_with_user("User already exists", user),
|
||||||
user,
|
|
||||||
message: "User already exists",
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
||||||
|
|
@ -355,48 +263,34 @@ pub async fn sign_up_validation(
|
||||||
let user = connection.load_user(user_id).await?;
|
let user = connection.load_user(user_id).await?;
|
||||||
Ok((
|
Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate {
|
MessageTemplate::new_with_user(
|
||||||
|
"Email validation successful, your account has been created",
|
||||||
user,
|
user,
|
||||||
message: "Email validation successful, your account has been created",
|
),
|
||||||
},
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
db::ValidationResult::ValidationExpired => Ok((
|
db::ValidationResult::ValidationExpired => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate {
|
MessageTemplate::new_with_user(
|
||||||
|
"The validation has expired. Try to sign up again",
|
||||||
user,
|
user,
|
||||||
message: "The validation has expired. Try to sign up again",
|
),
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
db::ValidationResult::UnknownUser => Ok((
|
db::ValidationResult::UnknownUser => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate {
|
MessageTemplate::new_with_user("Validation error. Try to sign up again", user),
|
||||||
user,
|
|
||||||
message: "Validation error. Try to sign up again",
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Ok((
|
None => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate {
|
MessageTemplate::new_with_user("Validation error", user),
|
||||||
user,
|
|
||||||
message: "Validation error",
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///// SIGN IN /////
|
///// SIGN IN /////
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "sign_in_form.html")]
|
|
||||||
struct SignInFormTemplate {
|
|
||||||
user: Option<model::User>,
|
|
||||||
email: String,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn sign_in_get(
|
pub async fn sign_in_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
|
@ -477,24 +371,15 @@ pub async fn sign_out(
|
||||||
|
|
||||||
///// RESET PASSWORD /////
|
///// RESET PASSWORD /////
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "ask_reset_password.html")]
|
|
||||||
struct AskResetPasswordTemplate {
|
|
||||||
user: Option<model::User>,
|
|
||||||
email: String,
|
|
||||||
message: String,
|
|
||||||
message_email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn ask_reset_password_get(
|
pub async fn ask_reset_password_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
Ok(MessageTemplate {
|
Ok(MessageTemplate::new_with_user(
|
||||||
|
"Can't ask to reset password when already logged in",
|
||||||
user,
|
user,
|
||||||
message: "Can't ask to reset password when already logged in",
|
)
|
||||||
}
|
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(AskResetPasswordTemplate {
|
Ok(AskResetPasswordTemplate {
|
||||||
|
|
@ -591,10 +476,10 @@ pub async fn ask_reset_password_post(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(MessageTemplate {
|
Ok(()) => Ok(MessageTemplate::new_with_user(
|
||||||
|
"An email has been sent, follow the link to reset your password.",
|
||||||
user,
|
user,
|
||||||
message: "An email has been sent, follow the link to reset your password.",
|
)
|
||||||
}
|
|
||||||
.into_response()),
|
.into_response()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// error!("Email validation error: {}", error); // TODO: log
|
// error!("Email validation error: {}", error); // TODO: log
|
||||||
|
|
@ -613,15 +498,6 @@ pub async fn ask_reset_password_post(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "reset_password.html")]
|
|
||||||
struct ResetPasswordTemplate {
|
|
||||||
user: Option<model::User>,
|
|
||||||
reset_token: String,
|
|
||||||
message: String,
|
|
||||||
message_password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn reset_password_get(
|
pub async fn reset_password_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
|
@ -636,11 +512,7 @@ pub async fn reset_password_get(
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate {
|
Ok(MessageTemplate::new_with_user("Reset token missing", user).into_response())
|
||||||
user,
|
|
||||||
message: "Reset token missing",
|
|
||||||
}
|
|
||||||
.into_response())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -708,10 +580,10 @@ pub async fn reset_password_post(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(db::ResetPasswordResult::Ok) => Ok(MessageTemplate {
|
Ok(db::ResetPasswordResult::Ok) => Ok(MessageTemplate::new_with_user(
|
||||||
|
"Your password has been reset",
|
||||||
user,
|
user,
|
||||||
message: "Your password has been reset",
|
)
|
||||||
}
|
|
||||||
.into_response()),
|
.into_response()),
|
||||||
Ok(db::ResetPasswordResult::ResetTokenExpired) => {
|
Ok(db::ResetPasswordResult::ResetTokenExpired) => {
|
||||||
error_response(ResetPasswordError::TokenExpired, &form_data, user)
|
error_response(ResetPasswordError::TokenExpired, &form_data, user)
|
||||||
|
|
@ -722,12 +594,6 @@ pub async fn reset_password_post(
|
||||||
|
|
||||||
///// EDIT PROFILE /////
|
///// EDIT PROFILE /////
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "profile.html")]
|
|
||||||
struct ProfileTemplate {
|
|
||||||
user: Option<model::User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn edit_user(
|
pub async fn edit_user(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
|
|
@ -736,18 +602,12 @@ pub async fn edit_user(
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
ProfileTemplate { user }.into_response()
|
ProfileTemplate { user }.into_response()
|
||||||
} else {
|
} else {
|
||||||
MessageTemplate {
|
MessageTemplate::new("Not logged in").into_response()
|
||||||
user: None,
|
|
||||||
message: "Not logged in",
|
|
||||||
}
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///// 404 /////
|
///// 404 /////
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn not_found() -> Result<impl IntoResponse> {
|
pub async fn not_found(Extension(user): Extension<Option<model::User>>) -> impl IntoResponse {
|
||||||
Ok(MessageWithoutUser {
|
MessageTemplate::new_with_user("404: Not found", user)
|
||||||
message: "404: Not found",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,21 +46,15 @@
|
||||||
// #[put("/ron-api/recipe/rm-step")]
|
// #[put("/ron-api/recipe/rm-step")]
|
||||||
// #[put("/ron-api/recipe/set-steps-order")]
|
// #[put("/ron-api/recipe/set-steps-order")]
|
||||||
|
|
||||||
use askama_axum::IntoResponse;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
debug_handler,
|
debug_handler,
|
||||||
extract::{Extension, State},
|
extract::{Extension, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::ErrorResponse,
|
response::{ErrorResponse, IntoResponse, Result},
|
||||||
response::Result,
|
|
||||||
};
|
};
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
||||||
use crate::{
|
use crate::{data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
|
||||||
data::db,
|
|
||||||
model,
|
|
||||||
ron_extractor::{ron_error, ron_response, ExtractRon},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn update_user(
|
pub async fn update_user(
|
||||||
|
|
@ -69,7 +63,14 @@ pub async fn update_user(
|
||||||
ExtractRon(ron): ExtractRon<common::ron_api::UpdateProfile>,
|
ExtractRon(ron): ExtractRon<common::ron_api::UpdateProfile>,
|
||||||
) -> Result<StatusCode> {
|
) -> Result<StatusCode> {
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
// connection.set_user_name(user.id, &ron.name).await?;
|
connection
|
||||||
|
.update_user(
|
||||||
|
user.id,
|
||||||
|
ron.email.as_deref(),
|
||||||
|
ron.name.as_deref(),
|
||||||
|
ron.password.as_deref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
return Err(ErrorResponse::from(ron_error(
|
return Err(ErrorResponse::from(ron_error(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
|
|
@ -79,17 +80,8 @@ pub async fn update_user(
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Example with RON return value.
|
///// 404 /////
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn set_user_name(
|
pub async fn not_found(Extension(user): Extension<Option<model::User>>) -> impl IntoResponse {
|
||||||
State(connection): State<db::Connection>,
|
ron_error(StatusCode::NOT_FOUND, "Not found")
|
||||||
Extension(user): Extension<Option<model::User>>,
|
|
||||||
ExtractRon(ron): ExtractRon<common::ron_api::SetProfileName>,
|
|
||||||
) -> Result<impl IntoResponse> {
|
|
||||||
|
|
||||||
|
|
||||||
Ok(ron_response(common::ron_api::SetProfileName {
|
|
||||||
name: "abc".to_string(),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
@font-face{font-family: Fira Code; font-weight:200; src:url(FiraCode-Light.woff2) format("woff2"); }
|
|
||||||
@font-face{font-family: Fira Code; font-weight:400; src:url(FiraCode-Regular.woff2) format("woff2"); }
|
|
||||||
@font-face{font-family: Fira Code; font-weight:600; src:url(FiraCode-SemiBold.woff2) format("woff2"); }
|
|
||||||
@font-face{font-family: Fira Code; font-weight:700; src:url(FiraCode-Bold.woff2) format("woff2"); }
|
|
||||||
|
|
||||||
$primary: #182430;
|
|
||||||
|
|
||||||
$background: darken($primary, 5%);
|
|
||||||
$background-container: lighten($primary, 10%);
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 10px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 80%
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: lighten($primary, 40%);
|
|
||||||
text-decoration: none;
|
|
||||||
&:hover { color: lighten($primary, 60%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: Fira Code, Helvetica Neue, Helvetica, Arial, sans-serif;
|
|
||||||
text-shadow: 2px 2px 2px rgb(0, 0, 0);
|
|
||||||
text-align: center;
|
|
||||||
// line-height: 18px;
|
|
||||||
color: rgb(255, 255, 255);
|
|
||||||
background-color: $background;
|
|
||||||
margin: 0px;
|
|
||||||
|
|
||||||
.recipe-item {
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-item-current {
|
|
||||||
padding: 3px;
|
|
||||||
border: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
.header-container {
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.main-container {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.list {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
background-color: $background-container;
|
|
||||||
border: 0.1em solid white;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-container {
|
|
||||||
font-size: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: 0px;
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,10 @@
|
||||||
run();
|
run();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
{% block body_container %}{% endblock %}
|
{% block body_container %}{% endblock %}
|
||||||
|
|
||||||
<footer class="footer-container">gburri - 2022</footer>
|
<footer class="footer-container">gburri - 2022</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
<nav class="list">
|
<nav class="recipes-list">
|
||||||
<ul>
|
<ul>
|
||||||
{% for (id, title) in recipes %}
|
{% for (id, title) in recipes %}
|
||||||
<li>
|
<li>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
{% extends "base_with_header.html" %}
|
{% extends "base_with_header.html" %}
|
||||||
|
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
{{ message|markdown }}
|
|
||||||
|
<div class="message">
|
||||||
|
{% if as_code %}
|
||||||
|
<pre><code>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ message|markdown }}
|
||||||
|
|
||||||
|
{% if as_code %}
|
||||||
|
</code></pre>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="/">Go to home</a>
|
<a href="/">Go to home</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block body_container %}
|
|
||||||
{% include "title.html" %}
|
|
||||||
{{ message|markdown }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -2,31 +2,35 @@
|
||||||
|
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
|
|
||||||
<h2>Profile</h2>
|
|
||||||
|
|
||||||
{% match user %}
|
{% match user %}
|
||||||
{% when Some with (user) %}
|
{% when Some with (user) %}
|
||||||
|
|
||||||
<div id="user-edit">
|
<div class="content">
|
||||||
|
|
||||||
<label for="title_field">Name</label>
|
<h1>Profile</h1>
|
||||||
<input
|
|
||||||
id="name_field"
|
<form id="user-edit">
|
||||||
type="text"
|
|
||||||
name="name"
|
<label for="input-name">Name</label>
|
||||||
value="{{ user.name }}"
|
<input
|
||||||
autocapitalize="none"
|
id="input-name"
|
||||||
autocomplete="title"
|
type="text"
|
||||||
autofocus="autofocus" />
|
name="name"
|
||||||
|
value="{{ user.name }}"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="title"
|
||||||
|
autofocus="autofocus" />
|
||||||
|
|
||||||
|
|
||||||
<label for="password_field_1">New password (minimum 8 characters)</label>
|
<label for="input-password-1">New password (minimum 8 characters)</label>
|
||||||
<input id="password_field_1" type="password" name="password_1" />
|
<input id="input-password-1" type="password" name="password_1" />
|
||||||
|
|
||||||
<label for="password_field_1">Re-enter password</label>
|
<label for="input-password-2">Re-enter password</label>
|
||||||
<input id="password_field_2" type="password" name="password_2" />
|
<input id="input-password-2" type="password" name="password_2" />
|
||||||
|
|
||||||
|
<input type="button" value="Save" />
|
||||||
|
</form>
|
||||||
|
|
||||||
<button class="button" typed="button">Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% when None %}
|
{% when None %}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
{% extends "base_with_header.html" %}
|
{% extends "base_with_header.html" %}
|
||||||
|
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
<div class="content">
|
|
||||||
<form action="/signin" method="post">
|
|
||||||
<label for="email_field">Email address</label>
|
|
||||||
<input id="email_field" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
|
||||||
|
|
||||||
<label for="password_field">Password</label>
|
<div id="sign-in" class="content">
|
||||||
<input id="password_field" type="password" name="password" autocomplete="current-password" />
|
|
||||||
|
<h1>Sign in</h1>
|
||||||
|
|
||||||
|
<form action="/signin" method="post">
|
||||||
|
<label for="input-email">Email address</label>
|
||||||
|
<input id="input-email" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
||||||
|
|
||||||
|
<label for="input-password">Password</label>
|
||||||
|
<input id="input-password" type="password" name="password" autocomplete="current-password" />
|
||||||
|
|
||||||
|
<input type="submit" value="Sign in" />
|
||||||
|
</form>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="submit" name="commit" value="Sign in" />
|
|
||||||
</form>
|
|
||||||
{{ message }}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
{% extends "base_with_header.html" %}
|
{% extends "base_with_header.html" %}
|
||||||
|
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/signup" method="post">
|
|
||||||
<label for="email_field">Your email address</label>
|
|
||||||
<input id="email_field" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
|
||||||
{{ message_email }}
|
|
||||||
|
|
||||||
<label for="password_field_1">Choose a password (minimum 8 characters)</label>
|
<h1>Sign up</h1>
|
||||||
<input id="password_field_1" type="password" name="password_1" />
|
|
||||||
|
|
||||||
<label for="password_field_1">Re-enter password</label>
|
<form action="/signup" method="post">
|
||||||
<input id="password_field_2" type="password" name="password_2" />
|
<label for="input-email">Your email address</label>
|
||||||
|
<input id="input-email" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
||||||
|
{{ message_email }}
|
||||||
|
|
||||||
{{ message_password }}
|
<label for="input-password-1">Choose a password (minimum 8 characters)</label>
|
||||||
|
<input id="input-password-1" type="password" name="password_1" />
|
||||||
|
|
||||||
<input type="submit" name="commit" value="Sign up" />
|
<label for="input-password-2">Re-enter password</label>
|
||||||
</form>
|
<input id="input-password-2" type="password" name="password_2" />
|
||||||
{{ message }}
|
|
||||||
</div>
|
{{ message_password }}
|
||||||
|
|
||||||
|
<input type="submit" name="commit" value="Sign up" />
|
||||||
|
</form>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,4 @@ regex = "1"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
|
|
||||||
ron = "0.8"
|
ron = "0.8"
|
||||||
serde = {version = "1.0", features = ["derive"]}
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use ron::{
|
||||||
|
de::from_bytes,
|
||||||
|
ser::{to_string_pretty, PrettyConfig},
|
||||||
|
};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
///// RECIPE /////
|
///// RECIPE /////
|
||||||
|
|
||||||
|
|
@ -102,3 +106,11 @@ pub struct UpdateProfile {
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_string<T>(ron: T) -> String
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
// TODO: handle'unwrap'.
|
||||||
|
to_string_pretty(&ron, PrettyConfig::new()).unwrap()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ default = ["console_error_panic_hook"]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
"console",
|
"console",
|
||||||
"Document",
|
"Document",
|
||||||
|
|
@ -24,8 +25,11 @@ web-sys = { version = "0.3", features = [
|
||||||
"Location",
|
"Location",
|
||||||
"EventTarget",
|
"EventTarget",
|
||||||
"HtmlLabelElement",
|
"HtmlLabelElement",
|
||||||
|
"HtmlInputElement",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
gloo = "0.11"
|
||||||
|
|
||||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||||
# logging them with `console.error`. This is great for development, but requires
|
# logging them with `console.error`. This is great for development, but requires
|
||||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,73 @@
|
||||||
|
use gloo::{console::log, events::EventListener, net::http::Request};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::{Document, HtmlLabelElement};
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use web_sys::{Document, HtmlInputElement};
|
||||||
|
|
||||||
pub fn recipe_edit(doc: &Document) {
|
use crate::toast::{self, Level};
|
||||||
|
|
||||||
|
pub fn recipe_edit(doc: Document) -> Result<(), JsValue> {
|
||||||
let title_input = doc.get_element_by_id("title_field").unwrap();
|
let title_input = doc.get_element_by_id("title_field").unwrap();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_edit(doc: &Document) {
|
pub fn user_edit(doc: Document) -> Result<(), JsValue> {
|
||||||
// let name_input = doc.get_element_by_id("name_field").unwrap().dyn_ref::<>()
|
log!("user_edit");
|
||||||
|
|
||||||
|
let button = doc
|
||||||
|
.query_selector("#user-edit input[type='button']")?
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let on_click_submit = EventListener::new(&button, "click", move |_event| {
|
||||||
|
log!("Click!");
|
||||||
|
|
||||||
|
let input_name = doc.get_element_by_id("input-name").unwrap();
|
||||||
|
let name = input_name.dyn_ref::<HtmlInputElement>().unwrap().value();
|
||||||
|
|
||||||
|
let update_data = common::ron_api::UpdateProfile {
|
||||||
|
name: Some(name),
|
||||||
|
email: None,
|
||||||
|
password: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = common::ron_api::to_string(update_data);
|
||||||
|
|
||||||
|
let doc = doc.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match Request::put("/ron-api/user/update")
|
||||||
|
.header("Content-Type", "application/ron")
|
||||||
|
.body(body)
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resp) => {
|
||||||
|
log!("Status code: {}", resp.status());
|
||||||
|
if resp.status() == 200 {
|
||||||
|
toast::show(Level::Info, "Profile saved", doc);
|
||||||
|
} else {
|
||||||
|
toast::show(
|
||||||
|
Level::Error,
|
||||||
|
&format!(
|
||||||
|
"Status code: {} {}",
|
||||||
|
resp.status(),
|
||||||
|
resp.text().await.unwrap()
|
||||||
|
),
|
||||||
|
doc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
toast::show(
|
||||||
|
Level::Info,
|
||||||
|
&format!("Internal server error: {}", error),
|
||||||
|
doc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
on_click_submit.forget();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
mod handles;
|
mod handles;
|
||||||
|
mod toast;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
use gloo::{console::log, events::EventListener};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::console;
|
use web_sys::console;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
// #[wasm_bindgen]
|
||||||
extern "C" {
|
// extern "C" {
|
||||||
fn alert(s: &str);
|
// fn alert(s: &str);
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[wasm_bindgen]
|
// #[wasm_bindgen]
|
||||||
pub fn greet(name: &str) {
|
// pub fn greet(name: &str) {
|
||||||
alert(&format!("Hello, {}!", name));
|
// alert(&format!("Hello, {}!", name));
|
||||||
console::log_1(&"Hello bg".into());
|
// console::log_1(&"Hello bg".into());
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn main() -> Result<(), JsValue> {
|
pub fn main() -> Result<(), JsValue> {
|
||||||
|
|
@ -33,17 +35,16 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
* - Call (as AJAR) /ron-api/set-title and set the body to a serialized RON of the type common::ron_api::SetTitle
|
* - Call (as AJAR) /ron-api/set-title and set the body to a serialized RON of the type common::ron_api::SetTitle
|
||||||
* - Display error message if needed
|
* - Display error message if needed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
match path[..] {
|
match path[..] {
|
||||||
["recipe", "edit", id] => {
|
["recipe", "edit", id] => {
|
||||||
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
|
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
|
||||||
console_log!("recipe edit ID: {}", id);
|
log!("recipe edit ID: {}", id);
|
||||||
|
|
||||||
handles::recipe_edit(&document);
|
handles::recipe_edit(document)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
["user", "edit"] => {
|
["user", "edit"] => {
|
||||||
handles::user_edit(&document);
|
handles::user_edit(document)?;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
frontend/src/toast.rs
Normal file
20
frontend/src/toast.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
use gloo::{console::log, timers::callback::Timeout};
|
||||||
|
use web_sys::{console, Document, HtmlInputElement};
|
||||||
|
|
||||||
|
pub enum Level {
|
||||||
|
Success,
|
||||||
|
Error,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(level: Level, message: &str, doc: Document) {
|
||||||
|
let toast_element = doc.get_element_by_id("toast").unwrap();
|
||||||
|
toast_element.set_inner_html(message);
|
||||||
|
toast_element.set_class_name("show");
|
||||||
|
|
||||||
|
Timeout::new(4_000, move || {
|
||||||
|
toast_element.set_class_name("");
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use web_sys::console;
|
// use web_sys::console;
|
||||||
|
|
||||||
pub fn set_panic_hook() {
|
pub fn set_panic_hook() {
|
||||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||||
|
|
@ -11,9 +11,9 @@ pub fn set_panic_hook() {
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
// #[macro_export]
|
||||||
macro_rules! console_log {
|
// macro_rules! console_log {
|
||||||
// Note that this is using the `log` function imported above during
|
// // Note that this is using the `log` function imported above during
|
||||||
// `bare_bones`
|
// // `bare_bones`
|
||||||
($($t:tt)*) => (console::log_1(&format_args!($($t)*).to_string().into()))
|
// ($($t:tt)*) => (console::log_1(&format_args!($($t)*).to_string().into()))
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue