cart
This commit is contained in:
parent
100bda3bab
commit
b7f3a43495
15 changed files with 949 additions and 142 deletions
1
.env
1
.env
|
@ -1 +1,2 @@
|
|||
DATABASE_URL="postgres://postgres:secret@localhost/ksiegarnia"
|
||||
SQLX_LOG=info
|
||||
|
|
109
Cargo.lock
generated
109
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
25
migrations/20250525061434_zamowienia.sql
Normal file
25
migrations/20250525061434_zamowienia.sql
Normal file
|
@ -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
|
||||
);
|
||||
|
28
src/auth.rs
28
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<RegistrationData>,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> 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<LoginData>,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> impl Responder {
|
||||
|
@ -93,7 +93,7 @@ async fn login(
|
|||
token: dummy_token,
|
||||
imie: user.imie,
|
||||
})
|
||||
}
|
||||
},
|
||||
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
|
||||
}
|
||||
}
|
||||
|
|
372
src/main.rs
372
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<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
|
@ -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<sqlx::PgPool>) -> 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<sqlx::PgPool>,
|
||||
path: web::Path<i32>,
|
||||
) -> 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<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"#,
|
||||
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"})))
|
||||
}
|
||||
|
||||
#[get("/api/order-history")]
|
||||
async fn get_order_history(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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<sqlx::PgPool>,
|
||||
data: web::Json<CheckoutRequest>,
|
||||
) -> 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?;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
21
static/cart.html
Normal file
21
static/cart.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- static/cart.html -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenaeum</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/css/styles.css" rel="stylesheet">
|
||||
<link href="https://fonts.cdnfonts.com/css/old-english-text-mt" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lobster&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<main class="container py-5">
|
||||
<h2 class="neon-title mb-4">Twój koszyk</h2>
|
||||
<div id="cart-items" class="row g-4"></div>
|
||||
</main>
|
||||
<script src="/js/cart.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<main class="container py-5">
|
||||
<h2 class="text-center mb-5 neon-title">NOWOŚCI</h2>
|
||||
|
||||
<div class="row g-4" id="books-container">
|
||||
<div class="row row-cols-1 row-cols-md-3 row-cols-lg-4 g-4" id="books-container">
|
||||
<!-- Dynamicznie ładowane książki -->
|
||||
<div class="col-12 text-center">
|
||||
<div class="spinner-border text-danger" role="status">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
91
static/js/cart.js
Normal file
91
static/js/cart.js
Normal file
|
@ -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 = `
|
||||
<div class="col-12 text-center">
|
||||
<p class="text-muted">Twój koszyk jest pusty</p>
|
||||
<a href="/" class="btn btn-gothic">Przeglądaj książki</a>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('cart-items');
|
||||
container.innerHTML = items.map(item => `
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card dark-card h-100">
|
||||
<img src="${item.obraz_url}"
|
||||
class="card-img-top"
|
||||
style="height: 200px; object-fit: cover;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${item.tytul}</h5>
|
||||
<p class="card-text">Ilość: ${item.quantity}</p>
|
||||
<p class="text-danger">${item.cena.toString()} PLN</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).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 = `
|
||||
<div class="col-md-6">
|
||||
<div class="card dark-card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<img src="${item.obraz_url}" class="img-fluid rounded-start" alt="${item.tytul}">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${item.tytul}</h5>
|
||||
<p class="card-text">Ilość: ${item.quantity}</p>
|
||||
<p class="card-text">Cena: ${item.cena} PLN</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', itemHTML);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Nie udało się załadować koszyka');
|
||||
}
|
||||
});
|
|
@ -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 = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<h3>Nie znaleziono książek</h3>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = books.map(book => `
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="book-card card h-100">
|
||||
<a href="/book.html?id=${book.id}" class="book-link">
|
||||
<div class="book-cover">
|
||||
<img src="${book.obraz_url}"
|
||||
class="card-img-top"
|
||||
<img src="${book.obraz_url}"
|
||||
class="card-img-top"
|
||||
alt="${book.tytul}"
|
||||
onerror="this.src='/images/placeholder.jpg'">
|
||||
<div class="book-overlay">
|
||||
|
@ -144,15 +141,60 @@ function renderBooks(books) {
|
|||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${book.tytul}</h5>
|
||||
<p class="card-text">${book.autor}</p>
|
||||
<p class="card-text">${book.cena} PLN</p>
|
||||
<button class="btn btn-gothic add-to-cart" data-book-id="${book.id}">Dodaj do koszyka</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).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 = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="alert alert-danger">
|
||||
|
@ -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);
|
||||
|
|
31
static/js/profile.js
Normal file
31
static/js/profile.js
Normal file
|
@ -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 => `
|
||||
<div class="card dark-card mb-3">
|
||||
<div class="card-body">
|
||||
<h5>Zamówienie #${order.id}</h5>
|
||||
<p>Data: ${new Date(order.data_zamowienia).toLocaleDateString()}</p>
|
||||
<p>Status: ${order.status}</p>
|
||||
<p>Suma: ${order.suma_totalna} PLN</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadOrderHistory);
|
60
static/profile.html
Normal file
60
static/profile.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenaeum</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/css/styles.css" rel="stylesheet">
|
||||
<link href="https://fonts.cdnfonts.com/css/old-english-text-mt" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lobster&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-black">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENAEUM</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
|
||||
<div class="navbar-nav gap-3">
|
||||
<form class="d-flex" id="searchForm">
|
||||
<input class="form-control me-2 dark-input"
|
||||
type="search"
|
||||
placeholder="Szukaj książek..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
|
||||
<div class="anonymous-links d-flex gap-3">
|
||||
<a class="nav-link" href="/login.html">Logowanie</a>
|
||||
<a class="nav-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links d-flex gap-3" style="display: none !important;">
|
||||
<a class="nav-link" href="/profile.html">Profil</a>
|
||||
<a class="nav-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
</div>
|
||||
|
||||
<a class="nav-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container py-5">
|
||||
<h2 class="neon-title mb-4">Twój profil</h2>
|
||||
|
||||
<div class="dark-card p-4 mb-4">
|
||||
<h3 class="lobster-font mb-3">Historia zamówień</h3>
|
||||
<div id="order-history"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/js/profile.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -64,6 +64,9 @@
|
|||
</div>
|
||||
<button type="submit" class="btn btn-gothic w-100">ZAREJESTRUJ SIĘ</button>
|
||||
</form>
|
||||
<div class="login-link">
|
||||
Masz już konto? <a href="/login.html" class="text-accent">Zaloguj się</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 py-3 bg-dark">
|
||||
|
|
Loading…
Add table
Reference in a new issue