diff options
| author | Akshay <[email protected]> | 2020-12-26 05:21:46 +0000 |
|---|---|---|
| committer | Akshay <[email protected]> | 2020-12-26 05:21:46 +0000 |
| commit | 8014def1a8da3397d78d1162f9e1b8c3f22d0322 (patch) | |
| tree | 346e1de0ac6aa4ca973c1b3e5897c2c44948e5a8 | |
| parent | 7c6006e1abc6094b5922ab69ccfa5449b8dbbc99 (diff) | |
add transactions and quantities
- backend exposes endpoints to perform transactions
- frontend introduces a new page to "checkout" cart
| -rw-r--r-- | backend/migrations/2020-12-25-041256_cart_quantity/down.sql | 4 | ||||
| -rw-r--r-- | backend/migrations/2020-12-25-041256_cart_quantity/up.sql | 3 | ||||
| -rw-r--r-- | backend/migrations/2020-12-25-150728_transaction_date/down.sql | 4 | ||||
| -rw-r--r-- | backend/migrations/2020-12-25-150728_transaction_date/up.sql | 4 | ||||
| -rw-r--r-- | backend/src/bin/server.rs | 18 | ||||
| -rw-r--r-- | backend/src/handlers/cart_items.rs | 131 | ||||
| -rw-r--r-- | backend/src/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | backend/src/handlers/transaction.rs | 74 | ||||
| -rw-r--r-- | backend/src/handlers/users.rs | 4 | ||||
| -rw-r--r-- | backend/src/models.rs | 28 | ||||
| -rw-r--r-- | backend/src/schema.rs | 2 | ||||
| -rw-r--r-- | frontend/src/Cart.elm | 76 | ||||
| -rw-r--r-- | frontend/src/Catalog.elm | 16 | ||||
| -rw-r--r-- | frontend/src/Checkout.elm | 126 | ||||
| -rw-r--r-- | frontend/src/Main.elm | 39 |
15 files changed, 485 insertions, 45 deletions
diff --git a/backend/migrations/2020-12-25-041256_cart_quantity/down.sql b/backend/migrations/2020-12-25-041256_cart_quantity/down.sql new file mode 100644 index 0000000..94aec7a --- /dev/null +++ b/backend/migrations/2020-12-25-041256_cart_quantity/down.sql | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | -- This file should undo anything in `up.sql` | ||
| 2 | |||
| 3 | alter table cart_items | ||
| 4 | drop column quantity; | ||
diff --git a/backend/migrations/2020-12-25-041256_cart_quantity/up.sql b/backend/migrations/2020-12-25-041256_cart_quantity/up.sql new file mode 100644 index 0000000..314c11c --- /dev/null +++ b/backend/migrations/2020-12-25-041256_cart_quantity/up.sql | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | -- Your SQL goes here | ||
| 2 | alter table cart_items | ||
| 3 | add quantity integer default 1; | ||
diff --git a/backend/migrations/2020-12-25-150728_transaction_date/down.sql b/backend/migrations/2020-12-25-150728_transaction_date/down.sql new file mode 100644 index 0000000..18fe306 --- /dev/null +++ b/backend/migrations/2020-12-25-150728_transaction_date/down.sql | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | -- This file should undo anything in `up.sql` | ||
| 2 | |||
| 3 | alter table transaction | ||
| 4 | drop column order_date; | ||
diff --git a/backend/migrations/2020-12-25-150728_transaction_date/up.sql b/backend/migrations/2020-12-25-150728_transaction_date/up.sql new file mode 100644 index 0000000..76f9820 --- /dev/null +++ b/backend/migrations/2020-12-25-150728_transaction_date/up.sql | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | -- Your SQL goes here | ||
| 2 | |||
| 3 | alter table transaction | ||
| 4 | add order_date date not null default curdate(); | ||
diff --git a/backend/src/bin/server.rs b/backend/src/bin/server.rs index 7c67e4f..310914e 100644 --- a/backend/src/bin/server.rs +++ b/backend/src/bin/server.rs | |||
| @@ -5,7 +5,7 @@ use actix_web::{web, App, HttpServer}; | |||
| 5 | use diesel::r2d2::{ConnectionManager, Pool}; | 5 | use diesel::r2d2::{ConnectionManager, Pool}; |
| 6 | use diesel::MysqlConnection; | 6 | use diesel::MysqlConnection; |
| 7 | use furby::handlers::smoke::manual_hello; | 7 | use furby::handlers::smoke::manual_hello; |
| 8 | use furby::handlers::{cart_items, product, rating, users}; | 8 | use furby::handlers::{cart_items, product, rating, transaction, users}; |
| 9 | use rand::Rng; | 9 | use rand::Rng; |
| 10 | 10 | ||
| 11 | #[actix_web::main] | 11 | #[actix_web::main] |
| @@ -48,6 +48,7 @@ async fn main() -> std::io::Result<()> { | |||
| 48 | web::scope("/user") | 48 | web::scope("/user") |
| 49 | .route("/existing", web::post().to(users::name_exists)) | 49 | .route("/existing", web::post().to(users::name_exists)) |
| 50 | .route("/login", web::post().to(users::login)) | 50 | .route("/login", web::post().to(users::login)) |
| 51 | .route("/logout", web::post().to(users::logout)) | ||
| 51 | .route("/{uname}", web::get().to(users::user_details)) | 52 | .route("/{uname}", web::get().to(users::user_details)) |
| 52 | .route("/new", web::post().to(users::new_user)) | 53 | .route("/new", web::post().to(users::new_user)) |
| 53 | .route( | 54 | .route( |
| @@ -75,6 +76,10 @@ async fn main() -> std::io::Result<()> { | |||
| 75 | "/items", | 76 | "/items", |
| 76 | web::get().to(cart_items::get_user_cart_items), | 77 | web::get().to(cart_items::get_user_cart_items), |
| 77 | ) | 78 | ) |
| 79 | .route( | ||
| 80 | "/total", | ||
| 81 | web::get().to(cart_items::get_user_cart_total), | ||
| 82 | ) | ||
| 78 | .route("/add", web::post().to(cart_items::add_to_cart)) | 83 | .route("/add", web::post().to(cart_items::add_to_cart)) |
| 79 | .route( | 84 | .route( |
| 80 | "/remove", | 85 | "/remove", |
| @@ -86,6 +91,17 @@ async fn main() -> std::io::Result<()> { | |||
| 86 | .route("/add", web::post().to(rating::add_rating)) | 91 | .route("/add", web::post().to(rating::add_rating)) |
| 87 | .route("/remove", web::post().to(rating::remove_rating)), | 92 | .route("/remove", web::post().to(rating::remove_rating)), |
| 88 | ) | 93 | ) |
| 94 | .service( | ||
| 95 | web::scope("/transaction") | ||
| 96 | .route( | ||
| 97 | "/checkout", | ||
| 98 | web::post().to(transaction::checkout_cart), | ||
| 99 | ) | ||
| 100 | .route( | ||
| 101 | "/list", | ||
| 102 | web::get().to(transaction::list_transactions), | ||
| 103 | ), | ||
| 104 | ) | ||
| 89 | .route("/hey", web::get().to(manual_hello)) | 105 | .route("/hey", web::get().to(manual_hello)) |
| 90 | }) | 106 | }) |
| 91 | .bind("127.0.0.1:7878")? | 107 | .bind("127.0.0.1:7878")? |
diff --git a/backend/src/handlers/cart_items.rs b/backend/src/handlers/cart_items.rs index 25baaeb..e17f4c4 100644 --- a/backend/src/handlers/cart_items.rs +++ b/backend/src/handlers/cart_items.rs | |||
| @@ -7,7 +7,7 @@ use actix_identity::Identity; | |||
| 7 | use actix_web::{web, HttpResponse, Responder}; | 7 | use actix_web::{web, HttpResponse, Responder}; |
| 8 | use diesel::prelude::*; | 8 | use diesel::prelude::*; |
| 9 | use log::{error, info}; | 9 | use log::{error, info}; |
| 10 | use serde::Deserialize; | 10 | use serde::Serialize; |
| 11 | 11 | ||
| 12 | pub async fn add_to_cart( | 12 | pub async fn add_to_cart( |
| 13 | cookie: Identity, | 13 | cookie: Identity, |
| @@ -27,16 +27,41 @@ pub async fn add_to_cart( | |||
| 27 | let new_cart_item = AddCartItem { | 27 | let new_cart_item = AddCartItem { |
| 28 | cart_id: selected_user.id, | 28 | cart_id: selected_user.id, |
| 29 | product_id: item_details, | 29 | product_id: item_details, |
| 30 | quantity: Some(1), | ||
| 30 | }; | 31 | }; |
| 31 | info!( | 32 | info!( |
| 32 | "cart id: {:?}, product id {:?}", | 33 | "cart id: {:?}, product id {:?}", |
| 33 | selected_user.id, item_details | 34 | selected_user.id, item_details |
| 34 | ); | 35 | ); |
| 35 | diesel::insert_into(cart_items) | 36 | let current_entry = cart_items |
| 36 | .values((cart_id.eq(selected_user.id), product_id.eq(item_details))) | 37 | .filter(cart_id.eq(selected_user.id)) |
| 37 | .execute(&conn) | 38 | .filter(product_id.eq(item_details)) |
| 38 | .expect("Coundn't connect to DB"); | 39 | .limit(1) |
| 39 | HttpResponse::Ok().body("Inserted successfully!") | 40 | .first::<CartItem>(&conn); |
| 41 | match current_entry { | ||
| 42 | Ok(v) => { | ||
| 43 | info!("Item already present in cart, increasing quantity."); | ||
| 44 | let old_quantity = v.quantity.unwrap_or(1); | ||
| 45 | diesel::update( | ||
| 46 | cart_items | ||
| 47 | .filter(cart_id.eq(selected_user.id)) | ||
| 48 | .filter(product_id.eq(item_details)), | ||
| 49 | ) | ||
| 50 | .set(quantity.eq(old_quantity + 1)) | ||
| 51 | .execute(&conn) | ||
| 52 | .unwrap(); | ||
| 53 | return HttpResponse::Ok() | ||
| 54 | .body("Updated quantity successfully!"); | ||
| 55 | } | ||
| 56 | Err(_) => { | ||
| 57 | info!("Item not present, adding to cart."); | ||
| 58 | diesel::insert_into(cart_items) | ||
| 59 | .values(new_cart_item) | ||
| 60 | .execute(&conn) | ||
| 61 | .expect("Couldn't connect to DB"); | ||
| 62 | HttpResponse::Ok().body("Inserted successfully!") | ||
| 63 | } | ||
| 64 | } | ||
| 40 | } else { | 65 | } else { |
| 41 | error!("Unauthorized add to cart action!"); | 66 | error!("Unauthorized add to cart action!"); |
| 42 | return HttpResponse::Unauthorized() | 67 | return HttpResponse::Unauthorized() |
| @@ -58,15 +83,44 @@ pub async fn remove_from_cart( | |||
| 58 | .limit(1) | 83 | .limit(1) |
| 59 | .first::<Customer>(&conn) | 84 | .first::<Customer>(&conn) |
| 60 | .expect("Couldn't connect to DB"); | 85 | .expect("Couldn't connect to DB"); |
| 61 | 86 | let current_entry = cart_items | |
| 62 | diesel::delete( | 87 | .filter(cart_id.eq(selected_user.id)) |
| 63 | cart_items | 88 | .filter(product_id.eq(item_details)) |
| 64 | .filter(cart_id.eq(selected_user.id)) | 89 | .limit(1) |
| 65 | .filter(product_id.eq(item_details)), | 90 | .first::<CartItem>(&conn); |
| 66 | ) | 91 | match current_entry { |
| 67 | .execute(&conn) | 92 | Ok(v) => { |
| 68 | .expect("Coundn't connect to DB"); | 93 | info!("Item already present in cart, increasing quantity."); |
| 69 | HttpResponse::Ok().body("Removed successfully!") | 94 | let old_quantity = v.quantity.unwrap_or(1); |
| 95 | if old_quantity == 1 { | ||
| 96 | diesel::delete( | ||
| 97 | cart_items | ||
| 98 | .filter(cart_id.eq(selected_user.id)) | ||
| 99 | .filter(product_id.eq(item_details)), | ||
| 100 | ) | ||
| 101 | .execute(&conn) | ||
| 102 | .expect("Coundn't connect to DB"); | ||
| 103 | } else { | ||
| 104 | diesel::update( | ||
| 105 | cart_items | ||
| 106 | .filter(cart_id.eq(selected_user.id)) | ||
| 107 | .filter(product_id.eq(item_details)), | ||
| 108 | ) | ||
| 109 | .set(quantity.eq(old_quantity - 1)) | ||
| 110 | .execute(&conn) | ||
| 111 | .unwrap(); | ||
| 112 | return HttpResponse::Ok() | ||
| 113 | .body("Updated quantity successfully!"); | ||
| 114 | } | ||
| 115 | return HttpResponse::Ok() | ||
| 116 | .body("Updated quantity successfully!"); | ||
| 117 | } | ||
| 118 | Err(_) => { | ||
| 119 | info!("Item not present."); | ||
| 120 | return HttpResponse::InternalServerError() | ||
| 121 | .body("Item not found!"); | ||
| 122 | } | ||
| 123 | } | ||
| 70 | } else { | 124 | } else { |
| 71 | error!("Unauthorized add to cart action!"); | 125 | error!("Unauthorized add to cart action!"); |
| 72 | return HttpResponse::Unauthorized() | 126 | return HttpResponse::Unauthorized() |
| @@ -74,6 +128,12 @@ pub async fn remove_from_cart( | |||
| 74 | } | 128 | } |
| 75 | } | 129 | } |
| 76 | 130 | ||
| 131 | #[derive(Serialize)] | ||
| 132 | struct UserCartItem { | ||
| 133 | product_item: Product, | ||
| 134 | quantity: i32, | ||
| 135 | } | ||
| 136 | |||
| 77 | pub async fn get_user_cart_items( | 137 | pub async fn get_user_cart_items( |
| 78 | cookie: Identity, | 138 | cookie: Identity, |
| 79 | pool: web::Data<TPool>, | 139 | pool: web::Data<TPool>, |
| @@ -92,11 +152,15 @@ pub async fn get_user_cart_items( | |||
| 92 | let cart_products = user_cart_items | 152 | let cart_products = user_cart_items |
| 93 | .into_iter() | 153 | .into_iter() |
| 94 | .map(|item| { | 154 | .map(|item| { |
| 95 | prod::product | 155 | let p = prod::product |
| 96 | .filter(prod::id.eq(item.product_id)) | 156 | .filter(prod::id.eq(item.product_id)) |
| 97 | .limit(1) | 157 | .limit(1) |
| 98 | .first::<Product>(&conn) | 158 | .first::<Product>(&conn) |
| 99 | .expect("Couldn't connect to db") | 159 | .expect("Couldn't connect to db"); |
| 160 | UserCartItem { | ||
| 161 | product_item: p, | ||
| 162 | quantity: item.quantity.unwrap_or(1), | ||
| 163 | } | ||
| 100 | }) | 164 | }) |
| 101 | .collect::<Vec<_>>(); | 165 | .collect::<Vec<_>>(); |
| 102 | return HttpResponse::Ok().json(&cart_products); | 166 | return HttpResponse::Ok().json(&cart_products); |
| @@ -105,3 +169,36 @@ pub async fn get_user_cart_items( | |||
| 105 | .body("Need to be logged in to add to cart!"); | 169 | .body("Need to be logged in to add to cart!"); |
| 106 | } | 170 | } |
| 107 | } | 171 | } |
| 172 | |||
| 173 | pub async fn get_user_cart_total( | ||
| 174 | cookie: Identity, | ||
| 175 | pool: web::Data<TPool>, | ||
| 176 | ) -> impl Responder { | ||
| 177 | let conn = pool.get().unwrap(); | ||
| 178 | if let Some(uname) = cookie.identity() { | ||
| 179 | let selected_user = customer | ||
| 180 | .filter(username.eq(&uname)) | ||
| 181 | .limit(1) | ||
| 182 | .first::<Customer>(&conn) | ||
| 183 | .expect("Couldn't connect to DB"); | ||
| 184 | let user_cart_items = cart_items | ||
| 185 | .filter(cart_id.eq(selected_user.id)) | ||
| 186 | .load::<CartItem>(&conn) | ||
| 187 | .expect("Couldn't connect to DB"); | ||
| 188 | let cart_total: f32 = user_cart_items | ||
| 189 | .into_iter() | ||
| 190 | .map(|item| { | ||
| 191 | let p = prod::product | ||
| 192 | .filter(prod::id.eq(item.product_id)) | ||
| 193 | .limit(1) | ||
| 194 | .first::<Product>(&conn) | ||
| 195 | .expect("Couldn't connect to db"); | ||
| 196 | return p.price * item.quantity.unwrap_or(1) as f32; | ||
| 197 | }) | ||
| 198 | .sum(); | ||
| 199 | return HttpResponse::Ok().json(&cart_total); | ||
| 200 | } else { | ||
| 201 | return HttpResponse::Unauthorized() | ||
| 202 | .body("Need to be logged in to add to cart!"); | ||
| 203 | } | ||
| 204 | } | ||
diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index 9416857..e4ecb3a 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs | |||
| @@ -2,4 +2,5 @@ pub mod cart_items; | |||
| 2 | pub mod product; | 2 | pub mod product; |
| 3 | pub mod rating; | 3 | pub mod rating; |
| 4 | pub mod smoke; | 4 | pub mod smoke; |
| 5 | pub mod transaction; | ||
| 5 | pub mod users; | 6 | pub mod users; |
diff --git a/backend/src/handlers/transaction.rs b/backend/src/handlers/transaction.rs new file mode 100644 index 0000000..1e87312 --- /dev/null +++ b/backend/src/handlers/transaction.rs | |||
| @@ -0,0 +1,74 @@ | |||
| 1 | use crate::models::{AddTransaction, CartItem, Customer, Product, Transaction}; | ||
| 2 | use crate::schema::cart_items::dsl::*; | ||
| 3 | use crate::schema::customer::dsl::*; | ||
| 4 | use crate::schema::product::dsl as prod; | ||
| 5 | use crate::schema::transaction::dsl::*; | ||
| 6 | use crate::TPool; | ||
| 7 | |||
| 8 | use actix_identity::Identity; | ||
| 9 | use actix_web::{web, HttpResponse, Responder}; | ||
| 10 | use diesel::prelude::*; | ||
| 11 | use log::{error, info}; | ||
| 12 | |||
| 13 | pub async fn checkout_cart( | ||
| 14 | pool: web::Data<TPool>, | ||
| 15 | pmt_kind: String, | ||
| 16 | cookie: Identity, | ||
| 17 | ) -> impl Responder { | ||
| 18 | let conn = pool.get().unwrap(); | ||
| 19 | info!("Checkout cart for user: {:?}", cookie.identity()); | ||
| 20 | if let Some(uname) = cookie.identity() { | ||
| 21 | let selected_user = customer | ||
| 22 | .filter(username.eq(&uname)) | ||
| 23 | .limit(1) | ||
| 24 | .first::<Customer>(&conn) | ||
| 25 | .expect("Couldn't connect to DB"); | ||
| 26 | let user_cart_items = cart_items | ||
| 27 | .filter(cart_id.eq(selected_user.id)) | ||
| 28 | .load::<CartItem>(&conn) | ||
| 29 | .expect("Couldn't connect to DB"); | ||
| 30 | let cart_total = user_cart_items.into_iter().fold(0., |acc, item| { | ||
| 31 | let item_price = prod::product | ||
| 32 | .filter(prod::id.eq(item.product_id)) | ||
| 33 | .limit(1) | ||
| 34 | .first::<Product>(&conn) | ||
| 35 | .unwrap() | ||
| 36 | .price; | ||
| 37 | acc + item.quantity.unwrap_or(1) as f32 * item_price | ||
| 38 | }); | ||
| 39 | let transaction_entry = AddTransaction { | ||
| 40 | customer_id: Some(selected_user.id), | ||
| 41 | amount: cart_total, | ||
| 42 | payment_type: pmt_kind, | ||
| 43 | }; | ||
| 44 | diesel::insert_into(transaction) | ||
| 45 | .values(transaction_entry) | ||
| 46 | .execute(&conn) | ||
| 47 | .expect("Coundn't connect to DB"); | ||
| 48 | return HttpResponse::Ok().body("Transaction performed successfully"); | ||
| 49 | } else { | ||
| 50 | return HttpResponse::Unauthorized().body("Login first"); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | pub async fn list_transactions( | ||
| 55 | pool: web::Data<TPool>, | ||
| 56 | cookie: Identity, | ||
| 57 | ) -> impl Responder { | ||
| 58 | let conn = pool.get().unwrap(); | ||
| 59 | if let Some(uname) = cookie.identity() { | ||
| 60 | let selected_user = customer | ||
| 61 | .filter(username.eq(&uname)) | ||
| 62 | .limit(1) | ||
| 63 | .first::<Customer>(&conn) | ||
| 64 | .expect("Couldn't connect to DB"); | ||
| 65 | let user_transactions = transaction | ||
| 66 | .filter(customer_id.eq(selected_user.id)) | ||
| 67 | .load::<Transaction>(&conn) | ||
| 68 | .expect("Couldn't connect to DB"); | ||
| 69 | return HttpResponse::Ok().json(&user_transactions); | ||
| 70 | } else { | ||
| 71 | return HttpResponse::Unauthorized() | ||
| 72 | .body("Need to be logged in to add to cart!"); | ||
| 73 | } | ||
| 74 | } | ||
diff --git a/backend/src/handlers/users.rs b/backend/src/handlers/users.rs index 24fb591..a043c1f 100644 --- a/backend/src/handlers/users.rs +++ b/backend/src/handlers/users.rs | |||
| @@ -58,7 +58,7 @@ pub async fn login( | |||
| 58 | login_details: web::Json<Login>, | 58 | login_details: web::Json<Login>, |
| 59 | ) -> impl Responder { | 59 | ) -> impl Responder { |
| 60 | info!("Login hit"); | 60 | info!("Login hit"); |
| 61 | if let Some(uname) = cookie.identity() { | 61 | if cookie.identity().is_some() { |
| 62 | info!("Found existing cookie: {:?}", cookie.identity()); | 62 | info!("Found existing cookie: {:?}", cookie.identity()); |
| 63 | return HttpResponse::Ok().finish(); | 63 | return HttpResponse::Ok().finish(); |
| 64 | } | 64 | } |
| @@ -84,7 +84,7 @@ pub async fn login( | |||
| 84 | 84 | ||
| 85 | pub async fn logout(cookie: Identity) -> impl Responder { | 85 | pub async fn logout(cookie: Identity) -> impl Responder { |
| 86 | cookie.forget(); | 86 | cookie.forget(); |
| 87 | HttpResponse::Found().header("location", "/").finish() | 87 | HttpResponse::Ok().body("Successful logout.") |
| 88 | } | 88 | } |
| 89 | 89 | ||
| 90 | pub async fn user_details( | 90 | pub async fn user_details( |
diff --git a/backend/src/models.rs b/backend/src/models.rs index a104209..bf531ad 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs | |||
| @@ -35,6 +35,8 @@ pub struct Product { | |||
| 35 | pub kind: Option<String>, | 35 | pub kind: Option<String>, |
| 36 | pub price: f32, | 36 | pub price: f32, |
| 37 | pub description: Option<String>, | 37 | pub description: Option<String>, |
| 38 | pub src: Option<String>, | ||
| 39 | pub ios_src: Option<String>, | ||
| 38 | } | 40 | } |
| 39 | 41 | ||
| 40 | #[derive(Insertable, Deserialize)] | 42 | #[derive(Insertable, Deserialize)] |
| @@ -48,6 +50,12 @@ pub struct NewProduct { | |||
| 48 | 50 | ||
| 49 | #[serde(skip_serializing_if = "Option::is_none")] | 51 | #[serde(skip_serializing_if = "Option::is_none")] |
| 50 | pub description: Option<String>, | 52 | pub description: Option<String>, |
| 53 | |||
| 54 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 55 | pub src: Option<String>, | ||
| 56 | |||
| 57 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 58 | pub ios_src: Option<String>, | ||
| 51 | } | 59 | } |
| 52 | 60 | ||
| 53 | #[derive(Deserialize)] | 61 | #[derive(Deserialize)] |
| @@ -63,6 +71,7 @@ pub struct UpdateProduct { | |||
| 63 | pub struct CartItem { | 71 | pub struct CartItem { |
| 64 | pub cart_id: i32, | 72 | pub cart_id: i32, |
| 65 | pub product_id: i32, | 73 | pub product_id: i32, |
| 74 | pub quantity: Option<i32>, | ||
| 66 | } | 75 | } |
| 67 | 76 | ||
| 68 | #[derive(Insertable, Deserialize)] | 77 | #[derive(Insertable, Deserialize)] |
| @@ -70,6 +79,7 @@ pub struct CartItem { | |||
| 70 | pub struct AddCartItem { | 79 | pub struct AddCartItem { |
| 71 | pub cart_id: i32, | 80 | pub cart_id: i32, |
| 72 | pub product_id: i32, | 81 | pub product_id: i32, |
| 82 | pub quantity: Option<i32>, | ||
| 73 | } | 83 | } |
| 74 | 84 | ||
| 75 | /* Rating */ | 85 | /* Rating */ |
| @@ -95,3 +105,21 @@ pub struct AddRating { | |||
| 95 | pub product_id: i32, | 105 | pub product_id: i32, |
| 96 | pub customer_id: i32, | 106 | pub customer_id: i32, |
| 97 | } | 107 | } |
| 108 | |||
| 109 | /* Transaction */ | ||
| 110 | #[derive(Queryable, Serialize)] | ||
| 111 | pub struct Transaction { | ||
| 112 | pub id: i32, | ||
| 113 | pub payment_type: String, | ||
| 114 | pub amount: f32, | ||
| 115 | pub customer_id: Option<i32>, | ||
| 116 | pub order_date: NaiveDate, | ||
| 117 | } | ||
| 118 | |||
| 119 | #[derive(Insertable, Deserialize)] | ||
| 120 | #[table_name = "transaction"] | ||
| 121 | pub struct AddTransaction { | ||
| 122 | pub payment_type: String, | ||
| 123 | pub amount: f32, | ||
| 124 | pub customer_id: Option<i32>, | ||
| 125 | } | ||
diff --git a/backend/src/schema.rs b/backend/src/schema.rs index f08221a..1419bc0 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs | |||
| @@ -2,6 +2,7 @@ table! { | |||
| 2 | cart_items (cart_id, product_id) { | 2 | cart_items (cart_id, product_id) { |
| 3 | cart_id -> Integer, | 3 | cart_id -> Integer, |
| 4 | product_id -> Integer, | 4 | product_id -> Integer, |
| 5 | quantity -> Nullable<Integer>, | ||
| 5 | } | 6 | } |
| 6 | } | 7 | } |
| 7 | 8 | ||
| @@ -43,6 +44,7 @@ table! { | |||
| 43 | payment_type -> Varchar, | 44 | payment_type -> Varchar, |
| 44 | amount -> Float, | 45 | amount -> Float, |
| 45 | customer_id -> Nullable<Integer>, | 46 | customer_id -> Nullable<Integer>, |
| 47 | order_date -> Date, | ||
| 46 | } | 48 | } |
| 47 | } | 49 | } |
| 48 | 50 | ||
diff --git a/frontend/src/Cart.elm b/frontend/src/Cart.elm index a1750f6..58fb72e 100644 --- a/frontend/src/Cart.elm +++ b/frontend/src/Cart.elm | |||
| @@ -21,9 +21,15 @@ type alias Product = | |||
| 21 | } | 21 | } |
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | type alias CartListing = | ||
| 25 | { productItem : Product | ||
| 26 | , quantity : Int | ||
| 27 | } | ||
| 28 | |||
| 29 | |||
| 24 | type alias Model = | 30 | type alias Model = |
| 25 | { pageStatus : Status | 31 | { pageStatus : Status |
| 26 | , products : List Product | 32 | , products : List CartListing |
| 27 | } | 33 | } |
| 28 | 34 | ||
| 29 | 35 | ||
| @@ -34,10 +40,12 @@ type Status | |||
| 34 | 40 | ||
| 35 | 41 | ||
| 36 | type Msg | 42 | type Msg |
| 37 | = CartLoaded (Result Http.Error (List Product)) | 43 | = CartLoaded (Result Http.Error (List CartListing)) |
| 38 | | FetchCartItems | 44 | | FetchCartItems |
| 39 | | RemoveFromCart Int | 45 | | RemoveFromCart Int |
| 40 | | CartItemRemoved (Result Http.Error ()) | 46 | | CartItemRemoved (Result Http.Error ()) |
| 47 | | AddToCartSuccess (Result Http.Error ()) | ||
| 48 | | AddToCartPressed Int | ||
| 41 | 49 | ||
| 42 | 50 | ||
| 43 | init : Model | 51 | init : Model |
| @@ -69,6 +77,12 @@ update msg model = | |||
| 69 | FetchCartItems -> | 77 | FetchCartItems -> |
| 70 | ( { model | pageStatus = Loading }, fetchCartItems ) | 78 | ( { model | pageStatus = Loading }, fetchCartItems ) |
| 71 | 79 | ||
| 80 | AddToCartPressed id -> | ||
| 81 | ( model, addToCart id ) | ||
| 82 | |||
| 83 | AddToCartSuccess _ -> | ||
| 84 | ( { model | pageStatus = Loading }, fetchCartItems ) | ||
| 85 | |||
| 72 | 86 | ||
| 73 | decodeProduct : D.Decoder Product | 87 | decodeProduct : D.Decoder Product |
| 74 | decodeProduct = | 88 | decodeProduct = |
| @@ -80,9 +94,13 @@ decodeProduct = | |||
| 80 | (D.field "description" (D.nullable D.string)) | 94 | (D.field "description" (D.nullable D.string)) |
| 81 | 95 | ||
| 82 | 96 | ||
| 83 | decodeResponse : D.Decoder (List Product) | 97 | decodeResponse : D.Decoder (List CartListing) |
| 84 | decodeResponse = | 98 | decodeResponse = |
| 85 | D.list decodeProduct | 99 | D.list |
| 100 | (D.map2 CartListing | ||
| 101 | (D.field "product_item" decodeProduct) | ||
| 102 | (D.field "quantity" D.int) | ||
| 103 | ) | ||
| 86 | 104 | ||
| 87 | 105 | ||
| 88 | removeProduct : Int -> Cmd Msg | 106 | removeProduct : Int -> Cmd Msg |
| @@ -132,15 +150,45 @@ viewStatus s = | |||
| 132 | "Not loaded ..." | 150 | "Not loaded ..." |
| 133 | 151 | ||
| 134 | 152 | ||
| 135 | viewProduct : Product -> Html Msg | 153 | addToCart : Int -> Cmd Msg |
| 136 | viewProduct p = | 154 | addToCart id = |
| 155 | let | ||
| 156 | _ = | ||
| 157 | Debug.log "err" <| "adding to cart: " ++ String.fromInt id | ||
| 158 | in | ||
| 159 | Http.riskyRequest | ||
| 160 | { method = "POST" | ||
| 161 | , headers = [] | ||
| 162 | , url = "http://127.0.0.1:7878/cart/add" | ||
| 163 | , body = Http.stringBody "applcation/json" <| String.fromInt <| id | ||
| 164 | , expect = Http.expectWhatever AddToCartSuccess | ||
| 165 | , timeout = Nothing | ||
| 166 | , tracker = Nothing | ||
| 167 | } | ||
| 168 | |||
| 169 | |||
| 170 | calculateTotal : Model -> Float | ||
| 171 | calculateTotal model = | ||
| 172 | let | ||
| 173 | items = | ||
| 174 | model.products | ||
| 175 | in | ||
| 176 | items | ||
| 177 | |> List.map (\i -> toFloat i.quantity * i.productItem.price) | ||
| 178 | |> List.foldl (+) 0 | ||
| 179 | |||
| 180 | |||
| 181 | viewCartItemListing : CartListing -> Html Msg | ||
| 182 | viewCartItemListing listing = | ||
| 137 | div [] | 183 | div [] |
| 138 | [ text p.name | 184 | [ text listing.productItem.name |
| 139 | , div [] [ text <| Maybe.withDefault "" p.kind ] | 185 | , div [] [ text <| Maybe.withDefault "" listing.productItem.kind ] |
| 140 | , div [] [ text <| Maybe.withDefault "" p.description ] | 186 | , div [] [ text <| Maybe.withDefault "" listing.productItem.description ] |
| 141 | , div [] [ text <| String.fromFloat p.price ] | 187 | , div [] [ text <| String.fromFloat listing.productItem.price ] |
| 142 | , div [] [ button [ onClick (RemoveFromCart p.id) ] [ text "Remove" ] ] | 188 | , div [] [ text <| String.fromInt listing.quantity ] |
| 143 | , div [] [ a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ] ] | 189 | , div [] [ button [ onClick (AddToCartPressed listing.productItem.id) ] [ text "Add" ] ] |
| 190 | , div [] [ button [ onClick (RemoveFromCart listing.productItem.id) ] [ text "Remove" ] ] | ||
| 191 | , div [] [ a [ href ("/product/" ++ String.fromInt listing.productItem.id) ] [ text "View Product" ] ] | ||
| 144 | ] | 192 | ] |
| 145 | 193 | ||
| 146 | 194 | ||
| @@ -154,11 +202,13 @@ view model = | |||
| 154 | div [] | 202 | div [] |
| 155 | [ let | 203 | [ let |
| 156 | cart = | 204 | cart = |
| 157 | List.map viewProduct model.products | 205 | List.map viewCartItemListing model.products |
| 158 | in | 206 | in |
| 159 | if List.isEmpty cart then | 207 | if List.isEmpty cart then |
| 160 | text "No items in cart" | 208 | text "No items in cart" |
| 161 | 209 | ||
| 162 | else | 210 | else |
| 163 | ul [] cart | 211 | ul [] cart |
| 212 | , calculateTotal model |> String.fromFloat |> text | ||
| 213 | , a [ href "/checkout" ] [ text "Checkout" ] | ||
| 164 | ] | 214 | ] |
diff --git a/frontend/src/Catalog.elm b/frontend/src/Catalog.elm index 6882c73..d00cb92 100644 --- a/frontend/src/Catalog.elm +++ b/frontend/src/Catalog.elm | |||
| @@ -188,13 +188,13 @@ viewFilters : Model -> Html Msg | |||
| 188 | viewFilters model = | 188 | viewFilters model = |
| 189 | let | 189 | let |
| 190 | priceRange = | 190 | priceRange = |
| 191 | range 0 50000 5000 | 191 | range 0 55000 5000 |
| 192 | 192 | ||
| 193 | ratingRange = | 193 | ratingRange = |
| 194 | List.range 1 5 | 194 | range 1 6 1 |
| 195 | 195 | ||
| 196 | viewRange = | 196 | viewRange default scale = |
| 197 | List.map (\i -> option [] [ text <| String.fromInt i ]) | 197 | List.map (\i -> option [ selected (i == default) ] [ text <| String.fromInt i ]) scale |
| 198 | 198 | ||
| 199 | inp = | 199 | inp = |
| 200 | Maybe.withDefault 0 << String.toFloat | 200 | Maybe.withDefault 0 << String.toFloat |
| @@ -202,15 +202,15 @@ viewFilters model = | |||
| 202 | div [] | 202 | div [] |
| 203 | [ div [] | 203 | [ div [] |
| 204 | [ text "Price" | 204 | [ text "Price" |
| 205 | , select [ onInput (ChangePriceLower << inp) ] (viewRange priceRange) | 205 | , select [ onInput (ChangePriceLower << inp) ] (viewRange 0 priceRange) |
| 206 | , text "to" | 206 | , text "to" |
| 207 | , select [ onInput (ChangePriceUpper << inp) ] (viewRange priceRange) | 207 | , select [ onInput (ChangePriceUpper << inp) ] (viewRange 50000 priceRange) |
| 208 | ] | 208 | ] |
| 209 | , div [] | 209 | , div [] |
| 210 | [ text "Rating" | 210 | [ text "Rating" |
| 211 | , select [ onInput (ChangeRatingLower << inp) ] (viewRange ratingRange) | 211 | , select [ onInput (ChangeRatingLower << inp) ] (viewRange 1 ratingRange) |
| 212 | , text "to" | 212 | , text "to" |
| 213 | , select [ onInput (ChangeRatingUpper << inp) ] (viewRange ratingRange) | 213 | , select [ onInput (ChangeRatingUpper << inp) ] (viewRange 5 ratingRange) |
| 214 | ] | 214 | ] |
| 215 | ] | 215 | ] |
| 216 | 216 | ||
diff --git a/frontend/src/Checkout.elm b/frontend/src/Checkout.elm new file mode 100644 index 0000000..c60da0d --- /dev/null +++ b/frontend/src/Checkout.elm | |||
| @@ -0,0 +1,126 @@ | |||
| 1 | module Checkout exposing (..) | ||
| 2 | |||
| 3 | import Browser | ||
| 4 | import Browser.Navigation as Nav | ||
| 5 | import Html exposing (..) | ||
| 6 | import Html.Attributes exposing (..) | ||
| 7 | import Html.Events exposing (..) | ||
| 8 | import Http | ||
| 9 | import Json.Decode as D | ||
| 10 | import Json.Encode as Encode | ||
| 11 | import Tuple exposing (..) | ||
| 12 | import Url | ||
| 13 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
| 14 | import Utils exposing (..) | ||
| 15 | |||
| 16 | |||
| 17 | type alias Model = | ||
| 18 | { pageStatus : Status | ||
| 19 | , paymentMode : String | ||
| 20 | , cartTotal : Float | ||
| 21 | } | ||
| 22 | |||
| 23 | |||
| 24 | type Status | ||
| 25 | = Loading | ||
| 26 | | Loaded | ||
| 27 | | NotLoaded | ||
| 28 | |||
| 29 | |||
| 30 | type Msg | ||
| 31 | = CheckoutPressed | ||
| 32 | | CheckoutSuccessful (Result Http.Error ()) | ||
| 33 | | AmountLoaded (Result Http.Error Float) | ||
| 34 | | FetchAmount | ||
| 35 | | PaymentModeSelected String | ||
| 36 | |||
| 37 | |||
| 38 | init : Model | ||
| 39 | init = | ||
| 40 | Model NotLoaded "Cash" 0 | ||
| 41 | |||
| 42 | |||
| 43 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
| 44 | update msg model = | ||
| 45 | case msg of | ||
| 46 | CheckoutPressed -> | ||
| 47 | ( model, tryCheckout model.paymentMode ) | ||
| 48 | |||
| 49 | CheckoutSuccessful _ -> | ||
| 50 | ( model, Cmd.none ) | ||
| 51 | |||
| 52 | AmountLoaded res -> | ||
| 53 | case res of | ||
| 54 | Ok v -> | ||
| 55 | ( { model | cartTotal = v }, Cmd.none ) | ||
| 56 | |||
| 57 | Err _ -> | ||
| 58 | ( { model | pageStatus = NotLoaded }, Cmd.none ) | ||
| 59 | |||
| 60 | FetchAmount -> | ||
| 61 | let | ||
| 62 | _ = | ||
| 63 | Debug.log "err" "fetching checkout amount" | ||
| 64 | in | ||
| 65 | ( { model | pageStatus = Loading }, fetchAmount ) | ||
| 66 | |||
| 67 | PaymentModeSelected s -> | ||
| 68 | ( { model | paymentMode = s }, Cmd.none ) | ||
| 69 | |||
| 70 | |||
| 71 | fetchAmount : Cmd Msg | ||
| 72 | fetchAmount = | ||
| 73 | Http.riskyRequest | ||
| 74 | { method = "GET" | ||
| 75 | , headers = [] | ||
| 76 | , url = "http://127.0.0.1:7878/cart/total" | ||
| 77 | , body = Http.emptyBody | ||
| 78 | , expect = Http.expectJson AmountLoaded D.float | ||
| 79 | , timeout = Nothing | ||
| 80 | , tracker = Nothing | ||
| 81 | } | ||
| 82 | |||
| 83 | |||
| 84 | tryCheckout : String -> Cmd Msg | ||
| 85 | tryCheckout pm = | ||
| 86 | Http.riskyRequest | ||
| 87 | { method = "POST" | ||
| 88 | , headers = [] | ||
| 89 | , url = "http://127.0.0.1:7878/transaction/checkout" | ||
| 90 | , body = Http.stringBody "application/json" pm | ||
| 91 | , expect = Http.expectWhatever CheckoutSuccessful | ||
| 92 | , timeout = Nothing | ||
| 93 | , tracker = Nothing | ||
| 94 | } | ||
| 95 | |||
| 96 | |||
| 97 | viewStatus : Status -> String | ||
| 98 | viewStatus s = | ||
| 99 | case s of | ||
| 100 | Loading -> | ||
| 101 | "Loading" | ||
| 102 | |||
| 103 | Loaded -> | ||
| 104 | "Ready!" | ||
| 105 | |||
| 106 | NotLoaded -> | ||
| 107 | "Not loaded ..." | ||
| 108 | |||
| 109 | |||
| 110 | view : Model -> Html Msg | ||
| 111 | view model = | ||
| 112 | case model.pageStatus of | ||
| 113 | Loading -> | ||
| 114 | div [] [ text <| viewStatus Loading ] | ||
| 115 | |||
| 116 | _ -> | ||
| 117 | div [] | ||
| 118 | [ div [] [ text <| String.fromFloat <| model.cartTotal ] | ||
| 119 | , select [] | ||
| 120 | [ option [ onInput PaymentModeSelected ] [ text "Cash" ] | ||
| 121 | , option [ onInput PaymentModeSelected ] [ text "Debit Card" ] | ||
| 122 | , option [ onInput PaymentModeSelected ] [ text "Credit Card" ] | ||
| 123 | ] | ||
| 124 | , div [] [ a [ href "/cart" ] [ text "Cancel" ] ] | ||
| 125 | , div [] [ button [ onClick CheckoutPressed ] [ text "Confirm and Pay" ] ] | ||
| 126 | ] | ||
diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm index bf1583c..f1883a1 100644 --- a/frontend/src/Main.elm +++ b/frontend/src/Main.elm | |||
| @@ -4,6 +4,7 @@ import Browser | |||
| 4 | import Browser.Navigation as Nav | 4 | import Browser.Navigation as Nav |
| 5 | import Cart | 5 | import Cart |
| 6 | import Catalog | 6 | import Catalog |
| 7 | import Checkout | ||
| 7 | import Html exposing (..) | 8 | import Html exposing (..) |
| 8 | import Html.Attributes exposing (..) | 9 | import Html.Attributes exposing (..) |
| 9 | import Html.Events exposing (..) | 10 | import Html.Events exposing (..) |
| @@ -43,6 +44,7 @@ type Route | |||
| 43 | | CatalogPage | 44 | | CatalogPage |
| 44 | | CartPage | 45 | | CartPage |
| 45 | | ProductPage Int | 46 | | ProductPage Int |
| 47 | | CheckoutPage | ||
| 46 | | NotFoundPage | 48 | | NotFoundPage |
| 47 | 49 | ||
| 48 | 50 | ||
| @@ -54,9 +56,8 @@ parseRoute = | |||
| 54 | , P.map CatalogPage (P.s "catalog") | 56 | , P.map CatalogPage (P.s "catalog") |
| 55 | , P.map CartPage (P.s "cart") | 57 | , P.map CartPage (P.s "cart") |
| 56 | , P.map SignupPage (P.s "signup") | 58 | , P.map SignupPage (P.s "signup") |
| 59 | , P.map CheckoutPage (P.s "checkout") | ||
| 57 | , P.map ProductPage (P.s "product" </> P.int) | 60 | , P.map ProductPage (P.s "product" </> P.int) |
| 58 | |||
| 59 | --, P.map ProductPage (P.s "product" </> int) | ||
| 60 | ] | 61 | ] |
| 61 | 62 | ||
| 62 | 63 | ||
| @@ -69,6 +70,7 @@ type alias Model = | |||
| 69 | , productModel : Product.Model | 70 | , productModel : Product.Model |
| 70 | , signupModel : Signup.Model | 71 | , signupModel : Signup.Model |
| 71 | , cartModel : Cart.Model | 72 | , cartModel : Cart.Model |
| 73 | , checkoutModel : Checkout.Model | ||
| 72 | } | 74 | } |
| 73 | 75 | ||
| 74 | 76 | ||
| @@ -92,8 +94,11 @@ init flags url key = | |||
| 92 | 94 | ||
| 93 | cart = | 95 | cart = |
| 94 | Cart.init | 96 | Cart.init |
| 97 | |||
| 98 | checkout = | ||
| 99 | Checkout.init | ||
| 95 | in | 100 | in |
| 96 | ( Model key url start login catalog product signup cart, Cmd.none ) | 101 | ( Model key url start login catalog product signup cart checkout, Cmd.none ) |
| 97 | 102 | ||
| 98 | 103 | ||
| 99 | 104 | ||
| @@ -108,6 +113,7 @@ type Msg | |||
| 108 | | ProductMessage Product.Msg | 113 | | ProductMessage Product.Msg |
| 109 | | SignupMessage Signup.Msg | 114 | | SignupMessage Signup.Msg |
| 110 | | CartMessage Cart.Msg | 115 | | CartMessage Cart.Msg |
| 116 | | CheckoutMessage Checkout.Msg | ||
| 111 | | LogoutPressed | 117 | | LogoutPressed |
| 112 | | LogoutSuccess (Result Http.Error ()) | 118 | | LogoutSuccess (Result Http.Error ()) |
| 113 | 119 | ||
| @@ -127,7 +133,7 @@ update msg model = | |||
| 127 | ( model, tryLogout ) | 133 | ( model, tryLogout ) |
| 128 | 134 | ||
| 129 | LogoutSuccess _ -> | 135 | LogoutSuccess _ -> |
| 130 | ( model, Nav.replaceUrl model.key "/login" ) | 136 | ( { model | loginModel = Login.init }, Nav.replaceUrl model.key "/login" ) |
| 131 | 137 | ||
| 132 | UrlChanged url -> | 138 | UrlChanged url -> |
| 133 | let | 139 | let |
| @@ -155,6 +161,16 @@ update msg model = | |||
| 155 | in | 161 | in |
| 156 | ( { model | location = CartPage }, cmd ) | 162 | ( { model | location = CartPage }, cmd ) |
| 157 | 163 | ||
| 164 | Just CheckoutPage -> | ||
| 165 | let | ||
| 166 | _ = | ||
| 167 | Debug.log "err" "loading checkout page ..." | ||
| 168 | |||
| 169 | cmd = | ||
| 170 | Cmd.map CheckoutMessage Checkout.fetchAmount | ||
| 171 | in | ||
| 172 | ( { model | location = CheckoutPage }, cmd ) | ||
| 173 | |||
| 158 | Just p -> | 174 | Just p -> |
| 159 | ( { model | location = p }, Cmd.none ) | 175 | ( { model | location = p }, Cmd.none ) |
| 160 | 176 | ||
| @@ -205,6 +221,16 @@ update msg model = | |||
| 205 | in | 221 | in |
| 206 | ( { model | cartModel = cmn }, Cmd.map CartMessage cmd ) | 222 | ( { model | cartModel = cmn }, Cmd.map CartMessage cmd ) |
| 207 | 223 | ||
| 224 | CheckoutMessage cm -> | ||
| 225 | let | ||
| 226 | ( cmn, cmd ) = | ||
| 227 | Checkout.update cm model.checkoutModel | ||
| 228 | |||
| 229 | _ = | ||
| 230 | Debug.log "err" "received checkout message ..." | ||
| 231 | in | ||
| 232 | ( { model | checkoutModel = cmn }, Cmd.map CheckoutMessage cmd ) | ||
| 233 | |||
| 208 | ProductMessage pm -> | 234 | ProductMessage pm -> |
| 209 | let | 235 | let |
| 210 | ( pmn, cmd ) = | 236 | ( pmn, cmd ) = |
| @@ -292,6 +318,11 @@ view model = | |||
| 292 | , body = pageWrap model (Html.map CartMessage (Cart.view model.cartModel)) | 318 | , body = pageWrap model (Html.map CartMessage (Cart.view model.cartModel)) |
| 293 | } | 319 | } |
| 294 | 320 | ||
| 321 | CheckoutPage -> | ||
| 322 | { title = "Checkout" | ||
| 323 | , body = pageWrap model (Html.map CheckoutMessage (Checkout.view model.checkoutModel)) | ||
| 324 | } | ||
| 325 | |||
| 295 | ProductPage item -> | 326 | ProductPage item -> |
| 296 | { title = "Product " ++ String.fromInt item | 327 | { title = "Product " ++ String.fromInt item |
| 297 | , body = pageWrap model (Html.map ProductMessage (Product.view model.productModel)) | 328 | , body = pageWrap model (Html.map ProductMessage (Product.view model.productModel)) |
