-* Finish updating profile
- * check password and message error
- * user can change email: add a field + revalidation of new email
* Check position of message error in profile/sign in/sign up with flex grid layout
-* Review the recipe model (SQL)
-* Describe the use cases in details.
- * Define the UI (mockups).
- * Two CSS: one for desktop and one for mobile
- * Use CSS flex/grid to define a good design/layout
- * Define the logic behind each page and action.
-* Implement:
+* Define the UI (mockups).
+ * Two CSS: one for desktop and one for mobile
+ * Use CSS flex/grid to define a good design/layout
+* Drag and drop of steps and groups to define their order
+* Make a search page
+* Use of markdown for some field (how to add markdown as rinja filter?)
+* Quick search left panel by tags ?
+* Make the home page: Define what to display to the user
+* Show existing tags when editing a recipe
+
+[ok] Add support to translations.
+ * Make a Text database (a bit like d-lan.net) and think about translation.
+ * The language is stored in cookie or in user profile if the user is connected
+ * A combobox in the header shows all languages
+[ok] Set a lang cookie (when not connected)
+[ok] User can choose language
+[ok] Implement:
.service(services::edit_recipe)
.service(services::new_recipe)
.service(services::webapi::set_recipe_title)
.service(services::webapi::set_recipe_description)
-* Add support to translations into db model.
-* Make a Text database (a bit like d-lan.net) and think about translation.
-
+[ok] Review the recipe model (SQL)
+[ok] Finish updating profile
+ [ok] check password and message error
+ [ok] user can change email: add a field + revalidation of new email
[ok] Try using WASM for all the client logic (test on editing/creating a recipe)
[ok] How to log error to journalctl or elsewhere + debug log?
[ok] Clean the old code + commit
pub const DB_FILENAME: &str = "recipes.sqlite";
pub const SQL_FILENAME: &str = "sql/version_{VERSION}.sql";
pub const VALIDATION_TOKEN_DURATION: i64 = 60 * 60; // [s]. (1 jour).
+
pub const COOKIE_AUTH_TOKEN_NAME: &str = "auth_token";
+pub const COOKIE_LANG_NAME: &str = "lang";
pub const VALIDATION_PASSWORD_RESET_TOKEN_DURATION: i64 = 60 * 60; // [s]. (1 jour).
pub const MAX_DB_CONNECTION: u32 = 1; // To avoid database lock.
+// TODO: remove, should be replaced by the translation module.
pub static LANGUAGES: LazyLock<[(&str, &str); 2]> = LazyLock::new(|| {
let mut langs = [("Français", "fr"), ("English", "en")];
langs.sort();
consts,
data::model,
hash::{hash, verify_password},
+ services::user,
};
#[derive(Debug)]
})
}
+ pub async fn set_user_lang(&self, user_id: i64, lang: &str) -> Result<()> {
+ sqlx::query("UPDATE [User] SET [lang] = $2 WHERE [id] = $1")
+ .bind(user_id)
+ .bind(lang)
+ .execute(&self.pool)
+ .await
+ .map(|_| ())
+ .map_err(DBError::from)
+ }
+
pub async fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResult> {
self.sign_up_with_given_time(email, password, Utc::now())
.await
}
impl MessageTemplate {
- pub fn new(message: &str, tr: Tr) -> MessageTemplate {
+ pub fn new(message: String, tr: Tr) -> MessageTemplate {
MessageTemplate {
user: None,
tr,
- message: message.to_string(),
+ message,
as_code: false,
}
}
- pub fn new_with_user(message: &str, tr: Tr, user: Option<model::User>) -> MessageTemplate {
+ pub fn new_with_user(message: String, tr: Tr, user: Option<model::User>) -> MessageTemplate {
MessageTemplate {
user,
tr,
- message: message.to_string(),
+ message,
as_code: false,
}
}
let ron_api_routes = Router::new()
// Disabled: update user profile is now made with a post data ('edit_user_post').
// .route("/user/update", put(services::ron::update_user))
+ .route("/set_lang", put(services::ron::set_lang))
.route("/recipe/set_title", put(services::ron::set_recipe_title))
.route(
"/recipe/set_description",
user.lang
} else {
let available_codes = Tr::available_codes();
-
- // TODO: Check cookies before http headers.
-
- let accept_language = req
- .headers()
- .get(axum::http::header::ACCEPT_LANGUAGE)
- .map(|v| v.to_str().unwrap_or_default())
- .unwrap_or_default()
- .split(',')
- .map(|l| l.split('-').next().unwrap_or_default())
- .find_or_first(|l| available_codes.contains(l));
-
- // TODO: Save to cookies.
-
- accept_language.unwrap_or("en").to_string()
+ let jar = CookieJar::from_headers(req.headers());
+ match jar.get(consts::COOKIE_LANG_NAME) {
+ Some(lang) if available_codes.contains(&lang.value()) => lang.value().to_string(),
+ _ => {
+ let accept_language = req
+ .headers()
+ .get(axum::http::header::ACCEPT_LANGUAGE)
+ .map(|v| v.to_str().unwrap_or_default())
+ .unwrap_or_default()
+ .split(',')
+ .map(|l| l.split('-').next().unwrap_or_default())
+ .find_or_first(|l| available_codes.contains(l));
+
+ accept_language.unwrap_or("en").to_string()
+ }
+ }
};
let tr = Tr::new(&language);
- // let jar = CookieJar::from_headers(req.headers());
req.extensions_mut().insert(tr);
Ok(next.run(req).await)
}
) -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
- MessageTemplate::new_with_user("404: Not found", tr, user),
+ MessageTemplate::new_with_user("404: Not found".to_string(), tr, user),
)
}
consts,
data::{db, model},
html_templates::*,
- translation,
+ translation::{self, Sentence},
};
#[debug_handler]
let recipe_id = connection.create_recipe(user.id).await?;
Ok(Redirect::to(&format!("/recipe/edit/{}", recipe_id)).into_response())
} else {
- Ok(MessageTemplate::new("Not logged in", tr).into_response())
+ Ok(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response())
}
}
}
.into_response())
} else {
- Ok(MessageTemplate::new("Not allowed to edit this recipe", tr).into_response())
+ Ok(
+ MessageTemplate::new(tr.t(Sentence::RecipeNotAllowedToEdit), tr)
+ .into_response(),
+ )
}
} else {
- Ok(MessageTemplate::new("Recipe not found", tr).into_response())
+ Ok(MessageTemplate::new(tr.t(Sentence::RecipeNotFound), tr).into_response())
}
} else {
- Ok(MessageTemplate::new("Not logged in", tr).into_response())
+ Ok(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response())
}
}
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
{
return Ok(MessageTemplate::new_with_user(
- &format!("Not allowed the view the recipe {}", recipe_id),
+ tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
tr,
user,
)
}
.into_response())
}
- None => Ok(MessageTemplate::new_with_user(
- &format!("Cannot find the recipe {}", recipe_id),
- tr,
- user,
- )
- .into_response()),
+ None => Ok(
+ MessageTemplate::new_with_user(tr.t(Sentence::RecipeNotFound), tr, user)
+ .into_response(),
+ ),
}
}
use axum::{
debug_handler,
extract::{Extension, Query, State},
- http::StatusCode,
+ http::{HeaderMap, StatusCode},
response::{ErrorResponse, IntoResponse, Result},
};
+use axum_extra::extract::cookie::{Cookie, CookieJar};
use serde::Deserialize;
// use tracing::{event, Level};
use crate::{
+ consts,
data::db,
model,
ron_extractor::ExtractRon,
id: i64,
}
-#[allow(dead_code)]
+// #[allow(dead_code)]
+// #[debug_handler]
+// pub async fn update_user(
+// State(connection): State<db::Connection>,
+// Extension(user): Extension<Option<model::User>>,
+// ExtractRon(ron): ExtractRon<common::ron_api::UpdateProfile>,
+// ) -> Result<StatusCode> {
+// if let Some(user) = user {
+// connection
+// .update_user(
+// user.id,
+// ron.email.as_deref().map(str::trim),
+// ron.name.as_deref(),
+// ron.password.as_deref(),
+// )
+// .await?;
+// } else {
+// return Err(ErrorResponse::from(ron_error(
+// StatusCode::UNAUTHORIZED,
+// NOT_AUTHORIZED_MESSAGE,
+// )));
+// }
+// Ok(StatusCode::OK)
+// }
+
#[debug_handler]
-pub async fn update_user(
+pub async fn set_lang(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
- ExtractRon(ron): ExtractRon<common::ron_api::UpdateProfile>,
-) -> Result<StatusCode> {
+ headers: HeaderMap,
+ ExtractRon(ron): ExtractRon<common::ron_api::SetLang>,
+) -> Result<(CookieJar, StatusCode)> {
+ let mut jar = CookieJar::from_headers(&headers);
if let Some(user) = user {
- connection
- .update_user(
- user.id,
- ron.email.as_deref().map(str::trim),
- ron.name.as_deref(),
- ron.password.as_deref(),
- )
- .await?;
+ connection.set_user_lang(user.id, &ron.lang).await?;
} else {
- return Err(ErrorResponse::from(ron_error(
- StatusCode::UNAUTHORIZED,
- NOT_AUTHORIZED_MESSAGE,
- )));
+ let cookie = Cookie::build((consts::COOKIE_LANG_NAME, ron.lang)).path("/");
+ jar = jar.add(cookie);
}
- Ok(StatusCode::OK)
+ Ok((jar, StatusCode::OK))
}
async fn check_user_rights_recipe(
let email = form_data.email.clone();
match email::send_email(
&email,
- &format!(
- "Follow this link to confirm your inscription: {}/validation?validation_token={}",
- url, token
+ &tr.tp(
+ Sentence::SignUpFollowEmailLink,
+ &[Box::new(format!(
+ "{}/validation?validation_token={}",
+ url, token
+ ))],
),
&config.smtp_relay_address,
&config.smtp_login,
)
.await
{
- Ok(()) => Ok(
- MessageTemplate::new_with_user(
- "An email has been sent, follow the link to validate your account",
- tr, user).into_response()),
+ Ok(()) => {
+ Ok(
+ MessageTemplate::new_with_user(tr.t(Sentence::SignUpEmailSent), tr, user)
+ .into_response(),
+ )
+ }
Err(_) => {
// error!("Email validation error: {}", error); // TODO: log
error_response(SignUpError::UnableSendEmail, &form_data, user, tr)
if user.is_some() {
return Ok((
jar,
- MessageTemplate::new_with_user("User already exists", tr, user),
+ MessageTemplate::new_with_user(tr.t(Sentence::ValidationUserAlreadyExists), tr, user),
));
}
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
Ok((
jar,
MessageTemplate::new_with_user(
- "Email validation successful, your account has been created",
+ tr.t(Sentence::SignUpEmailValidationSuccess),
tr,
user,
),
db::user::ValidationResult::ValidationExpired => Ok((
jar,
MessageTemplate::new_with_user(
- "The validation has expired. Try to sign up again",
+ tr.t(Sentence::SignUpValidationExpired),
tr,
user,
),
db::user::ValidationResult::UnknownUser => Ok((
jar,
MessageTemplate::new_with_user(
- "Validation error. Try to sign up again",
+ tr.t(Sentence::SignUpValidationErrorTryAgain),
tr,
user,
),
}
None => Ok((
jar,
- MessageTemplate::new_with_user("Validation error", tr, user),
+ MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user),
)),
}
}
Extension(tr): Extension<translation::Tr>,
) -> Result<Response> {
if user.is_some() {
- Ok(MessageTemplate::new_with_user(
- "Can't ask to reset password when already logged in",
- tr,
- user,
+ Ok(
+ MessageTemplate::new_with_user(tr.t(Sentence::AskResetAlreadyLoggedInError), tr, user)
+ .into_response(),
)
- .into_response())
} else {
Ok(AskResetPasswordTemplate {
user,
) -> Result<Response> {
Ok(AskResetPasswordTemplate {
user,
- tr,
email: email.to_string(),
message_email: match error {
- AskResetPasswordError::InvalidEmail => "Invalid email",
- _ => "",
- }
- .to_string(),
+ AskResetPasswordError::InvalidEmail => tr.t(Sentence::InvalidEmail),
+ _ => String::new(),
+ },
message: match error {
AskResetPasswordError::EmailAlreadyReset => {
- "The password has already been reset for this email"
+ tr.t(Sentence::AskResetEmailAlreadyResetError)
}
- AskResetPasswordError::EmailUnknown => "Email unknown",
- AskResetPasswordError::UnableSendEmail => "Unable to send the reset password email",
- AskResetPasswordError::DatabaseError => "Database error",
- _ => "",
- }
- .to_string(),
+ AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown),
+ AskResetPasswordError::UnableSendEmail => tr.t(Sentence::UnableToSendResetEmail),
+ AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
+ _ => String::new(),
+ },
+ tr,
}
.into_response())
}
let url = utils::get_url_from_host(&host);
match email::send_email(
&form_data.email,
- &format!(
- "Follow this link to reset your password: {}/reset_password?reset_token={}",
- url, token
+ &tr.tp(
+ Sentence::AskResetFollowEmailLink,
+ &[Box::new(format!(
+ "{}/reset_password?reset_token={}",
+ url, token
+ ))],
),
&config.smtp_relay_address,
&config.smtp_login,
)
.await
{
- Ok(()) => Ok(MessageTemplate::new_with_user(
- "An email has been sent, follow the link to reset your password.",
- tr,
- user,
- )
- .into_response()),
+ Ok(()) => {
+ Ok(
+ MessageTemplate::new_with_user(tr.t(Sentence::AskResetEmailSent), tr, user)
+ .into_response(),
+ )
+ }
Err(_) => {
// error!("Email validation error: {}", error); // TODO: log
error_response(
}
.into_response())
} else {
- Ok(MessageTemplate::new_with_user("Reset token missing", tr, user).into_response())
+ Ok(
+ MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
+ .into_response(),
+ )
}
}
) -> Result<Response> {
Ok(ResetPasswordTemplate {
user,
- tr,
reset_token: form_data.reset_token.clone(),
message_password: match error {
- ResetPasswordError::PasswordsNotEqual => "Passwords don't match",
- ResetPasswordError::InvalidPassword => {
- "Password must have at least eight characters"
- }
- _ => "",
- }
- .to_string(),
+ ResetPasswordError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
+ ResetPasswordError::InvalidPassword => tr.tp(
+ Sentence::InvalidPassword,
+ &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
+ ),
+ _ => String::new(),
+ },
message: match error {
- ResetPasswordError::TokenExpired => "Token expired, try to reset password again",
- ResetPasswordError::DatabaseError => "Database error",
- _ => "",
- }
- .to_string(),
+ ResetPasswordError::TokenExpired => tr.t(Sentence::AskResetTokenExpired),
+ ResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
+ _ => String::new(),
+ },
+ tr,
}
.into_response())
}
{
Ok(db::user::ResetPasswordResult::Ok) => {
Ok(
- MessageTemplate::new_with_user("Your password has been reset", tr, user)
+ MessageTemplate::new_with_user(tr.t(Sentence::PasswordReset), tr, user)
.into_response(),
)
}
}
.into_response()
} else {
- MessageTemplate::new("Not logged in", tr).into_response()
+ MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response()
}
}
username: form_data.name.clone(),
email: form_data.email.clone(),
message_email: match error {
- ProfileUpdateError::InvalidEmail => "Invalid email",
- ProfileUpdateError::EmailAlreadyTaken => "Email already taken",
- _ => "",
- }
- .to_string(),
+ ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail),
+ ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken),
+ _ => String::new(),
+ },
message_password: match error {
- ProfileUpdateError::PasswordsNotEqual => "Passwords don't match",
- ProfileUpdateError::InvalidPassword => {
- "Password must have at least eight characters"
- }
- _ => "",
- }
- .to_string(),
+ ProfileUpdateError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
+ ProfileUpdateError::InvalidPassword => tr.tp(
+ Sentence::InvalidPassword,
+ &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
+ ),
+ _ => String::new(),
+ },
message: match error {
- ProfileUpdateError::DatabaseError => "Database error",
- ProfileUpdateError::UnableSendEmail => "Unable to send the validation email",
- _ => "",
- }
- .to_string(),
+ ProfileUpdateError::DatabaseError => tr.t(Sentence::DatabaseError),
+ ProfileUpdateError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
+ _ => String::new(),
+ },
tr,
}
.into_response())
};
let email_trimmed = form_data.email.trim();
- let message: &str;
+ let message: String;
match connection
.update_user(
let email = form_data.email.clone();
match email::send_email(
&email,
- &format!(
- "Follow this link to validate this email address: {}/revalidation?validation_token={}",
- url, token
+ &tr.tp(
+ Sentence::ProfileFollowEmailLink,
+ &[Box::new(format!(
+ "{}/revalidation?validation_token={}",
+ url, token
+ ))],
),
&config.smtp_relay_address,
&config.smtp_login,
.await
{
Ok(()) => {
- message =
- "An email has been sent, follow the link to validate your new email";
+ message = tr.t(Sentence::ProfileEmailSent);
}
Err(_) => {
// error!("Email validation error: {}", error); // TODO: log
return error_response(
- ProfileUpdateError::UnableSendEmail, &form_data, user, tr);
+ ProfileUpdateError::UnableSendEmail,
+ &form_data,
+ user,
+ tr,
+ );
}
}
}
Ok(db::user::UpdateUserResult::Ok) => {
- message = "Profile saved";
+ message = tr.t(Sentence::ProfileSaved);
}
Err(_) => {
return error_response(ProfileUpdateError::DatabaseError, &form_data, user, tr)
user,
username: form_data.name,
email: form_data.email,
- message: message.to_string(),
+ message,
message_email: String::new(),
message_password: String::new(),
tr,
}
.into_response())
} else {
- Ok(MessageTemplate::new("Not logged in", tr).into_response())
+ Ok(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response())
}
}
if user.is_some() {
return Ok((
jar,
- MessageTemplate::new_with_user("User already exists", tr, user),
+ MessageTemplate::new_with_user(tr.t(Sentence::ValidationUserAlreadyExists), tr, user),
));
}
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
let user = connection.load_user(user_id).await?;
Ok((
jar,
- MessageTemplate::new_with_user("Email validation successful", tr, user),
+ MessageTemplate::new_with_user(
+ tr.t(Sentence::ValidationSuccessful),
+ tr,
+ user,
+ ),
))
}
db::user::ValidationResult::ValidationExpired => Ok((
jar,
- MessageTemplate::new_with_user(
- "The validation has expired. Try to sign up again with the same email",
- tr,
- user,
- ),
+ MessageTemplate::new_with_user(tr.t(Sentence::ValidationExpired), tr, user),
)),
db::user::ValidationResult::UnknownUser => Ok((
jar,
MessageTemplate::new_with_user(
- "Validation error. Try to sign up again with the same email",
+ tr.t(Sentence::ValidationErrorTryToSignUpAgain),
tr,
user,
),
}
None => Ok((
jar,
- MessageTemplate::new_with_user("Validation error", tr, user),
+ MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user),
)),
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Clone)]
pub enum Sentence {
- ProfileTitle,
MainTitle,
CreateNewRecipe,
UnpublishedRecipes,
UntitledRecipe,
+ Name,
EmailAddress,
Password,
+ SignOut,
+ Save,
+ NotLoggedIn,
+
+ DatabaseError,
+
// Sign in page.
SignInMenu,
SignInTitle,
SignUpMenu,
SignUpTitle,
SignUpButton,
+ SignUpEmailSent,
+ SignUpFollowEmailLink,
+ SignUpEmailValidationSuccess,
+ SignUpValidationExpired,
+ SignUpValidationErrorTryAgain,
ChooseAPassword,
ReEnterPassword,
AccountMustBeValidatedFirst,
EmailAlreadyTaken,
UnableToSendEmail,
+ // Validation.
+ ValidationSuccessful,
+ ValidationExpired,
+ ValidationErrorTryToSignUpAgain,
+ ValidationError,
+ ValidationUserAlreadyExists,
+
// Reset password page.
LostPassword,
AskResetButton,
+ AskResetAlreadyLoggedInError,
+ AskResetEmailAlreadyResetError,
+ AskResetFollowEmailLink,
+ AskResetEmailSent,
+ AskResetTokenMissing,
+ AskResetTokenExpired,
+ PasswordReset,
+ EmailUnknown,
+ UnableToSendResetEmail,
+
+ // Profile
+ ProfileTitle,
+ ProfileEmail,
+ ProfileNewPassword,
+ ProfileFollowEmailLink,
+ ProfileEmailSent,
+ ProfileSaved,
+
+ // Recipe.
+ RecipeNotAllowedToEdit,
+ RecipeNotAllowedToView,
+ RecipeNotFound,
}
#[derive(Clone)]
}
}
- pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString>]) -> String {
+ pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
match self.lang.translation.get(&sentence) {
Some(str) => {
let mut result = str.clone();
}
}
+ pub fn current_lang_code(&self) -> &str {
+ &self.lang.code
+ }
+
pub fn available_languages() -> Vec<(&'static str, &'static str)> {
TRANSLATIONS
.iter()
{% block main_container %}
<div class="content">
- <h1></h1>
+ <h1>{{ tr.t(Sentence::LostPassword) }}</h1>
<form action="/ask_reset_password" 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" />
+ <label for="email_field">{{ tr.t(Sentence::EmailAddress) }}</label>
+ <input id="email_field" type="email"
+ name="email" value="{{ email }}"
+ autocapitalize="none" autocomplete="email" autofocus="autofocus" />
{{ message_email }}
- <input type="submit" name="commit" value="Ask reset" />
+ <input type="submit" name="commit" value="{{ tr.t(Sentence::AskResetButton) }}" />
</form>
{{ message }}
</div>
{% else %}
{{ user.name }}
{% endif %}
- </a> / <a href="/signout" />Sign out</a></span>
+ </a> / <a href="/signout" />{{ tr.t(Sentence::SignOut) }}</a></span>
{% when None %}
<span>
<a href="/signin" >{{ tr.t(Sentence::SignInMenu) }}</a>/<a href="/signup">{{ tr.t(Sentence::SignUpMenu) }}</a>/<a href="/ask_reset_password">{{ tr.t(Sentence::LostPassword) }}</a>
</span>
{% endmatch %}
+ <select id="select-website-language">
+ {% for lang in Tr::available_languages() %}
+ <option value="{{ lang.0 }}"
+ {%+ if tr.current_lang_code() == lang.0 %}
+ selected
+ {% endif %}
+ >{{ lang.1 }}</option>
+ {% endfor %}
+ </select>
</div>
+
<div class="main-container">
{% block main_container %}{% endblock %}
</div>
+
{% endblock %}
\ No newline at end of file
<form action="/user/edit" method="post">
- <label for="input-name">Name</label>
+ <label for="input-name">{{ tr.t(Sentence::Name) }}</label>
<input
id="input-name"
type="text"
autocomplete="title"
autofocus="autofocus" />
- <label for="input-email">Email (need to be revalidated if changed)</label>
+ <label for="input-email">{{ tr.t(Sentence::ProfileEmail) }}</label>
<input id="input-email" type="email"
name="email" value="{{ email }}"
autocapitalize="none" autocomplete="email" autofocus="autofocus" />
{{ message_email }}
- <label for="input-password-1">New password (minimum 8 characters)</label>
+ <label for="input-password-1">{{ tr.tp(Sentence::ProfileNewPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}</label>
<input id="input-password-1" type="password" name="password_1" autocomplete="new-password" />
- <label for="input-password-2">Re-enter password</label>
+ <label for="input-password-2">{{ tr.t(Sentence::ReEnterPassword) }}</label>
<input id="input-password-2" type="password" name="password_2" autocomplete="new-password" />
{{ message_password }}
- <input type="submit" name="commit" value="Save" />
+ <input type="submit" name="commit" value="{{ tr.t(Sentence::Save) }}" />
</form>
{{ message }}
</div>
code: "en",
name: "English",
translation: {
- ProfileTitle: "Profile",
MainTitle: "Cooking Recipes",
CreateNewRecipe: "Create a new recipe",
UnpublishedRecipes: "Unpublished recipes",
UntitledRecipe: "Untitled recipe",
+ Name: "Name",
EmailAddress: "Email address",
Password: "Password",
+ SignOut: "Sign out",
+ Save: "Save",
+ NotLoggedIn: "No logged in",
+
+ DatabaseError: "Database error",
+
SignInMenu: "Sign in",
SignInTitle: "Sign in",
SignInButton: "Sign in",
EmailAlreadyTaken: "This email is not available",
UnableToSendEmail: "Unable to send the validation email",
+ ValidationSuccessful: "Email validation successful",
+ ValidationExpired: "The validation has expired. Try to sign up again with the same email",
+ ValidationErrorTryToSignUpAgain: "Validation error. Try to sign up again with the same email",
+ ValidationError: "Validation error",
+ ValidationUserAlreadyExists: "User already exists",
+
SignUpMenu: "Sign up",
SignUpTitle: "Sign up",
SignUpButton: "Sign up",
+ SignUpEmailSent: "An email has been sent, follow the link to validate your account",
+ SignUpFollowEmailLink: "Follow this link to confirm your inscription: {}",
+ SignUpEmailValidationSuccess: "Email validation successful, your account has been created",
+ SignUpValidationExpired: "The validation has expired. Try to sign up again",
+ SignUpValidationErrorTryAgain: "Validation error. Try to sign up again",
ChooseAPassword: "Choose a password (minimum {} characters)",
ReEnterPassword: "Re-enter password",
LostPassword: "Lost password",
AskResetButton: "Ask reset",
+ AskResetAlreadyLoggedInError: "Can't ask to reset password when already logged in",
+ AskResetEmailAlreadyResetError: "The password has already been reset for this email",
+ AskResetFollowEmailLink: "Follow this link to reset your password: {}",
+ AskResetEmailSent: "An email has been sent, follow the link to reset your password",
+ AskResetTokenMissing: "Reset token missing",
+ AskResetTokenExpired: "Token expired, try to reset password again",
+ PasswordReset: "Your password has been reset",
+ EmailUnknown: "Email unknown",
+ UnableToSendResetEmail: "Unable to send the reset password email",
+
+ ProfileTitle: "Profile",
+ ProfileEmail: "Email (need to be revalidated if changed)",
+ ProfileNewPassword: "New password (minimum {} characters)",
+ ProfileFollowEmailLink: "Follow this link to validate this email address: {}",
+ ProfileEmailSent: "An email has been sent, follow the link to validate your new email",
+ ProfileSaved: "Profile saved",
+
+ RecipeNotAllowedToEdit: "Not allowed to edit this recipe",
+ RecipeNotAllowedToView: "Not allowed the view the recipe {}",
+ RecipeNotFound: "Recipe not found",
}
),
(
code: "fr",
name: "Français",
translation: {
- ProfileTitle: "Profile",
- MainTitle: "Recette de Cuisine",
+ MainTitle: "Recettes de Cuisine",
CreateNewRecipe: "Créer une nouvelle recette",
UnpublishedRecipes: "Recettes non-publiés",
UntitledRecipe: "Recette sans nom",
+ Name: "Nom",
EmailAddress: "Adresse email",
Password: "Mot de passe",
+ SignOut: "Se déconnecter",
+ Save: "Sauvegarder",
+ NotLoggedIn: "Pas connecté",
+
+ DatabaseError: "Erreur de la base de données",
+
SignInMenu: "Se connecter",
SignInTitle: "Se connecter",
SignInButton: "Se connecter",
EmailAlreadyTaken: "Cette adresse email n'est pas disponible",
UnableToSendEmail: "L'email de validation n'a pas pu être envoyé",
+ ValidationSuccessful: "Email validé avec succès",
+ ValidationExpired: "La validation a expiré. Essayez de vous inscrire à nouveau avec la même adresse email",
+ ValidationErrorTryToSignUpAgain: "Erreur de validation. Essayez de vous inscrire à nouveau avec la même adresse email",
+ ValidationError: "Erreur de validation",
+ ValidationUserAlreadyExists: "Utilisateur déjà existant",
+
SignUpMenu: "S'inscrire",
SignUpTitle: "Inscription",
SignUpButton: "Valider",
+ SignUpEmailSent: "Un email a été envoyé, suivez le lien pour valider votre compte",
+ SignUpFollowEmailLink: "Suivez ce lien pour valider votre inscription: {}",
+ SignUpEmailValidationSuccess: "La validation de votre email s'est déroulée avec succès, votre compte a été créé",
+ SignUpValidationExpired: "La validation a expiré. Essayez de vous inscrire à nouveau",
+ SignUpValidationErrorTryAgain: "Erreur de validation. Essayez de vous inscrire à nouveau",
ChooseAPassword: "Choisir un mot de passe (minimum {} caractères)",
ReEnterPassword: "Entrez à nouveau le mot de passe",
LostPassword: "Mot de passe perdu",
AskResetButton: "Demander la réinitialisation",
+ AskResetAlreadyLoggedInError: "Impossible de demander une réinitialisation du mot de passe lorsque déjà connecté",
+ AskResetEmailAlreadyResetError: "Le mot de passe a déjà été réinitialisé pour cette adresse email",
+ AskResetFollowEmailLink: "Suivez ce lien pour réinitialiser votre mot de passe: {}",
+ AskResetEmailSent: "Un email a été envoyé, suivez le lien pour réinitialiser votre mot de passe",
+ AskResetTokenMissing: "Jeton de réinitialisation manquant",
+ AskResetTokenExpired: "Jeton expiré, essayez de réinitialiser votre mot de passe à nouveau",
+ PasswordReset: "Votre mot de passe a été réinitialisé",
+ EmailUnknown: "Email inconnu",
+ UnableToSendResetEmail: "Impossible d'envoyer l'email pour la réinitialisation du mot de passe",
+
+ ProfileTitle: "Profile",
+ ProfileEmail: "Email (doit être revalidé si changé)",
+ ProfileNewPassword: "Nouveau mot de passe (minimum {} caractères)",
+ ProfileFollowEmailLink: "Suivez ce lien pour valider l'adresse email: {}",
+ ProfileEmailSent: "Un email a été envoyé, suivez le lien pour valider la nouvelle adresse email",
+ ProfileSaved: "Profile sauvegardé",
+
+ RecipeNotAllowedToEdit: "Vous n'êtes pas autorisé à éditer cette recette",
+ RecipeNotAllowedToView: "Vous n'êtes pas autorisé à voir la recette {}",
+ RecipeNotFound: "Recette non-trouvée",
}
)
]
\ No newline at end of file
use ron::ser::{to_string_pretty, PrettyConfig};
use serde::{Deserialize, Serialize};
+#[derive(Serialize, Deserialize, Clone)]
+pub struct SetLang {
+ pub lang: String,
+}
+
///// RECIPE /////
#[derive(Serialize, Deserialize, Clone)]
mod toast;
mod utils;
-use gloo::utils::window;
+use gloo::{
+ console::log,
+ events::EventListener,
+ utils::{document, window},
+};
+use utils::by_id;
use wasm_bindgen::prelude::*;
+use wasm_bindgen_futures::spawn_local;
+use web_sys::HtmlSelectElement;
+
+use common::ron_api;
// #[wasm_bindgen]
// extern "C" {
// }
}
+ let select_language: HtmlSelectElement = by_id("select-website-language");
+ EventListener::new(&select_language.clone(), "input", move |_event| {
+ let lang = select_language.value();
+ let body = ron_api::SetLang { lang };
+ spawn_local(async move {
+ let _ = request::put::<(), _>("set_lang", body).await;
+ let _ = window().location().reload();
+ });
+
+ // log!(lang);
+ })
+ .forget();
+
Ok(())
}