diff --git a/.env b/.env index e9b296b..3c8bec8 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ DATABASE_URL="postgres://postgres:secret@localhost/ksiegarnia" +SQLX_LOG=info diff --git a/Cargo.lock b/Cargo.lock index 577de87..3e3adac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,10 +600,35 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "cipher" version = "0.4.4" @@ -955,6 +980,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -999,6 +1039,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -1017,8 +1068,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1411,8 +1464,11 @@ dependencies = [ "actix-web", "bcrypt", "bigdecimal", + "chrono", + "chrono-tz", "dotenv", "env_logger", + "futures", "log", "regex", "rust_decimal", @@ -1742,6 +1798,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -1763,6 +1828,44 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2294,6 +2397,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index d1b36d1..1901332 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,7 @@ bcrypt = "0.15" rust_decimal = { version = "1.37.1", features = ["serde", "db-postgres"] } bigdecimal = { version = "0.3.0", features = ["serde"] } regex = "1.10.4" +chrono = { version = "0.4", features = ["serde"] } +chrono-tz = "0.8" +futures = "0.3" + diff --git a/migrations/20250525061434_zamowienia.sql b/migrations/20250525061434_zamowienia.sql new file mode 100644 index 0000000..3588c3e --- /dev/null +++ b/migrations/20250525061434_zamowienia.sql @@ -0,0 +1,25 @@ +CREATE TABLE koszyk ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES uzytkownicy(id), + book_id INTEGER NOT NULL REFERENCES ksiazki(id), + quantity INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(user_id, book_id) +); + +CREATE TABLE zamowienia ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES uzytkownicy(id), + data_zamowienia TIMESTAMP DEFAULT NOW(), + status VARCHAR(50) DEFAULT 'Zakończone', + suma_totalna DECIMAL(10, 2) NOT NULL +); + +CREATE TABLE pozycje_zamowienia ( + id SERIAL PRIMARY KEY, + zamowienie_id INTEGER NOT NULL REFERENCES zamowienia(id), + book_id INTEGER NOT NULL REFERENCES ksiazki(id), + ilosc INTEGER NOT NULL, + cena DECIMAL(10, 2) NOT NULL +); + diff --git a/src/auth.rs b/src/auth.rs index a86863b..34ea8e0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -3,16 +3,16 @@ use bcrypt::{hash, verify, DEFAULT_COST}; use serde::{Deserialize, Serialize}; #[derive(Deserialize)] -struct RegistrationData { - email: String, - haslo: String, - imie: String, +pub struct RegistrationData { + pub email: String, + pub haslo: String, + pub imie: String, #[serde(rename = "confirmPassword")] - confirm_password: String, + pub confirm_password: String, } #[post("/rejestracja")] -async fn rejestracja( +pub async fn rejestracja( form: web::Json, pool: web::Data, ) -> impl Responder { @@ -57,19 +57,19 @@ async fn rejestracja( } #[derive(Deserialize)] -struct LoginData { - email: String, - haslo: String, +pub struct LoginData { + pub email: String, + pub haslo: String, } #[derive(Serialize)] -struct LoginResponse { - token: String, - imie: String, +pub struct LoginResponse { + pub token: String, + pub imie: String, } #[post("/login")] -async fn login( +pub async fn login( form: web::Json, pool: web::Data, ) -> impl Responder { @@ -93,7 +93,7 @@ async fn login( token: dummy_token, imie: user.imie, }) - } + }, _ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"), } } diff --git a/src/main.rs b/src/main.rs index a686221..82d03f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,41 @@ -mod auth; - +use actix_web::{Error, post, get, web, App, HttpResponse, HttpServer, Responder, HttpRequest}; use actix_cors::Cors; use actix_files::Files; -use actix_web::{ - get, http::header, web, App, HttpResponse, HttpServer, Responder, -}; use dotenv::dotenv; use env_logger::{Builder, Env}; use sqlx::postgres::PgPoolOptions; -use rust_decimal::Decimal; -use bigdecimal::BigDecimal; +use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::HashMap; -use std::convert::From; +use bigdecimal::BigDecimal; +use chrono::{TimeZone, DateTime, Utc, NaiveDateTime}; +use sqlx::FromRow; +use actix_web::http::header; +use sqlx::Row; +use bigdecimal::FromPrimitive; + +mod auth; + +#[derive(Deserialize)] +struct RegistrationData { + email: String, + haslo: String, + imie: String, + #[serde(rename = "confirmPassword")] + confirm_password: String, +} + +#[derive(Deserialize)] +struct LoginData { + email: String, + haslo: String, +} + +#[derive(Serialize)] +struct LoginResponse { + token: String, + imie: String, +} #[derive(sqlx::FromRow, serde::Serialize)] struct Book { @@ -24,6 +47,116 @@ struct Book { opis: Option, } +#[derive(Deserialize)] +struct CartItem { + book_id: i32, + quantity: i32, +} + +#[derive(sqlx::FromRow, Serialize)] +struct Order { + id: i32, + data_zamowienia: chrono::NaiveDateTime, // Zmiana na obowiązkowy typ + suma_totalna: BigDecimal, + status: Option, +} + +#[derive(Deserialize)] +struct CheckoutRequest { + items: Vec, + total: f64, +} + +async fn validate_token(token: Option<&str>) -> Result { + let raw_token = token.ok_or(actix_web::error::ErrorUnauthorized("Unauthorized"))?; + + // Usuń prefiks "Bearer " + let token = raw_token.trim_start_matches("Bearer ").trim(); + + if token.starts_with("user-") { + let user_id = token.replace("user-", "").replace("-token", "").parse() + .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?; + Ok(user_id) + } else { + Err(actix_web::error::ErrorUnauthorized("Unauthorized")) + } +} + +#[post("/rejestracja")] +async fn rejestracja( + form: web::Json, + pool: web::Data, +) -> impl Responder { + // Walidacja hasła + if form.haslo.len() < 8 { + return HttpResponse::BadRequest().body("Hasło musi mieć minimum 8 znaków"); + } + + // Sprawdzenie, czy hasła się zgadzają + if form.haslo != form.confirm_password { + return HttpResponse::BadRequest().body("Hasła nie są identyczne"); + } + + // Hashowanie hasła + let hashed_password = match bcrypt::hash(&form.haslo, bcrypt::DEFAULT_COST) { + Ok(h) => h, + Err(_) => return HttpResponse::InternalServerError().finish(), + }; + + // Zapisz do bazy danych + match sqlx::query!( + r#" + INSERT INTO uzytkownicy (email, haslo, imie) + VALUES ($1, $2, $3) + "#, + form.email, + hashed_password, + form.imie + ) + .execute(pool.get_ref()) + .await + { + Ok(_) => HttpResponse::Created().body("Konto utworzone pomyślnie"), + Err(e) => { + if e.to_string().contains("duplicate key value") { + HttpResponse::Conflict().body("Email jest już zarejestrowany") + } else { + HttpResponse::InternalServerError().finish() + } + } + } +} + +#[post("/login")] +async fn login( + form: web::Json, + pool: web::Data, +) -> impl Responder { + let user = match sqlx::query!( + "SELECT id, haslo, imie FROM uzytkownicy WHERE email = $1", + form.email + ) + .fetch_optional(pool.get_ref()) + .await + { + Ok(Some(u)) => u, + Ok(None) => return HttpResponse::Unauthorized().body("Nieprawidłowe dane"), + Err(_) => return HttpResponse::InternalServerError().finish(), + }; + + match bcrypt::verify(&form.haslo, &user.haslo) { + Ok(true) => { + // W praktyce użyj JWT lub innego mechanizmu autentykacji + let dummy_token = format!("user-{}-token", user.id); + HttpResponse::Ok().json(LoginResponse { + token: dummy_token, + imie: user.imie, + }) + }, + _ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"), + } +} + #[get("/api/ksiazki")] async fn get_ksiazki( pool: web::Data, @@ -31,14 +164,14 @@ async fn get_ksiazki( ) -> impl Responder { let search_term = params.get("search").map(|s| s.as_str()).unwrap_or(""); let sort_by = params.get("sort").map(|s| s.as_str()).unwrap_or("default"); - - let base_query = "SELECT + + let base_query = "SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, - COALESCE(opis, 'Brak opisu') as opis + COALESCE(opis, 'Brak opisu') as opis FROM ksiazki"; let sort_clause = match sort_by { @@ -59,7 +192,7 @@ async fn get_ksiazki( }; let mut query_builder = sqlx::query_as::<_, Book>(&query); - + if !search_term.is_empty() { query_builder = query_builder.bind(format!("%{}%", search_term)); } @@ -73,41 +206,24 @@ async fn get_ksiazki( } } -//#[get("/api/ksiazki")] -//async fn get_ksiazki(pool: web::Data) -> impl Responder { -// match sqlx::query_as!( -// Book, -// r#"SELECT id, tytul, autor, cena, obraz_url FROM ksiazki"# -// ) -// .fetch_all(pool.get_ref()) -// .await -// { -// Ok(books) => HttpResponse::Ok().json(books), -// Err(e) => { -// log::error!("Błąd bazy danych: {:?}", e); -// HttpResponse::InternalServerError().body(format!("Błąd: {}", e)) -// } -// } -//} - #[get("/api/ksiazki/{id}")] async fn get_ksiazka( pool: web::Data, path: web::Path, ) -> impl Responder { let id = path.into_inner(); - + match sqlx::query_as!( Book, r#" - SELECT + SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, COALESCE(opis, 'Brak opisu') as opis - FROM ksiazki + FROM ksiazki WHERE id = $1 "#, id @@ -125,11 +241,176 @@ async fn get_ksiazka( } #[get("/api/check-auth")] -async fn check_auth() -> impl Responder { - HttpResponse::Ok().json(json!({ - "authenticated": false, - "user": null - })) +async fn check_auth(req: HttpRequest) -> impl Responder { + let token = req.headers().get("Authorization") + .and_then(|h| h.to_str().ok()); + + match validate_token(token).await { + Ok(user_id) => HttpResponse::Ok().json(json!({ + "authenticated": true, + "user": {"id": user_id} + })), + Err(_) => HttpResponse::Ok().json(json!({ + "authenticated": false, + "user": null + })) + } +} + +#[derive(serde::Serialize)] +struct CartItemResponse { + book_id: i32, + quantity: i32, + tytul: String, + cena: BigDecimal, + #[serde(rename = "obraz_url")] + obraz_url: String, // Zmiana z Option na String +} + +#[get("/api/cart")] +async fn get_cart( + req: HttpRequest, + pool: web::Data, +) -> Result { + let user_id = validate_token(get_token(&req)).await?; + + let cart_items = sqlx::query_as!( + CartItemResponse, + r#"SELECT + k.book_id as "book_id!", + k.quantity as "quantity!", + b.tytul as "tytul!", + b.cena as "cena!", + COALESCE('/images/' || NULLIF(b.obraz_url, ''), '/images/placeholder.jpg') as "obraz_url!" + FROM koszyk k + JOIN ksiazki b ON k.book_id = b.id + WHERE k.user_id = $1"#, + user_id + ) + .fetch_all(pool.get_ref()) + .await + .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; + + Ok(HttpResponse::Ok().json(cart_items)) +} + +fn get_token(req: &HttpRequest) -> Option<&str> { + req.headers() + .get("Authorization") + .and_then(|h| h.to_str().ok()) +} + +#[post("/api/add-to-cart")] +async fn add_to_cart( + cart_item: web::Json, + req: HttpRequest, + pool: web::Data, +) -> Result { + let token = req.headers().get("Authorization") + .and_then(|h| h.to_str().ok()); + + let user_id = validate_token(token).await?; + + sqlx::query!( + "INSERT INTO koszyk (user_id, book_id, quantity) + VALUES ($1, $2, $3) + ON CONFLICT (user_id, book_id) + DO UPDATE SET quantity = koszyk.quantity + $3", + user_id, + cart_item.book_id, + cart_item.quantity + ) + .execute(pool.get_ref()) + .await + .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; + + Ok(HttpResponse::Ok().json(json!({"status": "success"}))) +} + +#[get("/api/order-history")] +async fn get_order_history( + req: HttpRequest, + pool: web::Data, +) -> Result { + let token = req.headers().get("Authorization") + .and_then(|h| h.to_str().ok()); + + let user_id = validate_token(token).await?; + + let orders = sqlx::query_as!( + Order, + r#"SELECT + id, + data_zamowienia as "data_zamowienia!", + suma_totalna, + status + FROM zamowienia WHERE user_id = $1"#, + user_id + ) + .fetch_all(pool.get_ref()) + .await + .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; + + Ok(HttpResponse::Ok().json(orders)) +} + +#[post("/api/checkout")] +async fn checkout( + req: HttpRequest, + pool: web::Data, + data: web::Json, +) -> Result { + let token = req.headers().get("Authorization") + .and_then(|h| h.to_str().ok()); + + let user_id = validate_token(token).await?; + + let mut transaction = pool.begin().await.map_err(|e| { + actix_web::error::ErrorInternalServerError(e) + })?; + + let total_bigdec = BigDecimal::from_f64(data.total) + .ok_or_else(|| actix_web::error::ErrorBadRequest("Invalid total value"))?; + + let order_id = sqlx::query!( + "INSERT INTO zamowienia (user_id, suma_totalna) + VALUES ($1, $2) RETURNING id", + user_id, + total_bigdec + ) + .fetch_one(&mut *transaction) + .await + .map_err(|e| actix_web::error::ErrorInternalServerError(e))? + .id; + + // Dodaj pozycje zamówienia + for item in &data.items { + let book = sqlx::query!( + "SELECT cena FROM ksiazki WHERE id = $1", + item.book_id + ) + .fetch_one(&mut *transaction) + .await + .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; + + sqlx::query!( + "INSERT INTO pozycje_zamowienia (zamowienie_id, book_id, ilosc, cena) + VALUES ($1, $2, $3, $4)", + order_id, + item.book_id, + item.quantity, + book.cena + ) + .execute(&mut *transaction) + .await + .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; + } + + transaction.commit().await.map_err(|e| { + actix_web::error::ErrorInternalServerError(e) + })?; + + Ok(HttpResponse::Ok().json(json!({"status": "success", "order_id": order_id}))) } #[actix_web::main] @@ -158,18 +439,24 @@ async fn main() -> std::io::Result<()> { header::AUTHORIZATION, header::ACCEPT, header::HeaderName::from_static("content-type"), - ]); + ]) + .supports_credentials(); App::new() - .app_data(web::Data::new(pool.clone())) // Dodaj pulę jako globalny stan + .app_data(web::Data::new(pool.clone())) .wrap(cors) .wrap(actix_web::middleware::Logger::default()) .service(get_ksiazki) .service(get_ksiazka) - .service(auth::rejestracja) - .service(auth::login) + .service(rejestracja) + .service(login) + .service(get_cart) + .service(add_to_cart) // Dodaj + .service(checkout) // Dodaj + .service(check_auth) + .service(get_order_history) .service( - Files::new("/images", "./static/images") // Nowy endpoint dla obrazków + Files::new("/images", "./static/images") .show_files_listing(), ) .service(Files::new("/", "./static").index_file("index.html")) @@ -178,3 +465,4 @@ async fn main() -> std::io::Result<()> { .run() .await } + diff --git a/static/cart.html b/static/cart.html new file mode 100644 index 0000000..69070c1 --- /dev/null +++ b/static/cart.html @@ -0,0 +1,21 @@ + + + + + + + Dark Athenaeum + + + + + + + +
+

Twój koszyk

+
+
+ + + diff --git a/static/css/styles.css b/static/css/styles.css index d0e730e..9a66f6a 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -25,6 +25,8 @@ .auth-container a { color: var(--accent-blue) !important; text-decoration: none; + text-align: center; + margin-top: 1rem; } footer { @@ -53,9 +55,11 @@ footer { } .auth-container { - background: rgba(7, 44, 36, 0.9); - border: 2px solid #2a6b5e; - backdrop-filter: blur(5px); + max-width: 500px; + margin: 2rem auto; + padding: 2rem; + border: 1px solid #2a6b5e; + border-radius: 8px; } .auth-container h2 { @@ -66,10 +70,9 @@ footer { } #books-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; - padding: 1rem; + padding: 1rem 2%; } .book-card { @@ -145,9 +148,10 @@ footer a:hover { } .dark-card { - background: #09342b; - border: 1px solid #1c4d42; - transition: transform 0.3s ease; + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + color: #fff; } .dark-card:hover { @@ -212,3 +216,25 @@ footer a { color: #93B8B1; opacity: 0.7; } + +.user-links, .anonymous-links { + gap: 1rem; + align-items: center; +} + +/* Responsywność */ +@media (max-width: 768px) { + #books-container { + grid-template-columns: 1fr; + } + + .navbar-nav { + flex-direction: column; + gap: 0.5rem !important; + } +} + +.neon-title { + color: #0ff; + text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); +} diff --git a/static/index.html b/static/index.html index 7040c86..23e1853 100644 --- a/static/index.html +++ b/static/index.html @@ -50,7 +50,7 @@

NOWOŚCI

-
+
diff --git a/static/js/book.js b/static/js/book.js index 7eee6a4..90f3d60 100644 --- a/static/js/book.js +++ b/static/js/book.js @@ -19,12 +19,46 @@ document.addEventListener('DOMContentLoaded', async () => { throw new Error(`Status: ${response.status}`); } const book = await response.json(); - + document.getElementById('book-title').textContent = book.tytul; document.getElementById('book-author').textContent = `Autor: ${book.autor}`; document.getElementById('book-price').textContent = `Cena: ${book.cena} PLN`; document.getElementById('book-description').textContent = book.opis; document.getElementById('book-cover').src = book.obraz_url; + + // Dodaj obsługę przycisku "Dodaj do koszyka" + document.querySelector('.btn-gothic').addEventListener('click', async () => { + const token = localStorage.getItem('token'); + if (!token) { + alert('Musisz być zalogowany, aby dodać książkę do koszyka'); + window.location.href = '/login.html'; + return; + } + + try { + const response = await fetch('/api/add-to-cart', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + book_id: parseInt(bookId), + quantity: 1 + }) + }); + + if (response.ok) { + alert('Dodano do koszyka!'); + } else { + const error = await response.json(); + alert(error.error || 'Wystąpił błąd'); + } + } catch (error) { + console.error('Błąd:', error); + alert('Wystąpił błąd podczas dodawania do koszyka'); + } + }); } catch (error) { console.error('Błąd:', error); bookDetails.innerHTML = ` @@ -36,3 +70,25 @@ document.addEventListener('DOMContentLoaded', async () => { `; } }); + +document.querySelector('button').addEventListener('click', async () => { + try { + const response = await fetch(`/api/add-to-cart`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ + book_id: new URLSearchParams(window.location.search).get('id'), + quantity: 1 + }) + }); + + if (response.status === 401) { + window.location.href = '/login.html'; + return; + } + + // ... reszta obsługi + } catch (error) { + console.error('Error:', error); + } +}); diff --git a/static/js/cart.js b/static/js/cart.js new file mode 100644 index 0000000..3863aa1 --- /dev/null +++ b/static/js/cart.js @@ -0,0 +1,91 @@ +async function loadCart() { + try { + const response = await fetch('/api/cart', { + headers: getAuthHeaders() + }); + + if (response.status === 401) { + window.location.href = '/login.html'; + return; + } + + const items = await response.json(); + + if (items.length === 0) { + container.innerHTML = ` +
+

Twój koszyk jest pusty

+ Przeglądaj książki +
+ `; + return; + } + + const container = document.getElementById('cart-items'); + container.innerHTML = items.map(item => ` +
+
+ +
+
${item.tytul}
+

Ilość: ${item.quantity}

+

${item.cena.toString()} PLN

+
+
+
+ `).join(''); + } catch (error) { + console.error('Błąd:', error); + showError('Wystąpił błąd podczas ładowania koszyka'); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + try { + const token = localStorage.getItem('token'); + if (!token) { + window.location.href = '/login.html'; + return; + } + + const response = await fetch('/api/cart', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error('Błąd ładowania koszyka'); + + const cartItems = await response.json(); + const container = document.getElementById('cart-items'); + container.innerHTML = ''; + + cartItems.forEach(item => { + const itemHTML = ` +
+
+
+
+ ${item.tytul} +
+
+
+
${item.tytul}
+

Ilość: ${item.quantity}

+

Cena: ${item.cena} PLN

+
+
+
+
+
+ `; + container.insertAdjacentHTML('beforeend', itemHTML); + }); + + } catch (error) { + console.error('Error:', error); + alert('Nie udało się załadować koszyka'); + } +}); diff --git a/static/js/main.js b/static/js/main.js index 77fa015..6ec00b9 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,6 +1,6 @@ document.addEventListener('DOMContentLoaded', () => { updateNavVisibility(); - setInterval(updateNavVisibility, 1000); // Aktualizuj co sekundę + checkAuthStatus(); }); function updateNavVisibility() { @@ -8,7 +8,7 @@ function updateNavVisibility() { document.querySelectorAll('.anonymous-links, .user-links').forEach(el => { el.style.display = 'none'; }); - + if (token) { document.querySelector('.user-links').style.display = 'flex'; } else { @@ -16,50 +16,55 @@ function updateNavVisibility() { } } +async function checkAuthStatus() { + try { + const response = await fetch('/api/check-auth'); + const data = await response.json(); + + if (data.authenticated) { + localStorage.setItem('token', data.token); + localStorage.setItem('imie', data.user.imie); + updateNavVisibility(); + } + } catch (error) { + console.error('Błąd sprawdzania autentykacji:', error); + } +} + document.getElementById('logoutLink')?.addEventListener('click', (e) => { e.preventDefault(); localStorage.removeItem('token'); localStorage.removeItem('imie'); + updateNavVisibility(); window.location.href = '/'; }); -document.addEventListener('DOMContentLoaded', () => { - updateNavVisibility(); - - // Aktualizuj awatar/imię w nagłówku - const userName = localStorage.getItem('imie'); - if(userName) { - const profileLink = document.querySelector('a[href="/profile.html"]'); - if(profileLink) profileLink.innerHTML = `👤 ${userName}`; - } -}); - -async function loadUserData() { - const response = await fetch('/api/check-auth'); - const data = await response.json(); - updateNavVisibility(data.authenticated); -} - document.getElementById('loginForm')?.addEventListener('submit', async (e) => { e.preventDefault(); const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; - const response = await fetch('/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, haslo: password }), - }); + try { + const response = await fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, haslo: password }), + }); - const data = await response.json(); - if (response.ok) { - localStorage.setItem('token', data.token); - localStorage.setItem('imie', data.imie); - window.location.href = '/'; - } else { - alert(data.message || 'Logowanie nieudane'); + const data = await response.json(); + if (response.ok) { + localStorage.setItem('token', data.token); + localStorage.setItem('imie', data.imie); + updateNavVisibility(); + window.location.href = '/'; + } else { + alert(data.message || 'Logowanie nieudane'); + } + } catch (error) { + console.error('Błąd logowania:', error); + alert('Wystąpił błąd podczas logowania'); } }); @@ -75,26 +80,31 @@ document.getElementById('registerForm')?.addEventListener('submit', async (e) => return; } - const response = await fetch('/rejestracja', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ imie: name, email, haslo: password, confirmPassword }), - }); + try { + const response = await fetch('/rejestracja', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ imie: name, email, haslo: password, confirmPassword }), + }); - if (response.ok) { - alert('Rejestracja udana'); - window.location.href = '/login.html'; - } else { - const data = await response.text(); - alert(data || 'Rejestracja nieudana'); + if (response.ok) { + alert('Rejestracja udana'); + window.location.href = '/login.html'; + } else { + const data = await response.text(); + alert(data || 'Rejestracja nieudana'); + } + } catch (error) { + console.error('Błąd rejestracji:', error); + alert('Wystąpił błąd podczas rejestracji'); } }); -async function loadBooks(searchTerm = '') { +async function loadBooks(searchTerm = '', sortBy = 'default') { try { - const response = await fetch(`/api/ksiazki?search=${encodeURIComponent(searchTerm)}`); + const response = await fetch(`/api/ksiazki?search=${encodeURIComponent(searchTerm)}&sort=${sortBy}`); if (!response.ok) throw new Error(`Błąd HTTP: ${response.status}`); const books = await response.json(); renderBooks(books); @@ -104,39 +114,26 @@ async function loadBooks(searchTerm = '') { } } -// Inicjalizacja przy pierwszym załadowaniu -document.addEventListener('DOMContentLoaded', () => { - if (document.getElementById('books-container')) { - loadBooks(); - } - - // Nasłuchiwanie wyszukiwania - document.getElementById('searchInput')?.addEventListener('input', (e) => { - loadBooks(e.target.value); - }); - - updateNavVisibility(); -}); - -document.getElementById('searchForm')?.addEventListener('submit', async (e) => { - e.preventDefault(); - const searchTerm = document.querySelector('#searchForm input').value; - const response = await fetch(`/api/ksiazki?search=${encodeURIComponent(searchTerm)}`); - const books = await response.json(); - renderBooks(books); -}); - function renderBooks(books) { const container = document.getElementById('books-container'); if (!container) return; - + + if (books.length === 0) { + container.innerHTML = ` +
+

Nie znaleziono książek

+
+ `; + return; + } + container.innerHTML = books.map(book => `
- ${book.tytul}
@@ -144,15 +141,60 @@ function renderBooks(books) {
+
+
${book.tytul}
+

${book.autor}

+

${book.cena} PLN

+ +
`).join(''); + + // Dodaj obsługę przycisków "Dodaj do koszyka" + document.querySelectorAll('.add-to-cart').forEach(button => { + button.addEventListener('click', async (e) => { + e.preventDefault(); + const token = localStorage.getItem('token'); + if (!token) { + alert('Musisz być zalogowany, aby dodać książkę do koszyka'); + window.location.href = '/login.html'; + return; + } + + const bookId = e.target.getAttribute('data-book-id'); + + try { + const response = await fetch('/api/add-to-cart', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + book_id: parseInt(bookId), + quantity: 1 + }) + }); + + if (response.ok) { + alert('Dodano do koszyka!'); + } else { + const error = await response.json(); + alert(error.error || 'Wystąpił błąd'); + } + } catch (error) { + console.error('Błąd:', error); + alert('Wystąpił błąd podczas dodawania do koszyka'); + } + }); + }); } function showError(message) { const container = document.getElementById('books-container'); if (!container) return; - + container.innerHTML = `
@@ -165,4 +207,54 @@ function showError(message) { `; } -document.addEventListener('DOMContentLoaded', updateNavVisibility); +// Inicjalizacja przy pierwszym załadowaniu +document.addEventListener('DOMContentLoaded', () => { + if (document.getElementById('books-container')) { + loadBooks(); + } + + // Nasłuchiwanie wyszukiwania + document.getElementById('searchInput')?.addEventListener('input', (e) => { + loadBooks(e.target.value, document.getElementById('sortSelect')?.value || 'default'); + }); + + // Nasłuchiwanie zmiany sortowania + document.getElementById('sortSelect')?.addEventListener('change', (e) => { + loadBooks(document.getElementById('searchInput')?.value || '', e.target.value); + }); +}); + +function getAuthHeaders() { + const token = localStorage.getItem('token'); + return { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + }; +} + +async function updateNavbar() { + try { + const response = await fetch('/api/check-auth', { + headers: getAuthHeaders() + }); + + const authInfo = await response.json(); + const userLinks = document.querySelector('.user-links'); + const anonLinks = document.querySelector('.anonymous-links'); + + if (authInfo.authenticated) { + userLinks.style.display = 'flex'; + anonLinks.style.display = 'none'; + } else { + userLinks.style.display = 'none'; + anonLinks.style.display = 'flex'; + } + } catch (error) { + console.error('Error checking auth:', error); + } +} + +// Wywołaj przy każdym załadowaniu strony +document.addEventListener('DOMContentLoaded', updateNavbar); + +localStorage.setItem('token', response.token); diff --git a/static/js/profile.js b/static/js/profile.js new file mode 100644 index 0000000..5332f88 --- /dev/null +++ b/static/js/profile.js @@ -0,0 +1,31 @@ +async function loadOrderHistory() { + try { + const response = await fetch('/api/order-history', { + headers: getAuthHeaders() + }); + + if (response.status === 401) { + window.location.href = '/login.html'; + return; + } + + const orders = await response.json(); + + const container = document.getElementById('order-history'); + container.innerHTML = orders.map(order => ` +
+
+
Zamówienie #${order.id}
+

Data: ${new Date(order.data_zamowienia).toLocaleDateString()}

+

Status: ${order.status}

+

Suma: ${order.suma_totalna} PLN

+
+
+ `).join(''); + + } catch (error) { + console.error('Error:', error); + } +} + +document.addEventListener('DOMContentLoaded', loadOrderHistory); diff --git a/static/profile.html b/static/profile.html new file mode 100644 index 0000000..bf83cc4 --- /dev/null +++ b/static/profile.html @@ -0,0 +1,60 @@ + + + + + + Dark Athenaeum + + + + + + + + +
+

Twój profil

+ +
+

Historia zamówień

+
+
+
+ + + + diff --git a/static/register.html b/static/register.html index ece1e54..272f313 100644 --- a/static/register.html +++ b/static/register.html @@ -64,6 +64,9 @@
+