Sign in and sign up views
This commit is contained in:
parent
cc0f47321e
commit
353cdd602a
6
.env
6
.env
@ -22,3 +22,9 @@ WEB_HOST=0.0.0.0
|
||||
|
||||
FILES_PUBLIC_PATH=/files
|
||||
FILES_LOCAL_PATH=./tmp
|
||||
|
||||
SONIC_SEARCH_ADDR=0.0.0.0:1491
|
||||
SONIC_SEARCH_PASS=SecretPassword
|
||||
SONIC_INGEST_ADDR=0.0.0.0:1491
|
||||
SONIC_INGEST_PASS=SecretPassword
|
||||
SEARCH_ACTIVE=true
|
||||
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4359,6 +4359,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
@ -7,6 +7,7 @@ use model::{
|
||||
};
|
||||
|
||||
use super::Result;
|
||||
use crate::MultiLoad;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
@ -22,6 +23,8 @@ pub enum Error {
|
||||
ShoppingCartProducts,
|
||||
#[error("Product with id {0} can't be found")]
|
||||
Single(model::ProductId),
|
||||
#[error("Failed to load products for given ids")]
|
||||
FindProducts,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
@ -44,6 +47,7 @@ SELECT id,
|
||||
price,
|
||||
deliver_days_flag
|
||||
FROM products
|
||||
ORDER BY id
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
@ -256,6 +260,7 @@ SELECT products.id,
|
||||
FROM products
|
||||
INNER JOIN shopping_cart_items ON shopping_cart_items.product_id = products.id
|
||||
WHERE shopping_cart_id = $1
|
||||
ORDER BY products.id
|
||||
"#,
|
||||
)
|
||||
.bind(msg.shopping_cart_id)
|
||||
@ -266,3 +271,46 @@ WHERE shopping_cart_id = $1
|
||||
crate::Error::Product(Error::ShoppingCartProducts)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<Vec<model::Product>>")]
|
||||
pub struct FindProducts {
|
||||
pub product_ids: Vec<model::ProductId>,
|
||||
}
|
||||
|
||||
crate::db_async_handler!(
|
||||
FindProducts,
|
||||
find_products,
|
||||
Vec<Product>,
|
||||
inner_find_products
|
||||
);
|
||||
|
||||
pub(crate) async fn find_products(
|
||||
msg: FindProducts,
|
||||
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Vec<model::Product>> {
|
||||
MultiLoad::new(
|
||||
pool,
|
||||
r#"
|
||||
SELECT id,
|
||||
name,
|
||||
short_description,
|
||||
long_description,
|
||||
category,
|
||||
price,
|
||||
deliver_days_flag
|
||||
FROM products
|
||||
WHERE
|
||||
"#,
|
||||
"products.id =",
|
||||
)
|
||||
.load(
|
||||
msg.product_ids.len(),
|
||||
msg.product_ids.into_iter().map(|id| *id),
|
||||
|e| {
|
||||
log::error!("{e:?}");
|
||||
crate::Error::Product(Error::FindProducts)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ pub(crate) async fn search(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(Some(vec![]))
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
#![feature(drain_filter)]
|
||||
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix::Actor;
|
||||
use actix_session::storage::RedisActorSessionStore;
|
||||
@ -18,6 +19,8 @@ use opts::{
|
||||
};
|
||||
use validator::{validate_email, validate_length};
|
||||
|
||||
use crate::opts::ReIndexOpts;
|
||||
|
||||
mod opts;
|
||||
pub mod routes;
|
||||
|
||||
@ -52,7 +55,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
||||
.await
|
||||
.expect("Failed to start payment manager")
|
||||
.start();
|
||||
let search_manager = search_manager::SearchManager::new(app_config.clone());
|
||||
let search_manager = search_manager::SearchManager::new(app_config.clone()).start();
|
||||
let fs_manager = fs_manager::FsManager::build(app_config.clone())
|
||||
.await
|
||||
.expect("Failed to initialize file system storage");
|
||||
@ -164,8 +167,8 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
|
||||
.unwrap();
|
||||
|
||||
db.send(database_manager::CreateAccount {
|
||||
email: Email::from(opts.email),
|
||||
login: Login::from(opts.login),
|
||||
email: Email::from_str(&opts.email).unwrap(),
|
||||
login: Login::new(opts.login),
|
||||
pass_hash: PassHash::from(hash),
|
||||
role,
|
||||
})
|
||||
@ -193,6 +196,39 @@ async fn test_mailer(opts: TestMailerOpts) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reindex(opts: ReIndexOpts) -> Result<()> {
|
||||
let config = config::default_load(&opts);
|
||||
opts.update_config(&mut *config.lock());
|
||||
let db = database_manager::Database::build(config.clone())
|
||||
.await?
|
||||
.start();
|
||||
let search = search_manager::SearchManager::new(config).start();
|
||||
let products: Vec<model::Product> = db
|
||||
.send(database_manager::AllProducts)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
for product in products {
|
||||
search
|
||||
.send(search_manager::CreateIndex {
|
||||
key: product.id.to_string(),
|
||||
value: vec![
|
||||
product.long_description.into_inner(),
|
||||
product.short_description.into_inner(),
|
||||
product.name.into_inner(),
|
||||
]
|
||||
.join(" "),
|
||||
collection: "products".into(),
|
||||
lang: opts.lang.clone(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
println!("Success!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<()> {
|
||||
human_panic::setup_panic!();
|
||||
@ -211,5 +247,6 @@ async fn main() -> Result<()> {
|
||||
config::config_info().await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
Command::ReIndex(opts) => reindex(opts).await,
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ pub enum Command {
|
||||
TestMailer(TestMailerOpts),
|
||||
#[options(help = "Print config information")]
|
||||
ConfigInfo(ConfigInfo),
|
||||
#[options(help = "Perform all search indexing")]
|
||||
ReIndex(ReIndexOpts),
|
||||
}
|
||||
|
||||
impl UpdateConfig for Command {
|
||||
@ -64,7 +66,7 @@ impl UpdateConfig for Command {
|
||||
Command::TestMailer(opts) => {
|
||||
opts.update_config(config);
|
||||
}
|
||||
Command::ConfigInfo(_) => {}
|
||||
Command::ReIndex(..) | Command::ConfigInfo(..) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,6 +77,14 @@ impl Default for Command {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Options, Debug)]
|
||||
pub struct ReIndexOpts {
|
||||
pub help: bool,
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
impl UpdateConfig for ReIndexOpts {}
|
||||
|
||||
#[derive(Options, Debug)]
|
||||
pub struct ConfigInfo {
|
||||
pub help: bool,
|
||||
@ -85,9 +95,7 @@ pub struct GenerateHashOpts {
|
||||
pub help: bool,
|
||||
}
|
||||
|
||||
impl UpdateConfig for GenerateHashOpts {
|
||||
fn update_config(&self, _config: &mut AppConfig) {}
|
||||
}
|
||||
impl UpdateConfig for GenerateHashOpts {}
|
||||
|
||||
#[derive(Options, Debug)]
|
||||
pub struct ServerOpts {
|
||||
|
@ -109,8 +109,13 @@ async fn create_product(
|
||||
);
|
||||
|
||||
search.do_send(search_manager::CreateIndex {
|
||||
key: format!("{}", product.id),
|
||||
value: product.long_description.to_string(),
|
||||
key: product.id.to_string(),
|
||||
value: vec![
|
||||
product.long_description.to_string(),
|
||||
product.short_description.to_string(),
|
||||
product.name.to_string(),
|
||||
]
|
||||
.join(" "),
|
||||
collection: "products".into(),
|
||||
lang: payload.lang,
|
||||
});
|
||||
|
@ -1,16 +1,58 @@
|
||||
use actix::Addr;
|
||||
use actix_web::web::{Data, Json, Path, ServiceConfig};
|
||||
use actix_web::web::{Data, Json, Path, Query, ServiceConfig};
|
||||
use actix_web::{get, post, HttpResponse};
|
||||
use config::SharedAppConfig;
|
||||
use database_manager::{query_db, Database};
|
||||
use model::{api, AccessTokenString, Audience, Encrypt, FullAccount, RefreshTokenString, Token};
|
||||
use model::{api, Encrypt};
|
||||
use payment_manager::{PaymentManager, PaymentNotification};
|
||||
use search_manager::SearchManager;
|
||||
use token_manager::{query_tm, TokenManager};
|
||||
|
||||
use crate::public_send_db;
|
||||
use crate::routes::public::Error as PublicError;
|
||||
use crate::routes::{self, Result};
|
||||
|
||||
#[get("/search")]
|
||||
async fn search(
|
||||
db: Data<Addr<Database>>,
|
||||
_config: Data<SharedAppConfig>,
|
||||
search: Data<Addr<SearchManager>>,
|
||||
query: Query<model::api::SearchRequest>,
|
||||
) -> routes::Result<Json<Vec<model::Product>>> {
|
||||
let q = query.into_inner();
|
||||
let product_ids: Vec<model::ProductId> = match search
|
||||
.send(search_manager::Search {
|
||||
query: q.q,
|
||||
collection: "products".into(),
|
||||
lang: q.lang,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(Some(res))) => res
|
||||
.into_iter()
|
||||
.filter_map(|s| {
|
||||
s.parse::<model::RecordId>()
|
||||
.ok()
|
||||
.map(model::ProductId::from)
|
||||
})
|
||||
.collect(),
|
||||
Ok(Ok(None)) => return Ok(Json(vec![])),
|
||||
Ok(Err(e)) => {
|
||||
log::error!("{e}");
|
||||
return Ok(Json(vec![]));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
return Ok(Json(vec![]));
|
||||
}
|
||||
};
|
||||
Ok(Json(public_send_db!(
|
||||
owned,
|
||||
db,
|
||||
database_manager::FindProducts { product_ids }
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/products")]
|
||||
async fn products(
|
||||
db: Data<Addr<Database>>,
|
||||
@ -117,15 +159,15 @@ pub async fn create_account(
|
||||
}
|
||||
|
||||
pub(crate) struct AuthPair {
|
||||
pub access_token: Token,
|
||||
pub access_token_string: AccessTokenString,
|
||||
pub _refresh_token: Token,
|
||||
pub refresh_token_string: RefreshTokenString,
|
||||
pub access_token: model::Token,
|
||||
pub access_token_string: model::AccessTokenString,
|
||||
pub _refresh_token: model::Token,
|
||||
pub refresh_token_string: model::RefreshTokenString,
|
||||
}
|
||||
|
||||
pub(crate) async fn create_auth_pair(
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
account: FullAccount,
|
||||
account: model::FullAccount,
|
||||
) -> routes::Result<AuthPair> {
|
||||
let (access_token, refresh_token) = query_tm!(
|
||||
multi,
|
||||
@ -135,19 +177,21 @@ pub(crate) async fn create_auth_pair(
|
||||
customer_id: account.customer_id,
|
||||
role: account.role,
|
||||
subject: account.id,
|
||||
audience: Some(Audience::Web),
|
||||
audience: Some(model::Audience::Web),
|
||||
exp: None
|
||||
},
|
||||
token_manager::CreateToken {
|
||||
customer_id: account.customer_id,
|
||||
role: account.role,
|
||||
subject: account.id,
|
||||
audience: Some(Audience::Web),
|
||||
audience: Some(model::Audience::Web),
|
||||
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc())
|
||||
}
|
||||
);
|
||||
let (access_token, access_token_string): (Token, AccessTokenString) = access_token?;
|
||||
let (refresh_token, refresh_token_string): (Token, AccessTokenString) = refresh_token?;
|
||||
let (access_token, access_token_string): (model::Token, model::AccessTokenString) =
|
||||
access_token?;
|
||||
let (refresh_token, refresh_token_string): (model::Token, model::AccessTokenString) =
|
||||
refresh_token?;
|
||||
Ok(AuthPair {
|
||||
access_token,
|
||||
access_token_string,
|
||||
@ -164,7 +208,7 @@ async fn sign_in(
|
||||
) -> Result<Json<api::SignInOutput>> {
|
||||
let db = db.into_inner();
|
||||
|
||||
let account: FullAccount = query_db!(
|
||||
let account: model::FullAccount = query_db!(
|
||||
db,
|
||||
database_manager::AccountByIdentity {
|
||||
login: Some(payload.login),
|
||||
@ -205,6 +249,7 @@ async fn handle_notification(
|
||||
|
||||
pub(crate) fn configure(config: &mut ServiceConfig) {
|
||||
config
|
||||
.service(search)
|
||||
.service(product)
|
||||
.service(products)
|
||||
.service(stocks)
|
||||
|
@ -114,7 +114,13 @@ pub(crate) async fn create_products(
|
||||
"Lexal 128G",
|
||||
model::Category::MEMORY_NAME,
|
||||
None,
|
||||
None
|
||||
Some(r#"Nothing beats a freshly pulled pint in your favourite pub—except maybe a freshly pulled pint in your very own home.
|
||||
|
||||
Never battle with crowds, struggle for a seat, or have to hang about outside on the pavement just to enjoy your favourite beer again! The Fizzics DraftPour gives you nitro-style draft beer from ANY can or bottle. Even the cheapest economy lager can be instantly transformed into a luxurious draft pint with just one pull of the lever.
|
||||
|
||||
The DraftPour may be a sleek piece of kit, but it’s deceptively high tech under the hood, applying sound waves to convert your beer’s natural carbonation into a smooth micro-foam. These diddy little bubbles create the optimal density for enhanced aroma, flavour, and a silky smooth mouth-feel.
|
||||
|
||||
Get a fruit machine and a few boxes of pork scratchings in and you’ve basically completely replicated your local pub. Sticky bar-top and ancient, dubiously-stained carpet not included."#)
|
||||
),
|
||||
create_product(
|
||||
db.clone(),
|
||||
@ -122,7 +128,7 @@ pub(crate) async fn create_products(
|
||||
"Fujifilm X-T10",
|
||||
model::Category::CAMERAS_NAME,
|
||||
None,
|
||||
None
|
||||
Some(r#"The Dauré family own one of the Roussillon’s top properties, the Château de Jau. Around the dinner table one Christmas they agreed it was time to spread their wings and look to new wine horizons. The womenfolk (Las Niñas) fancied Chile and won out in the end, achieving their dream when they established an estate in the Apalta Valley of Colchagua. The terroir is excellent and close neighbours of the Chilean star Montes winery."#)
|
||||
),
|
||||
create_product(
|
||||
db.clone(),
|
||||
|
@ -10,7 +10,7 @@ pub enum Error {}
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub trait UpdateConfig {
|
||||
fn update_config(&self, config: &mut AppConfig);
|
||||
fn update_config(&self, _config: &mut AppConfig) {}
|
||||
}
|
||||
|
||||
trait Example: Sized {
|
||||
|
@ -363,6 +363,13 @@ pub struct CreateItemOutput {
|
||||
pub shopping_cart_item: ShoppingCartItem,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SearchRequest {
|
||||
/// Match string
|
||||
pub q: String,
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
pub mod admin {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -6,7 +6,7 @@ pub mod api;
|
||||
mod dummy;
|
||||
pub mod encrypt;
|
||||
|
||||
use std::fmt::Formatter;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{Deref, Display, From};
|
||||
@ -319,6 +319,10 @@ impl FromStr for Email {
|
||||
Err(TransformError::NotEmail)
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_empty() -> Self {
|
||||
Self("".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Email {
|
||||
@ -665,16 +669,32 @@ impl From<FullAccount> for Account {
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Deref, Display, From)]
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Deref, From)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProductId(RecordId);
|
||||
|
||||
impl Display for ProductId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProductName(String);
|
||||
|
||||
impl ProductName {
|
||||
pub fn new<S: Into<String>>(s: S) -> Self {
|
||||
Self(s.into())
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)]
|
||||
@ -685,6 +705,10 @@ impl ProductShortDesc {
|
||||
pub fn new<S: Into<String>>(s: S) -> Self {
|
||||
Self(s.into())
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
@ -693,6 +717,12 @@ impl ProductShortDesc {
|
||||
#[serde(transparent)]
|
||||
pub struct ProductLongDesc(String);
|
||||
|
||||
impl ProductLongDesc {
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ProductLongDesc {
|
||||
pub fn new<S: Into<String>>(s: S) -> Self {
|
||||
Self(s.into())
|
||||
|
@ -29,6 +29,8 @@ rusty-money = { version = "0.4.1", features = ["iso"] }
|
||||
|
||||
pure-rust-locales = { version = "0.5.6" }
|
||||
|
||||
thiserror = { version = "1.0.31" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 's'
|
||||
|
@ -102,12 +102,21 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
Msg::UrlChanged(subs::UrlChanged(url)) => model.page.page_changed(url, orders),
|
||||
Msg::Public(pages::public::Msg::Listing(msg)) => {
|
||||
let page = fetch_page!(public model, Listing);
|
||||
pages::public::listing::update(msg, page, &mut orders.proxy(proxy_public_listing));
|
||||
pages::public::listing::update(msg, page, &mut orders.proxy(Into::into));
|
||||
}
|
||||
Msg::Public(pages::public::Msg::Product(msg)) => {
|
||||
let page = fetch_page!(public model, Product);
|
||||
pages::public::product::update(msg, page, &mut orders.proxy(proxy_public_product))
|
||||
pages::public::product::update(msg, page, &mut orders.proxy(Into::into))
|
||||
}
|
||||
Msg::Public(pages::public::Msg::SignIn(msg)) => {
|
||||
let page = fetch_page!(public model, SignIn);
|
||||
pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into))
|
||||
}
|
||||
Msg::Public(pages::public::Msg::SignUp(msg)) => {
|
||||
let page = fetch_page!(public model, SignUp);
|
||||
pages::public::sign_up::update(msg, page, &mut orders.proxy(Into::into))
|
||||
}
|
||||
Msg::Admin(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,6 +124,8 @@ fn view(model: &Model) -> Node<Msg> {
|
||||
match &model.page {
|
||||
Page::Public(PublicPage::Listing(page)) => pages::public::listing::view(model, page),
|
||||
Page::Public(PublicPage::Product(page)) => pages::public::product::view(model, page),
|
||||
Page::Public(PublicPage::SignIn(page)) => pages::public::sign_in::view(model, page),
|
||||
Page::Public(PublicPage::SignUp(page)) => pages::public::sign_up::view(model, page),
|
||||
_ => empty![],
|
||||
}
|
||||
}
|
||||
@ -123,11 +134,3 @@ fn view(model: &Model) -> Node<Msg> {
|
||||
pub fn start() {
|
||||
App::start("main", init, update, view);
|
||||
}
|
||||
|
||||
fn proxy_public_listing(msg: pages::public::listing::Msg) -> Msg {
|
||||
Msg::Public(pages::public::Msg::Listing(msg))
|
||||
}
|
||||
|
||||
fn proxy_public_product(msg: pages::public::product::Msg) -> Msg {
|
||||
Msg::Public(pages::public::Msg::Product(msg))
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use crate::shared;
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
Public(public::Msg),
|
||||
Admin(admin::Msg),
|
||||
UrlChanged(subs::UrlChanged),
|
||||
CheckAccessToken,
|
||||
Shared(shared::Msg),
|
||||
@ -24,6 +25,8 @@ pub enum AdminPage {
|
||||
pub enum PublicPage {
|
||||
Listing(public::listing::ListingPage),
|
||||
Product(public::product::ProductPage),
|
||||
SignIn(public::sign_in::SignInPage),
|
||||
SignUp(public::sign_up::SignUpPage),
|
||||
ShoppingCart,
|
||||
Checkout,
|
||||
}
|
||||
@ -38,20 +41,28 @@ impl Page {
|
||||
match url.clone().remaining_path_parts().as_slice() {
|
||||
[] => Self::Public(PublicPage::Listing(public::listing::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Listing(msg))),
|
||||
&mut orders.proxy(Into::into),
|
||||
))),
|
||||
["products", _rest @ ..] => Self::Public(PublicPage::Listing(public::listing::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Listing(msg))),
|
||||
&mut orders.proxy(Into::into),
|
||||
))),
|
||||
["product", _rest @ ..] => Self::Public(PublicPage::Product(public::product::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Product(msg))),
|
||||
&mut orders.proxy(Into::into),
|
||||
))),
|
||||
["sign-in", _rest @ ..] => Self::Public(PublicPage::SignIn(public::sign_in::init(
|
||||
url,
|
||||
&mut orders.proxy(Into::into),
|
||||
))),
|
||||
["sign-up", _rest @ ..] => Self::Public(PublicPage::SignUp(public::sign_up::init(
|
||||
url,
|
||||
&mut orders.proxy(Into::into),
|
||||
))),
|
||||
["admin"] => Self::Admin(AdminPage::Landing),
|
||||
_ => Self::Public(PublicPage::Listing(public::listing::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Listing(msg))),
|
||||
&mut orders.proxy(Into::into),
|
||||
))),
|
||||
}
|
||||
}
|
||||
@ -70,6 +81,14 @@ impl Page {
|
||||
let page = crate::fetch_page!(public page self, Product, Page::init(url, orders));
|
||||
public::product::page_changed(url, page);
|
||||
}
|
||||
["sign-in", ..] => {
|
||||
let page = crate::fetch_page!(public page self, SignIn, Page::init(url, orders));
|
||||
public::sign_in::page_changed(url, page);
|
||||
}
|
||||
["sign-up", ..] => {
|
||||
let page = crate::fetch_page!(public page self, SignUp, Page::init(url, orders));
|
||||
public::sign_up::page_changed(url, page);
|
||||
}
|
||||
["admin"] => {}
|
||||
_ => {}
|
||||
}
|
||||
@ -104,6 +123,14 @@ impl<'a> Urls<'a> {
|
||||
self.base_url().add_path_part("sign-in")
|
||||
}
|
||||
|
||||
pub fn sign_up(self) -> Url {
|
||||
self.base_url().add_path_part("sign-up")
|
||||
}
|
||||
|
||||
pub fn forgot_password(self) -> Url {
|
||||
self.base_url().add_path_part("forgot-password")
|
||||
}
|
||||
|
||||
// Admin
|
||||
pub fn admin_landing(self) -> Url {
|
||||
self.base_url()
|
||||
|
@ -1,2 +1,18 @@
|
||||
mod landing;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {}
|
||||
pub enum Msg {
|
||||
Landing(landing::Msg),
|
||||
}
|
||||
|
||||
impl From<landing::Msg> for Msg {
|
||||
fn from(msg: landing::Msg) -> Self {
|
||||
Self::Landing(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Msg> for crate::Msg {
|
||||
fn from(msg: Msg) -> Self {
|
||||
crate::Msg::Admin(msg)
|
||||
}
|
||||
}
|
||||
|
2
web/src/pages/admin/landing.rs
Normal file
2
web/src/pages/admin/landing.rs
Normal file
@ -0,0 +1,2 @@
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Msg {}
|
@ -1,10 +1,68 @@
|
||||
pub mod listing;
|
||||
pub mod product;
|
||||
pub mod sign_in;
|
||||
pub(crate) mod sign_up;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
Listing(listing::Msg),
|
||||
Product(product::Msg),
|
||||
SignIn(sign_in::Msg),
|
||||
SignUp(sign_up::Msg),
|
||||
}
|
||||
|
||||
impl From<listing::Msg> for Msg {
|
||||
fn from(msg: listing::Msg) -> Self {
|
||||
Self::Listing(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<product::Msg> for Msg {
|
||||
fn from(msg: product::Msg) -> Self {
|
||||
Self::Product(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_in::Msg> for Msg {
|
||||
fn from(msg: sign_in::Msg) -> Self {
|
||||
Self::SignIn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_up::Msg> for Msg {
|
||||
fn from(msg: sign_up::Msg) -> Self {
|
||||
Self::SignUp(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<listing::Msg> for crate::Msg {
|
||||
fn from(msg: listing::Msg) -> Self {
|
||||
crate::Msg::Public(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<product::Msg> for crate::Msg {
|
||||
fn from(msg: product::Msg) -> Self {
|
||||
crate::Msg::Public(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_in::Msg> for crate::Msg {
|
||||
fn from(msg: sign_in::Msg) -> Self {
|
||||
crate::Msg::Public(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_up::Msg> for crate::Msg {
|
||||
fn from(msg: sign_up::Msg) -> Self {
|
||||
crate::Msg::Public(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Msg> for crate::Msg {
|
||||
fn from(msg: Msg) -> Self {
|
||||
crate::Msg::Public(msg)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod layout {
|
||||
|
@ -8,7 +8,7 @@ use crate::pages::Urls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ListingPage {
|
||||
url: Url,
|
||||
pub product_ids: Vec<model::ProductId>,
|
||||
pub products: HashMap<model::ProductId, model::api::Product>,
|
||||
pub errors: Vec<String>,
|
||||
pub categories: Vec<model::api::Category>,
|
||||
@ -25,8 +25,8 @@ pub enum Msg {
|
||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> ListingPage {
|
||||
orders.send_msg(Msg::FetchProducts);
|
||||
let model = ListingPage {
|
||||
filters: url_to_filters(url.clone()),
|
||||
url: url.set_path(&[] as &[&str]),
|
||||
product_ids: vec![],
|
||||
filters: url_to_filters(url),
|
||||
products: Default::default(),
|
||||
errors: vec![],
|
||||
categories: vec![],
|
||||
@ -57,13 +57,15 @@ pub fn page_changed(url: Url, model: &mut ListingPage) {
|
||||
|
||||
fn filter_products(model: &mut ListingPage) {
|
||||
model.visible_products = model
|
||||
.products
|
||||
.product_ids
|
||||
.iter()
|
||||
.filter_map(|(_, p)| {
|
||||
p.category
|
||||
.as_ref()
|
||||
.filter(|c| model.filters.contains(c.key.as_str()))
|
||||
.map(|_| p.id)
|
||||
.filter_map(|id| {
|
||||
model.products.get(id).and_then(|p| {
|
||||
p.category
|
||||
.as_ref()
|
||||
.filter(|c| model.filters.contains(c.key.as_str()))
|
||||
.map(|_| p.id)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@ -88,6 +90,7 @@ pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<Msg>)
|
||||
.into_iter()
|
||||
.collect();
|
||||
model.categories.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
model.product_ids = products.0.iter().map(|p| p.id).collect();
|
||||
model.products = {
|
||||
let len = products.0.len();
|
||||
products
|
||||
@ -108,7 +111,11 @@ pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<Msg>)
|
||||
|
||||
pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
|
||||
let products: Vec<Node<Msg>> = if page.visible_products.is_empty() {
|
||||
page.products.values().map(|p| product(model, p)).collect()
|
||||
page.product_ids
|
||||
.iter()
|
||||
.filter_map(|id| page.products.get(id))
|
||||
.map(|p| product(model, p))
|
||||
.collect()
|
||||
} else {
|
||||
page.visible_products
|
||||
.iter()
|
||||
@ -121,11 +128,11 @@ pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
|
||||
C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"],
|
||||
products
|
||||
]
|
||||
.map_msg(|msg: Msg| crate::Msg::Public(super::Msg::Listing(msg)));
|
||||
.map_msg(Into::into);
|
||||
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(&model, content, Some(&page.categories))
|
||||
super::layout::view(model, content, Some(&page.categories))
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
||||
attrs!["id" => "product-header"],
|
||||
div![description]
|
||||
]
|
||||
].map_msg(map_to_global);
|
||||
].map_msg(Into::into);
|
||||
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
@ -187,7 +187,3 @@ fn image(img: &model::api::Photo) -> Node<Msg> {
|
||||
img![C!["h-64 md:h-80"], attrs!["src" => img.url.as_str()]]
|
||||
]
|
||||
}
|
||||
|
||||
fn map_to_global(msg: Msg) -> crate::Msg {
|
||||
crate::pages::Msg::Public(crate::pages::public::Msg::Product(msg))
|
||||
}
|
||||
|
102
web/src/pages/public/sign_in.rs
Normal file
102
web/src/pages/public/sign_in.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::pages::Urls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
LoginChanged(String),
|
||||
PasswordChanged(String),
|
||||
Submit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignInPage {
|
||||
pub login: model::Login,
|
||||
pub password: model::Password,
|
||||
}
|
||||
|
||||
pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignInPage {
|
||||
SignInPage {
|
||||
login: model::Login::new(""),
|
||||
password: model::Password::new(""),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_changed(_url: Url, _model: &mut SignInPage) {}
|
||||
|
||||
pub fn update(_msg: Msg, _model: &mut SignInPage, _orders: &mut impl Orders<Msg>) {}
|
||||
|
||||
pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
|
||||
let content = div![
|
||||
C!["relative flex flex-col justify-center overflow-hidden"],
|
||||
div![
|
||||
C!["w-full p-6 m-auto bg-white border-t-4 rounded-md shadow-md border-top lg:max-w-md"],
|
||||
h1![C!["text-3xl font-semibold text-center text-indigo-700"], "Logo"],
|
||||
sign_in_form(model, page),
|
||||
p![
|
||||
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
||||
model.i18n.t("Don't have an account?"),
|
||||
a![
|
||||
C!["font-medium text-indigo-600 hover:underline"],
|
||||
attrs![At::Href => Urls::new(model.url.clone().set_path(&[] as &[&str])).sign_up()],
|
||||
" ",
|
||||
model.i18n.t("Sign up")
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
.map_msg(Into::into);
|
||||
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(model, content, None)
|
||||
]
|
||||
}
|
||||
|
||||
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<Msg> {
|
||||
form![
|
||||
C!["mt-6"],
|
||||
ev("submit", |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::Submit
|
||||
}),
|
||||
div![
|
||||
label![attrs!["for" => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")],
|
||||
input![
|
||||
attrs!["type" => "text", "id" => "login"],
|
||||
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::LoginChanged)
|
||||
})
|
||||
]
|
||||
],
|
||||
div![
|
||||
C!["mt-4"],
|
||||
div![
|
||||
label![attrs!["for" => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")],
|
||||
input![attrs!["type" => "password", "id" => "password"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"]],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordChanged)
|
||||
})
|
||||
],
|
||||
a![
|
||||
C!["text-xs text-indigo-600 hover:underline"],
|
||||
attrs![At::Href => Urls::new(model.url.clone().set_path(&[] as &[&str])).forgot_password()],
|
||||
model.i18n.t("Forget Password?"),
|
||||
],
|
||||
div![
|
||||
C!["mt-6"],
|
||||
button![
|
||||
C!["w-full px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-indigo-700 rounded-md hover:bg-indigo-600 focus:outline-none focus:bg-indigo-600"],
|
||||
model.i18n.t("Log in")
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
113
web/src/pages/public/sign_up.rs
Normal file
113
web/src/pages/public/sign_up.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::pages::Urls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
LoginChanged(String),
|
||||
EmailChanged(String),
|
||||
PasswordChanged(String),
|
||||
Submit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignUpPage {
|
||||
pub login: model::Login,
|
||||
pub email: model::Email,
|
||||
pub password: model::Password,
|
||||
}
|
||||
|
||||
pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
||||
SignUpPage {
|
||||
login: model::Login::new(""),
|
||||
email: model::Email::invalid_empty(),
|
||||
password: model::Password::new(""),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_changed(_url: Url, _model: &mut SignUpPage) {}
|
||||
|
||||
pub fn update(_msg: Msg, _model: &mut SignUpPage, _orders: &mut impl Orders<Msg>) {}
|
||||
|
||||
pub fn view(model: &crate::Model, page: &SignUpPage) -> Node<crate::Msg> {
|
||||
let content = div![
|
||||
C!["relative flex flex-col justify-center overflow-hidden"],
|
||||
div![
|
||||
C!["w-full p-6 m-auto bg-white border-t-4 rounded-md shadow-md border-top lg:max-w-md"],
|
||||
h1![C!["text-3xl font-semibold text-center text-indigo-700"], "Logo"],
|
||||
sign_up_form(model, page),
|
||||
p![
|
||||
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
||||
model.i18n.t("Have an account?"),
|
||||
a![
|
||||
C!["font-medium text-indigo-600 hover:underline"],
|
||||
attrs![At::Href => Urls::new(model.url.clone().set_path(&[] as &[&str])).sign_in()],
|
||||
" ",
|
||||
model.i18n.t("Sign in")
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
.map_msg(Into::into);
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(model, content, None)
|
||||
]
|
||||
}
|
||||
|
||||
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
||||
form![
|
||||
C!["mt-6"],
|
||||
ev("submit", |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::Submit
|
||||
}),
|
||||
div![
|
||||
label![attrs!["for" => "email"], C!["block text-sm text-indigo-800"], model.i18n.t("E-Mail")],
|
||||
input![
|
||||
attrs!["type" => "email", "id" => "email"],
|
||||
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::EmailChanged)
|
||||
})
|
||||
]
|
||||
],
|
||||
div![
|
||||
label![attrs!["for" => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")],
|
||||
input![
|
||||
attrs!["type" => "text", "id" => "login"],
|
||||
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::LoginChanged)
|
||||
})
|
||||
]
|
||||
],
|
||||
div![
|
||||
C!["mt-4"],
|
||||
div![
|
||||
label![attrs!["for" => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")],
|
||||
input![attrs!["type" => "password", "id" => "password"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"]],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordChanged)
|
||||
})
|
||||
],
|
||||
div![
|
||||
C!["mt-6"],
|
||||
button![
|
||||
C!["w-full px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-indigo-700 rounded-md hover:bg-indigo-600 focus:outline-none focus:bg-indigo-600"],
|
||||
model.i18n.t("Register")
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user