refactoring

This commit is contained in:
Lheorvine 2025-05-30 18:00:52 +02:00
parent 5aee4f61bb
commit 1adcd8617a
17 changed files with 743 additions and 850 deletions

View file

@ -1,39 +1,25 @@
use actix_web::{post, web, HttpResponse, Responder}; use actix_web::{post, web, HttpResponse, Responder};
use crate::models::{RegistrationData, LoginData, LoginResponse};
use crate::error::AppError;
use bcrypt::{hash, verify, DEFAULT_COST}; use bcrypt::{hash, verify, DEFAULT_COST};
use serde::{Deserialize, Serialize}; use sqlx::PgPool;
#[derive(Deserialize)]
pub struct RegistrationData {
pub email: String,
pub haslo: String,
pub imie: String,
#[serde(rename = "confirmPassword")]
pub confirm_password: String,
}
#[post("/rejestracja")] #[post("/rejestracja")]
pub async fn rejestracja( pub async fn register(
form: web::Json<RegistrationData>, form: web::Json<RegistrationData>,
pool: web::Data<sqlx::PgPool>, pool: web::Data<PgPool>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
// Walidacja hasła
if form.haslo.len() < 8 { if form.haslo.len() < 8 {
return HttpResponse::BadRequest().body("Hasło musi mieć minimum 8 znaków"); return Err(AppError::BadRequest("Hasło musi mieć minimum 8 znaków".to_string()));
} }
// Sprawdzenie, czy hasła się zgadzają
if form.haslo != form.confirm_password { if form.haslo != form.confirm_password {
return HttpResponse::BadRequest().body("Hasła nie są identyczne"); return Err(AppError::BadRequest("Hasła nie są identyczne".to_string()));
} }
// Hashowanie hasła let hashed_password = hash(&form.haslo, DEFAULT_COST).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
let hashed_password = match hash(&form.haslo, DEFAULT_COST) {
Ok(h) => h,
Err(_) => return HttpResponse::InternalServerError().finish(),
};
// Zapisz do bazy danych sqlx::query!(
match sqlx::query!(
r#" r#"
INSERT INTO uzytkownicy (email, haslo, imie) INSERT INTO uzytkownicy (email, haslo, imie)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
@ -44,57 +30,39 @@ pub async fn rejestracja(
) )
.execute(pool.get_ref()) .execute(pool.get_ref())
.await .await
{ .map_err(|e| {
Ok(_) => HttpResponse::Created().body("Konto utworzone pomyślnie"), if e.to_string().contains("duplicate key value") {
Err(e) => { AppError::BadRequest("Email jest już zarejestrowany".to_string())
if e.to_string().contains("duplicate key value") { } else {
HttpResponse::Conflict().body("Email jest już zarejestrowany") AppError::InternalServerError("Błąd serwera".to_string())
} else {
HttpResponse::InternalServerError().finish()
}
} }
} })?;
}
#[derive(Deserialize)] Ok(HttpResponse::Created().body("Konto utworzone pomyślnie"))
pub struct LoginData {
pub email: String,
pub haslo: String,
}
#[derive(Serialize)]
pub struct LoginResponse {
pub token: String,
pub imie: String,
} }
#[post("/login")] #[post("/login")]
pub async fn login( pub async fn login(
form: web::Json<LoginData>, form: web::Json<LoginData>,
pool: web::Data<sqlx::PgPool>, pool: web::Data<PgPool>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let user = match sqlx::query!( let user = sqlx::query!(
"SELECT id, haslo, imie FROM uzytkownicy WHERE email = $1", "SELECT id, haslo, imie FROM uzytkownicy WHERE email = $1",
form.email form.email
) )
.fetch_optional(pool.get_ref()) .fetch_optional(pool.get_ref())
.await .await
{ .map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?
Ok(Some(u)) => u, .ok_or_else(|| AppError::Unauthorized("Nieprawidłowe dane".to_string()))?;
Ok(None) => return HttpResponse::Unauthorized().body("Nieprawidłowe dane"),
Err(_) => return HttpResponse::InternalServerError().finish(),
};
match verify(&form.haslo, &user.haslo) { if verify(&form.haslo, &user.haslo).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))? {
Ok(true) => { let dummy_token = format!("user-{}-token", user.id);
// W praktyce użyj JWT lub innego mechanizmu autentykacji Ok(HttpResponse::Ok().json(LoginResponse {
let dummy_token = format!("user-{}-token", user.id); token: dummy_token,
HttpResponse::Ok().json(LoginResponse { imie: user.imie,
token: dummy_token, }))
imie: user.imie, } else {
}) Err(AppError::Unauthorized("Nieprawidłowe hasło".to_string()))
},
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
} }
} }

67
src/books.rs Normal file
View file

@ -0,0 +1,67 @@
use actix_web::{get, web, HttpResponse, Responder};
use crate::models::Book;
use sqlx::PgPool;
use std::collections::HashMap;
#[get("/api/ksiazki")]
pub async fn get_books(
pool: web::Data<PgPool>,
query: web::Query<HashMap<String, String>>,
) -> impl Responder {
let search_term = query.get("search").map(|s| s.as_str()).unwrap_or("");
let sort_by = query.get("sort").map(|s| s.as_str()).unwrap_or("default");
let mut base_query = "SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, COALESCE(opis, 'Brak opisu') as opis FROM ksiazki".to_string();
let where_clause = if !search_term.is_empty() {
" WHERE LOWER(tytul) LIKE LOWER($1) OR LOWER(autor) LIKE LOWER($1)"
} else {
""
};
let order_clause = match sort_by {
"price_asc" => " ORDER BY cena ASC",
"price_desc" => " ORDER BY cena DESC",
"title_asc" => " ORDER BY tytul ASC",
"author_asc" => " ORDER BY autor ASC",
_ => " ORDER BY tytul ASC",
};
let query_str = format!("{}{}{}", base_query, where_clause, order_clause);
let mut query_builder = sqlx::query_as::<_, Book>(&query_str);
if !search_term.is_empty() {
query_builder = query_builder.bind(format!("%{}%", search_term));
}
match query_builder.fetch_all(pool.get_ref()).await {
Ok(books) => HttpResponse::Ok().json(books),
Err(_) => HttpResponse::InternalServerError().body("Błąd serwera"),
}
}
#[get("/api/ksiazki/{id}")]
pub async fn get_book(
pool: web::Data<PgPool>,
path: web::Path<i32>,
) -> impl Responder {
let id = path.into_inner();
match sqlx::query_as!(
Book,
r#"
SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, COALESCE(opis, 'Brak opisu') as opis
FROM ksiazki
WHERE id = $1
"#,
id
)
.fetch_optional(pool.get_ref())
.await {
Ok(Some(book)) => HttpResponse::Ok().json(book),
Ok(None) => HttpResponse::NotFound().body("Książka nie znaleziona"),
Err(_) => HttpResponse::InternalServerError().body("Błąd serwera"),
}
}

97
src/cart.rs Normal file
View file

@ -0,0 +1,97 @@
use actix_web::{get, post, delete, web, HttpResponse, HttpRequest, Responder};
use crate::models::{CartItem, CartItemResponse};
use crate::error::AppError;
use sqlx::PgPool;
async fn validate_token(token: Option<&str>) -> Result<i32, AppError> {
let raw_token = token.ok_or(AppError::Unauthorized("Unauthorized".to_string()))?;
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(|_| AppError::Unauthorized("Invalid token".to_string()))?;
Ok(user_id)
} else {
Err(AppError::Unauthorized("Unauthorized".to_string()))
}
}
fn get_token(req: &HttpRequest) -> Option<&str> {
req.headers().get("Authorization").and_then(|h| h.to_str().ok())
}
#[get("/api/cart")]
pub async fn get_cart(
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
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
ORDER BY b.tytul"#,
user_id
)
.fetch_all(pool.get_ref())
.await
.map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
Ok(HttpResponse::Ok().json(cart_items))
}
#[post("/api/add-to-cart")]
pub async fn add_to_cart(
cart_item: web::Json<CartItem>,
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
let token = get_token(&req);
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(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"})))
}
#[delete("/api/remove-from-cart/{book_id}")]
pub async fn remove_from_cart(
req: HttpRequest,
pool: web::Data<PgPool>,
book_id: web::Path<i32>,
) -> Result<HttpResponse, AppError> {
let user_id = validate_token(get_token(&req)).await?;
let book_id = book_id.into_inner();
sqlx::query!(
"DELETE FROM koszyk WHERE user_id = $1 AND book_id = $2",
user_id,
book_id
)
.execute(pool.get_ref())
.await
.map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"})))
}

30
src/error.rs Normal file
View file

@ -0,0 +1,30 @@
use actix_web::{HttpResponse, ResponseError};
use std::fmt;
#[derive(Debug)]
pub enum AppError {
BadRequest(String),
Unauthorized(String),
InternalServerError(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::BadRequest(msg) => write!(f, "BadRequest: {}", msg),
AppError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
AppError::InternalServerError(msg) => write!(f, "InternalServerError: {}", msg),
}
}
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
match self {
AppError::BadRequest(msg) => HttpResponse::BadRequest().body(msg.clone()),
AppError::Unauthorized(msg) => HttpResponse::Unauthorized().body(msg.clone()),
AppError::InternalServerError(msg) => HttpResponse::InternalServerError().body(msg.clone()),
}
}
}

View file

@ -1,564 +1,24 @@
use actix_web::{Error, post, get, web, delete, App, HttpResponse, HttpServer, Responder, HttpRequest}; mod models;
mod auth;
mod books;
mod cart;
mod profile;
mod error;
use actix_web::{web, App, HttpServer};
use actix_cors::Cors; use actix_cors::Cors;
use actix_files::Files; use actix_files::Files;
use dotenv::dotenv; use dotenv::dotenv;
use env_logger::{Builder, Env}; use env_logger::Builder;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use serde::{Deserialize, Serialize}; use std::env;
use serde_json::json;
use std::collections::HashMap;
use bigdecimal::BigDecimal;
use chrono::{TimeZone, DateTime, Utc, NaiveDateTime};
use sqlx::FromRow;
use actix_web::http::header;
use sqlx::Row;
use bigdecimal::FromPrimitive;
use std::convert::Infallible;
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 {
id: i32,
tytul: String,
autor: String,
cena: BigDecimal,
obraz_url: Option<String>,
opis: Option<String>,
}
#[derive(Deserialize)]
struct CartItem {
book_id: i32,
quantity: i32,
}
#[derive(Serialize)]
struct OrderWithItems {
id: i32,
data_zamowienia: NaiveDateTime,
suma_totalna: BigDecimal,
status: Option<String>,
items: Vec<OrderItem>,
}
#[derive(sqlx::FromRow, Serialize)]
struct OrderItem {
tytul: String,
autor: String,
ilosc: i32,
cena: BigDecimal,
}
#[derive(Deserialize)]
struct CheckoutRequest {
items: Vec<CartItem>,
total: f64,
}
async fn validate_token(token: Option<&str>) -> Result<i32, actix_web::Error> {
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<RegistrationData>,
pool: web::Data<sqlx::PgPool>,
) -> 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<LoginData>,
pool: web::Data<sqlx::PgPool>,
) -> 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<sqlx::PgPool>,
web::Query(params): web::Query<HashMap<String, String>>,
) -> 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");
// Poprawione zapytanie bazowe
let mut base_query = "SELECT
id,
tytul,
autor,
cena,
COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url,
COALESCE(opis, 'Brak opisu') as opis
FROM ksiazki".to_string();
// Warunek WHERE
let where_clause = if !search_term.is_empty() {
" WHERE LOWER(tytul) LIKE LOWER($1) OR LOWER(autor) LIKE LOWER($1)"
} else {
""
};
// Poprawna kolejność klauzul
let order_clause = match sort_by {
"price_asc" => " ORDER BY cena ASC",
"price_desc" => " ORDER BY cena DESC",
"title_asc" => " ORDER BY tytul ASC",
"author_asc" => " ORDER BY autor ASC",
_ => " ORDER BY tytul ASC" // Domyślne sortowanie
};
// Łączymy części zapytania w odpowiedniej kolejności
let query = format!("{}{}{}", base_query, where_clause, order_clause);
let mut query_builder = sqlx::query_as::<_, Book>(&query);
if !search_term.is_empty() {
query_builder = query_builder.bind(format!("%{}%", search_term));
}
match query_builder.fetch_all(pool.get_ref()).await {
Ok(books) => HttpResponse::Ok().json(books),
Err(e) => {
log::error!("Błąd bazy danych: {:?}", e);
HttpResponse::InternalServerError().json(json!({"error": "Błąd serwera"}))
}
}
}
#[get("/api/ksiazki/{id}")]
async fn get_ksiazka(
pool: web::Data<sqlx::PgPool>,
path: web::Path<i32>,
) -> impl Responder {
let id = path.into_inner();
match sqlx::query_as!(
Book,
r#"
SELECT
id,
tytul,
autor,
cena,
COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url,
COALESCE(opis, 'Brak opisu') as opis
FROM ksiazki
WHERE id = $1
"#,
id
)
.fetch_optional(pool.get_ref())
.await
{
Ok(Some(book)) => HttpResponse::Ok().json(book),
Ok(None) => HttpResponse::NotFound().json(json!({"error": "Książka nie znaleziona"})),
Err(e) => {
log::error!("Błąd bazy danych: {}", e);
HttpResponse::InternalServerError().json(json!({"error": "Błąd serwera"}))
}
}
}
#[derive(Serialize)]
struct UserInfo {
id: i32,
imie: String,
}
#[get("/api/check-auth")]
async fn check_auth(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>, // Dodajemy pool jako parametr
) -> impl Responder {
let token = req.headers().get("Authorization")
.and_then(|h| h.to_str().ok());
match validate_token(token).await {
Ok(user_id) => {
match sqlx::query!(
"SELECT imie FROM uzytkownicy WHERE id = $1",
user_id
)
.fetch_one(pool.get_ref()) // Używamy pool z parametru
.await {
Ok(u) => HttpResponse::Ok().json(json!({
"authenticated": true,
"user": {
"id": user_id,
"imie": u.imie
}
})),
Err(_) => HttpResponse::Ok().json(json!({
"authenticated": false,
"user": null
}))
}
},
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<String> na String
}
#[get("/api/cart")]
async fn get_cart(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
) -> Result<HttpResponse, Error> {
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
ORDER BY b.tytul"#,
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<CartItem>,
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
) -> Result<HttpResponse, actix_web::Error> {
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"})))
}
#[delete("/api/remove-from-cart/{book_id}")]
async fn remove_from_cart(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
book_id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
let user_id = validate_token(get_token(&req)).await?;
let book_id = book_id.into_inner();
sqlx::query!(
"DELETE FROM koszyk WHERE user_id = $1 AND book_id = $2",
user_id,
book_id
)
.execute(pool.get_ref())
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Dodaj mapowanie błędu
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
}
#[post("/api/decrease-cart-item/{book_id}")]
async fn decrease_cart_item(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
book_id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
let user_id = validate_token(get_token(&req)).await?;
let book_id = book_id.into_inner();
// Sprawdź aktualną ilość
let current_quantity = sqlx::query!(
"SELECT quantity FROM koszyk WHERE user_id = $1 AND book_id = $2",
user_id,
book_id
)
.fetch_optional(pool.get_ref())
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))? // Dodaj mapowanie błędu
.map(|r| r.quantity);
if let Some(qty) = current_quantity {
if qty > 1 {
// Zmniejsz ilość o 1
sqlx::query!(
"UPDATE koszyk SET quantity = quantity - 1 WHERE user_id = $1 AND book_id = $2",
user_id,
book_id
)
.execute(pool.get_ref())
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Dodaj mapowanie błędu
} else {
// Usuń pozycję jeśli ilość = 1
sqlx::query!(
"DELETE FROM koszyk WHERE user_id = $1 AND book_id = $2",
user_id,
book_id
)
.execute(pool.get_ref())
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Dodaj mapowanie błędu
}
}
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
}
#[get("/api/order-history")]
async fn get_order_history(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
) -> Result<HttpResponse, Error> {
let user_id = validate_token(get_token(&req)).await?;
let orders = sqlx::query!(
r#"
SELECT
z.id as "id!",
z.data_zamowienia as "data_zamowienia!",
z.suma_totalna as "suma_totalna!",
z.status,
pz.ilosc as "ilosc!",
pz.cena as "item_price!",
k.tytul as "tytul!",
k.autor as "autor!"
FROM zamowienia z
JOIN pozycje_zamowienia pz ON z.id = pz.zamowienie_id
JOIN ksiazki k ON pz.book_id = k.id
WHERE z.user_id = $1
ORDER BY z.id DESC, k.tytul ASC
"#,
user_id
)
.fetch_all(pool.get_ref())
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let mut grouped_orders = HashMap::new();
for record in orders {
let entry = grouped_orders.entry(record.id).or_insert(OrderWithItems {
id: record.id,
data_zamowienia: record.data_zamowienia,
suma_totalna: record.suma_totalna,
status: record.status,
items: Vec::new(),
});
entry.items.push(OrderItem {
tytul: record.tytul,
autor: record.autor,
ilosc: record.ilosc,
cena: record.item_price,
});
}
let result: Vec<OrderWithItems> = grouped_orders.into_values().collect();
Ok(HttpResponse::Ok().json(result))
}
#[post("/api/checkout")]
async fn checkout(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
data: web::Json<CheckoutRequest>,
) -> Result<HttpResponse, actix_web::Error> {
let user_id = validate_token(get_token(&req)).await?;
let mut transaction = pool.begin().await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
// 1. Utwórz zamówienie
let order_id = sqlx::query!(
"INSERT INTO zamowienia (user_id, suma_totalna)
VALUES ($1, $2) RETURNING id",
user_id,
BigDecimal::from_f64(data.total).ok_or_else(||
actix_web::error::ErrorBadRequest("Invalid total value"))?
)
.fetch_one(&mut *transaction)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?
.id;
// 2. 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))?;
}
// 3. Wyczyść koszyk
sqlx::query!(
"DELETE FROM koszyk WHERE user_id = $1",
user_id
)
.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"})))
}
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
// Inicjalizacja loggera Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
Builder::from_env(Env::default().default_filter_or("debug")).init();
// Ładowanie zmiennych środowiskowych
dotenv().ok(); dotenv().ok();
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set in .env");
// Utwórz pulę połączeń let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set in .env");
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(&database_url) .connect(&database_url)
@ -570,10 +30,9 @@ async fn main() -> std::io::Result<()> {
.allow_any_origin() .allow_any_origin()
.allowed_methods(vec!["GET", "POST", "DELETE"]) .allowed_methods(vec!["GET", "POST", "DELETE"])
.allowed_headers(vec![ .allowed_headers(vec![
header::CONTENT_TYPE, actix_web::http::header::CONTENT_TYPE,
header::AUTHORIZATION, actix_web::http::header::AUTHORIZATION,
header::ACCEPT, actix_web::http::header::ACCEPT,
header::HeaderName::from_static("content-type"),
]) ])
.supports_credentials(); .supports_credentials();
@ -581,17 +40,14 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(pool.clone()))
.wrap(cors) .wrap(cors)
.wrap(actix_web::middleware::Logger::default()) .wrap(actix_web::middleware::Logger::default())
.service(get_ksiazki) .service(books::get_books)
.service(get_ksiazka) .service(books::get_book)
.service(rejestracja) .service(auth::register)
.service(login) .service(auth::login)
.service(get_cart) .service(cart::get_cart)
.service(add_to_cart) // Dodaj .service(cart::add_to_cart)
.service(checkout) // Dodaj .service(cart::remove_from_cart)
.service(check_auth) .service(profile::get_order_history)
.service(remove_from_cart)
.service(decrease_cart_item)
.service(get_order_history)
.service( .service(
Files::new("/images", "./static/images") Files::new("/images", "./static/images")
.show_files_listing(), .show_files_listing(),

51
src/models.rs Normal file
View file

@ -0,0 +1,51 @@
use serde::{Deserialize, Serialize};
use bigdecimal::BigDecimal;
use chrono::NaiveDateTime;
#[derive(Deserialize)]
pub struct RegistrationData {
pub email: String,
pub haslo: String,
pub imie: String,
#[serde(rename = "confirmPassword")]
pub confirm_password: String,
}
#[derive(Deserialize)]
pub struct LoginData {
pub email: String,
pub haslo: String,
}
#[derive(Serialize)]
pub struct LoginResponse {
pub token: String,
pub imie: String,
}
#[derive(sqlx::FromRow, Serialize)]
pub struct Book {
pub id: i32,
pub tytul: String,
pub autor: String,
pub cena: BigDecimal,
pub obraz_url: Option<String>,
pub opis: Option<String>,
}
#[derive(Deserialize)]
pub struct CartItem {
pub book_id: i32,
pub quantity: i32,
}
#[derive(Serialize)]
pub struct CartItemResponse {
pub book_id: i32,
pub quantity: i32,
pub tytul: String,
pub cena: BigDecimal,
#[serde(rename = "obraz_url")]
pub obraz_url: String,
}

97
src/profile.rs Normal file
View file

@ -0,0 +1,97 @@
use actix_web::{get, web, HttpResponse, HttpRequest};
use serde::Serialize;
use sqlx::PgPool;
use crate::error::AppError;
use bigdecimal::BigDecimal;
use chrono::NaiveDateTime;
// Struktura reprezentująca pojedynczą pozycję zamówienia
#[derive(Serialize)]
pub struct OrderItem {
pub tytul: String,
pub autor: String,
pub ilosc: i32,
pub cena: BigDecimal,
}
// Struktura reprezentująca zamówienie z pozycjami
#[derive(Serialize)]
pub struct OrderWithItems {
pub id: i32,
pub data_zamowienia: NaiveDateTime,
pub suma_totalna: BigDecimal,
pub status: Option<String>,
pub items: Vec<OrderItem>,
}
// Funkcja pomocnicza do walidacji tokenu
async fn validate_token(token: Option<&str>) -> Result<i32, AppError> {
let raw_token = token.ok_or(AppError::Unauthorized("Unauthorized".to_string()))?;
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(|_| AppError::Unauthorized("Invalid token".to_string()))?;
Ok(user_id)
} else {
Err(AppError::Unauthorized("Unauthorized".to_string()))
}
}
// Funkcja pomocnicza do pobierania tokenu z nagłówka
fn get_token(req: &HttpRequest) -> Option<&str> {
req.headers().get("Authorization").and_then(|h| h.to_str().ok())
}
#[get("/api/order-history")]
pub async fn get_order_history(
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
let user_id = validate_token(get_token(&req)).await?;
let orders = sqlx::query!(
r#"
SELECT
z.id as "id!",
z.data_zamowienia as "data_zamowienia!",
z.suma_totalna as "suma_totalna!",
z.status,
pz.ilosc as "ilosc!",
pz.cena as "item_price!",
k.tytul as "tytul!",
k.autor as "autor!"
FROM zamowienia z
JOIN pozycje_zamowienia pz ON z.id = pz.zamowienie_id
JOIN ksiazki k ON pz.book_id = k.id
WHERE z.user_id = $1
ORDER BY z.id DESC, k.tytul ASC
"#,
user_id
)
.fetch_all(pool.get_ref())
.await
.map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
let mut grouped_orders: std::collections::HashMap<i32, OrderWithItems> = std::collections::HashMap::new();
for record in orders {
let entry = grouped_orders.entry(record.id).or_insert(OrderWithItems {
id: record.id,
data_zamowienia: record.data_zamowienia,
suma_totalna: record.suma_totalna,
status: record.status,
items: Vec::new(),
});
entry.items.push(OrderItem {
tytul: record.tytul,
autor: record.autor,
ilosc: record.ilosc,
cena: record.item_price,
});
}
let result: Vec<OrderWithItems> = grouped_orders.into_values().collect();
Ok(HttpResponse::Ok().json(result))
}

View file

@ -54,7 +54,7 @@
<p id="book-description"></p> <p id="book-description"></p>
</div> </div>
</div> </div>
<button class="btn">Dodaj do koszyka</button> <button class="btn btn-add-to-cart">Dodaj do koszyka</button>
</div> </div>
</div> </div>
</main> </main>
@ -76,7 +76,8 @@
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/main.js"></script> <script type="module" src="/js/utils.js"></script>
<script src="/js/book.js"></script> <script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/book.js"></script>
</body> </body>
</html> </html>

View file

@ -72,6 +72,8 @@
</div> </div>
</footer> </footer>
<script src="/js/cart.js"></script> <script type="module" src="/js/utils.js"></script>
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/cart.js"></script>
</body> </body>
</html> </html>

View file

@ -61,7 +61,6 @@ footer {
font-size: 12px; font-size: 12px;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
color: var(--gold1); color: var(--gold1);
position: fixed;
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 100%; width: 100%;

66
static/js/auth.js Normal file
View file

@ -0,0 +1,66 @@
// static/js/auth.js
import { handleApiError, updateAuthUI, setupLogout } from './utils.js';
document.addEventListener('DOMContentLoaded', () => {
updateAuthUI();
setupLogout();
// Logowanie
document.getElementById('loginForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
try {
const response = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: document.getElementById('loginEmail').value,
haslo: document.getElementById('loginPassword').value
})
});
if (response.ok) {
const { token, imie } = await response.json();
localStorage.setItem('token', token);
localStorage.setItem('imie', imie);
window.location.href = '/';
} else {
throw new Error('Błąd logowania!');
}
} catch (error) {
handleApiError(error);
}
});
// Rejestracja
document.getElementById('registerForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('registerName').value;
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
const confirmPassword = document.getElementById('registerConfirmPassword').value;
if (password !== confirmPassword) {
alert('Hasła nie są identyczne');
return;
}
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! Możesz się teraz zalogować.');
window.location.href = '/login.html';
} else {
throw new Error('Rejestracja nieudana');
}
} catch (error) {
handleApiError(error);
}
});
});

View file

@ -1,3 +1,7 @@
// static/js/book.js
import { getAuthHeaders, handleApiError } from './utils.js';
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const bookId = urlParams.get('id'); const bookId = urlParams.get('id');
@ -15,9 +19,7 @@ document.addEventListener('DOMContentLoaded', async () => {
try { try {
const response = await fetch(`/api/ksiazki/${bookId}`); const response = await fetch(`/api/ksiazki/${bookId}`);
if (!response.ok) { if (!response.ok) throw new Error(`Status: ${response.status}`);
throw new Error(`Status: ${response.status}`);
}
const book = await response.json(); const book = await response.json();
document.getElementById('book-title').textContent = book.tytul; document.getElementById('book-title').textContent = book.tytul;
@ -26,8 +28,8 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('book-description').textContent = book.opis; document.getElementById('book-description').textContent = book.opis;
document.getElementById('book-cover').src = book.obraz_url; document.getElementById('book-cover').src = book.obraz_url;
// Dodaj obsługę przycisku "Dodaj do koszyka" // Dodaj do koszyka
document.querySelector('.btn').addEventListener('click', async () => { document.querySelector('.btn-add-to-cart').addEventListener('click', async () => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
alert('Musisz być zalogowany, aby dodać książkę do koszyka'); alert('Musisz być zalogowany, aby dodać książkę do koszyka');
@ -38,10 +40,7 @@ document.addEventListener('DOMContentLoaded', async () => {
try { try {
const response = await fetch('/api/add-to-cart', { const response = await fetch('/api/add-to-cart', {
method: 'POST', method: 'POST',
headers: { headers: getAuthHeaders(),
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ body: JSON.stringify({
book_id: parseInt(bookId), book_id: parseInt(bookId),
quantity: 1 quantity: 1
@ -51,16 +50,13 @@ document.addEventListener('DOMContentLoaded', async () => {
if (response.ok) { if (response.ok) {
alert('Dodano do koszyka!'); alert('Dodano do koszyka!');
} else { } else {
const error = await response.json(); throw new Error('Wystąpił błąd');
alert(error.error || 'Wystąpił błąd');
} }
} catch (error) { } catch (error) {
console.error('Błąd:', error); handleApiError(error);
alert('Wystąpił błąd podczas dodawania do koszyka');
} }
}); });
} catch (error) { } catch (error) {
console.error('Błąd:', error);
bookDetails.innerHTML = ` bookDetails.innerHTML = `
<div class="col-12 text-center py-5"> <div class="col-12 text-center py-5">
<h2 class="text-danger">Błąd ładowania książki</h2> <h2 class="text-danger">Błąd ładowania książki</h2>

66
static/js/books.js Normal file
View file

@ -0,0 +1,66 @@
// static/js/books.js
import { getAuthHeaders, handleApiError } from './utils.js';
export async function loadBooks(searchTerm = '', sortBy = 'default') {
try {
const response = await fetch(`/api/ksiazki?search=${encodeURIComponent(searchTerm)}&sort=${sortBy}`);
if (!response.ok) throw new Error(`Błąd HTTP: ${response.status}`);
return await response.json();
} catch (error) {
handleApiError(error, 'Wystąpił błąd podczas ładowania książek');
return [];
}
}
export function renderBooks(books, containerId) {
const container = document.getElementById(containerId);
if (!container) return;
if (books.length === 0) {
container.innerHTML = '<div class="col-12 text-center py-5"><h3>Nie znaleziono książek</h3></div>';
return;
}
container.innerHTML = books.map(book => `
<div class="col">
<div class="book-card h-100">
<a href="/book.html?id=${book.id}">
<div class="cover-container">
<img src="${book.obraz_url}" class="book-cover" alt="${book.tytul}">
</div>
</a>
<div class="book-body p-3">
<h5 class="book-title">${book.tytul}</h5>
<div class="mt-auto">
<p class="book-author mb-1">${book.autor}</p>
<p class="book-price mb-0">${book.cena} PLN</p>
</div>
</div>
</div>
</div>
`).join('');
}
// Inicjalizacja na stronie głównej
document.addEventListener('DOMContentLoaded', async () => {
if (!document.getElementById('books-container')) return;
const books = await loadBooks();
renderBooks(books, 'books-container');
// Obsługa wyszukiwania i sortowania
const searchInput = document.getElementById('searchInput');
const sortSelect = document.getElementById('sortSelect');
const reloadBooks = async () => {
const books = await loadBooks(
searchInput?.value || '',
sortSelect?.value || 'default'
);
renderBooks(books, 'books-container');
};
searchInput?.addEventListener('input', reloadBooks);
sortSelect?.addEventListener('change', reloadBooks);
});

View file

@ -1,3 +1,7 @@
// static/js/cart.js
import { getAuthHeaders, handleApiError } from './utils.js';
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
@ -7,123 +11,122 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
const response = await fetch('/api/cart', { const response = await fetch('/api/cart', {
headers: { headers: getAuthHeaders()
'Authorization': `Bearer ${token}`
}
}); });
if (!response.ok) throw new Error('Błąd ładowania koszyka'); if (!response.ok) throw new Error('Błąd ładowania koszyka');
const cartItems = await response.json(); const cartItems = await response.json();
const container = document.getElementById('cart-items'); renderCart(cartItems);
container.innerHTML = '';
if (cartItems.length === 0) { } catch (error) {
container.innerHTML = '<p class="text-center">Twój koszyk jest pusty</p>'; handleApiError(error, 'Nie udało się załadować koszyka');
return; }
} });
let totalCartValue = 0; function renderCart(cartItems) {
const container = document.getElementById('cart-items');
container.innerHTML = '';
cartItems.forEach(item => { if (cartItems.length === 0) {
// Formatowanie cen container.innerHTML = '<p class="text-center">Twój koszyk jest pusty</p>';
const price = parseFloat(item.cena); return;
const formattedPrice = price.toFixed(2); }
const itemTotal = price * item.quantity;
const formattedTotal = itemTotal.toFixed(2);
totalCartValue += itemTotal;
const itemHTML = ` let totalCartValue = 0;
<div class="col-12 mb-4 cart-item">
<div class="card dark-card"> cartItems.forEach(item => {
<div class="row g-0"> const price = parseFloat(item.cena);
<div class="col-md-4 text-center p-3 cart-item-image"> const itemTotal = price * item.quantity;
<img src="${item.obraz_url}" totalCartValue += itemTotal;
class="img-fluid rounded"
alt="${item.tytul}" const itemHTML = `
style="max-height: 200px;"> <div class="col-12 mb-4 cart-item">
</div> <div class="card dark-card">
<div class="col-md-8 cart-item-details"> <div class="row g-0">
<div class="card-body h-100 d-flex flex-column"> <div class="col-md-4 text-center p-3 cart-item-image">
<div> <img src="${item.obraz_url}"
<h5 class="card-title cart-item-title">${item.tytul}</h5> class="img-fluid rounded"
<p class="card-text cart-item-author">${item.autor}</p> alt="${item.tytul}"
style="max-height: 200px;">
</div>
<div class="col-md-8 cart-item-details">
<div class="card-body h-100 d-flex flex-column">
<div>
<h5 class="card-title cart-item-title">${item.tytul}</h5>
<p class="card-text cart-item-author">${item.autor}</p>
</div>
<div class="d-flex flex-wrap gap-3 mb-2 cart-item-info mt-auto">
<div class="d-flex flex-column flex-grow-1">
<span class="fw-bold cart-item-price">Cena jednostkowa</span>
<span class="fs-5">${price.toFixed(2)} PLN</span>
</div> </div>
<div class="d-flex flex-wrap gap-3 mb-2 cart-item-info mt-auto"> <div class="d-flex flex-column">
<div class="d-flex flex-column flex-grow-1"> <span class="fw-bold">Ilość</span>
<span class="fw-bold cart-item-price">Cena jednostkowa</span> <div class="d-flex align-items-center gap-2">
<span class="fs-5">${formattedPrice} PLN</span> <span class="fs-5 badge bg-dark rounded-pill">${item.quantity}</span>
</div>
<div class="d-flex flex-column">
<span class="fw-bold">Ilość</span>
<div class="d-flex align-items-center gap-2">
<span class="fs-5 badge bg-dark rounded-pill">${item.quantity}</span>
</div>
</div>
<div class="d-flex flex-column">
<span class="fw-bold cart-item-total">Suma</span>
<span class="fs-5 text-warning">${formattedTotal} PLN</span>
</div> </div>
</div> </div>
<div class="mt-3"> <div class="d-flex flex-column">
<div class="btn-group w-100" role="group"> <span class="fw-bold cart-item-total">Suma</span>
<button class="btn btn-sm btn-outline-secondary decrease-quantity flex-grow-1" <span class="fs-5 text-warning">${itemTotal.toFixed(2)} PLN</span>
data-book-id="${item.book_id}"> </div>
<i class="bi bi-dash"></i> Zmniejsz ilość </div>
</button>
<button class="btn btn-sm btn-outline-danger remove-from-cart cart-remove-btn flex-grow-1" <div class="mt-3">
data-book-id="${item.book_id}"> <div class="btn-group w-100" role="group">
<i class="bi bi-trash"></i> Usuń <button class="btn btn-sm btn-outline-secondary decrease-quantity flex-grow-1"
</button> data-book-id="${item.book_id}">
</div> <i class="bi bi-dash"></i> Zmniejsz ilość
</button>
<button class="btn btn-sm btn-outline-danger remove-from-cart cart-remove-btn flex-grow-1"
data-book-id="${item.book_id}">
<i class="bi bi-trash"></i> Usuń
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
`;
container.insertAdjacentHTML('beforeend', itemHTML);
});
// Dodaj całkowitą sumę koszyka
const totalHTML = `
<div class="col-12 mt-4">
<div class="card dark-card p-4">
<div class="d-flex justify-content-between align-items-center">
<h2 class="mb-0">Suma całkowita</h2>
<h2 class="mb-0 total-cart-value">${totalCartValue.toFixed(2)} PLN</h2>
</div>
</div>
</div> </div>
`; `;
container.insertAdjacentHTML('beforeend', totalHTML); container.insertAdjacentHTML('beforeend', itemHTML);
});
// Obsługa przycisku zmniejszania ilości // Suma całkowita
document.querySelectorAll('.decrease-quantity').forEach(button => { const totalHTML = `
button.addEventListener('click', function() { <div class="col-12 mt-4">
const bookId = this.getAttribute('data-book-id'); <div class="card dark-card p-4">
updateCartItemQuantity(bookId, 'decrease'); <div class="d-flex justify-content-between align-items-center">
}); <h2 class="mb-0">Suma całkowita</h2>
<h2 class="mb-0 total-cart-value">${totalCartValue.toFixed(2)} PLN</h2>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', totalHTML);
// Obsługa przycisków
document.querySelectorAll('.decrease-quantity').forEach(button => {
button.addEventListener('click', function() {
const bookId = this.dataset.bookId;
updateCartItemQuantity(bookId, 'decrease');
}); });
});
// Obsługa przycisku usuwania document.querySelectorAll('.remove-from-cart').forEach(button => {
document.querySelectorAll('.remove-from-cart').forEach(button => { button.addEventListener('click', function() {
button.addEventListener('click', function() { const bookId = this.dataset.bookId;
const bookId = this.getAttribute('data-book-id'); updateCartItemQuantity(bookId, 'remove');
updateCartItemQuantity(bookId, 'remove');
});
}); });
});
} catch (error) { // Finalizacja zamówienia
console.error('Error:', error); document.getElementById('checkoutBtn')?.addEventListener('click', handleCheckout);
alert('Nie udało się załadować koszyka'); }
}
});
async function updateCartItemQuantity(bookId, action) { async function updateCartItemQuantity(bookId, action) {
try { try {
@ -133,36 +136,28 @@ async function updateCartItemQuantity(bookId, action) {
return; return;
} }
let endpoint, method; let method, endpoint;
if (action === 'decrease') { if (action === 'decrease') {
endpoint = `/api/decrease-cart-item/${bookId}`; endpoint = `/api/decrease-cart-item/${bookId}`;
method = 'POST'; method = 'POST';
} else if (action === 'remove') { } else {
endpoint = `/api/remove-from-cart/${bookId}`; endpoint = `/api/remove-from-cart/${bookId}`;
method = 'DELETE'; method = 'DELETE';
} else {
throw new Error('Nieznana akcja');
} }
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: method, method,
headers: { headers: getAuthHeaders()
'Authorization': `Bearer ${token}`
}
}); });
if (!response.ok) throw new Error('Błąd aktualizacji koszyka'); if (!response.ok) throw new Error('Błąd aktualizacji koszyka');
// Odśwież koszyk
location.reload(); location.reload();
} catch (error) { } catch (error) {
console.error('Error:', error); handleApiError(error);
alert('Nie udało się zaktualizować koszyka');
} }
} }
async function removeFromCart(bookId) { async function handleCheckout() {
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
@ -170,64 +165,30 @@ async function removeFromCart(bookId) {
return; return;
} }
const response = await fetch(`/api/remove-from-cart/${bookId}`, { // Pobierz zawartość koszyka
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) throw new Error('Błąd usuwania z koszyka');
// Odśwież koszyk
location.reload();
} catch (error) {
console.error('Error:', error);
alert('Nie udało się usunąć z koszyka');
}
}
document.getElementById('checkoutBtn').addEventListener('click', async () => {
try {
const token = localStorage.getItem('token');
if (!token) {
window.location.href = '/login.html';
return;
}
// Pobierz aktualną zawartość koszyka
const cartResponse = await fetch('/api/cart', { const cartResponse = await fetch('/api/cart', {
headers: { 'Authorization': `Bearer ${token}` } headers: getAuthHeaders()
}); });
if (!cartResponse.ok) throw new Error('Błąd pobierania koszyka');
const cartItems = await cartResponse.json(); const cartItems = await cartResponse.json();
// Przygotuj dane do zamówienia
const checkoutData = {
items: cartItems.map(item => ({
book_id: item.book_id,
quantity: item.quantity
})),
total: cartItems.reduce((sum, item) =>
sum + (parseFloat(item.cena) * item.quantity), 0)
};
// Wyślij zamówienie // Wyślij zamówienie
const response = await fetch('/api/checkout', { const response = await fetch('/api/checkout', {
method: 'POST', method: 'POST',
headers: { headers: getAuthHeaders(),
'Content-Type': 'application/json', body: JSON.stringify({
'Authorization': `Bearer ${token}` items: cartItems.map(item => ({
}, book_id: item.book_id,
body: JSON.stringify(checkoutData) quantity: item.quantity
})),
total: cartItems.reduce((sum, item) =>
sum + (parseFloat(item.cena) * item.quantity), 0)
})
}); });
if (!response.ok) throw new Error('Błąd podczas składania zamówienia'); if (!response.ok) throw new Error('Błąd podczas składania zamówienia');
window.location.href = '/thankyou.html'; window.location.href = '/thankyou.html';
} catch (error) { } catch (error) {
console.error('Error:', error); handleApiError(error, 'Nie udało się złożyć zamówienia');
alert('Nie udało się złożyć zamówienia: ' + error.message);
} }
}); }

View file

@ -1,3 +1,7 @@
// static/js/profile.js
import { getAuthHeaders, handleApiError } from './utils.js';
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
@ -7,58 +11,58 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
const response = await fetch('/api/order-history', { const response = await fetch('/api/order-history', {
headers: { 'Authorization': `Bearer ${token}` } headers: getAuthHeaders()
}); });
if (!response.ok) throw new Error('Błąd ładowania historii'); if (!response.ok) throw new Error('Błąd ładowania historii');
const orders = await response.json(); const orders = await response.json();
const container = document.getElementById('order-history'); renderOrderHistory(orders);
container.innerHTML = '';
orders.forEach((order, index) => {
const orderNumber = orders.length - index;
const orderDate = new Date(order.data_zamowienia).toLocaleDateString();
const itemsList = order.items.map(item => `
<div class="order card mb-2">
<div class="card-body">
<h6 class="card-subtitle mb-2">${item.tytul}</h6>
<p class="card-text">Autor: ${item.autor}</p>
<p class="card-text">Ilość: ${item.ilosc} × ${item.cena} PLN</p>
</div>
</div>
`).join('');
const orderHTML = `
<div class="mb-4">
<div class="card">
<div class="card-header">
<h5 class="card-title">Zamówienie #${orderNumber}</h5>
<p class="card-text">Data: ${orderDate}</p>
</div>
<div class="card-body">
<h6>Pozycje:</h6>
${itemsList}
<hr class="bg-secondary">
<div class="d-flex justify-content-between">
<p class="fw-bold">Suma całkowita:</p>
<p class="fw-bold">${order.suma_totalna} PLN</p>
</div>
<div class="d-flex justify-content-between">
<p>Status:</p>
<p>${order.status || 'Przyjęto do realizacji'}</p>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', orderHTML);
});
} catch (error) { } catch (error) {
console.error('Error:', error); handleApiError(error, 'Nie udało się załadować historii zamówień');
alert('Nie udało się załadować historii zamówień');
} }
}); });
function renderOrderHistory(orders) {
const container = document.getElementById('order-history');
container.innerHTML = '';
orders.forEach((order, index) => {
const orderNumber = orders.length - index;
const orderDate = new Date(order.data_zamowienia).toLocaleDateString();
const itemsList = order.items.map(item => `
<div class="order card mb-2">
<div class="card-body">
<h6 class="card-subtitle mb-2">${item.tytul}</h6>
<p class="card-text">Autor: ${item.autor}</p>
<p class="card-text">Ilość: ${item.ilosc} × ${item.cena} PLN</p>
</div>
</div>
`).join('');
const orderHTML = `
<div class="mb-4">
<div class="card">
<div class="card-header">
<h5 class="card-title">Zamówienie #${orderNumber}</h5>
<p class="card-text">Data: ${orderDate}</p>
</div>
<div class="card-body">
<h6>Pozycje:</h6>
${itemsList}
<hr class="bg-secondary">
<div class="d-flex justify-content-between">
<p class="fw-bold">Suma całkowita:</p>
<p class="fw-bold">${order.suma_totalna} PLN</p>
</div>
<div class="d-flex justify-content-between">
<p>Status:</p>
<p>${order.status || 'Przyjęto do realizacji'}</p>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', orderHTML);
});
}

30
static/js/utils.js Normal file
View file

@ -0,0 +1,30 @@
// static/js/utils.js
export function getAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
};
}
export function handleApiError(error, fallbackMsg = 'Wystąpił błąd') {
console.error('Error:', error);
alert(error.message || fallbackMsg);
}
export function updateAuthUI() {
const token = localStorage.getItem('token');
document.querySelectorAll('.auth-links').forEach(container => {
container.querySelector('.anonymous-links').style.display = token ? 'none' : 'flex';
container.querySelector('.user-links').style.display = token ? 'flex' : 'none';
});
}
export function setupLogout() {
document.getElementById('logoutLink')?.addEventListener('click', (e) => {
e.preventDefault();
localStorage.removeItem('token');
window.location.href = '/';
});
}

View file

@ -67,6 +67,8 @@
</div> </div>
</footer> </footer>
<script src="/js/profile.js"></script> <script type="module" src="/js/utils.js"></script>
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/profile.js"></script>
</body> </body>
</html> </html>