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"
|
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 = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
@ -955,6 +980,21 @@ version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
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]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -999,6 +1039,17 @@ version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
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]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -1017,8 +1068,10 @@ version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1411,8 +1464,11 @@ dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"futures",
|
||||||
"log",
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
|
@ -1742,6 +1798,15 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
@ -1763,6 +1828,44 @@ version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
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]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
@ -2294,6 +2397,12 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
|
|
@ -18,3 +18,7 @@ bcrypt = "0.15"
|
||||||
rust_decimal = { version = "1.37.1", features = ["serde", "db-postgres"] }
|
rust_decimal = { version = "1.37.1", features = ["serde", "db-postgres"] }
|
||||||
bigdecimal = { version = "0.3.0", features = ["serde"] }
|
bigdecimal = { version = "0.3.0", features = ["serde"] }
|
||||||
regex = "1.10.4"
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct RegistrationData {
|
pub struct RegistrationData {
|
||||||
email: String,
|
pub email: String,
|
||||||
haslo: String,
|
pub haslo: String,
|
||||||
imie: String,
|
pub imie: String,
|
||||||
#[serde(rename = "confirmPassword")]
|
#[serde(rename = "confirmPassword")]
|
||||||
confirm_password: String,
|
pub confirm_password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/rejestracja")]
|
#[post("/rejestracja")]
|
||||||
async fn rejestracja(
|
pub async fn rejestracja(
|
||||||
form: web::Json<RegistrationData>,
|
form: web::Json<RegistrationData>,
|
||||||
pool: web::Data<sqlx::PgPool>,
|
pool: web::Data<sqlx::PgPool>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
|
@ -57,19 +57,19 @@ async fn rejestracja(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct LoginData {
|
pub struct LoginData {
|
||||||
email: String,
|
pub email: String,
|
||||||
haslo: String,
|
pub haslo: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct LoginResponse {
|
pub struct LoginResponse {
|
||||||
token: String,
|
pub token: String,
|
||||||
imie: String,
|
pub imie: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/login")]
|
#[post("/login")]
|
||||||
async fn login(
|
pub async fn login(
|
||||||
form: web::Json<LoginData>,
|
form: web::Json<LoginData>,
|
||||||
pool: web::Data<sqlx::PgPool>,
|
pool: web::Data<sqlx::PgPool>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
|
@ -93,7 +93,7 @@ async fn login(
|
||||||
token: dummy_token,
|
token: dummy_token,
|
||||||
imie: user.imie,
|
imie: user.imie,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
|
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
352
src/main.rs
352
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_cors::Cors;
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
use actix_web::{
|
|
||||||
get, http::header, web, App, HttpResponse, HttpServer, Responder,
|
|
||||||
};
|
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use env_logger::{Builder, Env};
|
use env_logger::{Builder, Env};
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use rust_decimal::Decimal;
|
use serde::{Deserialize, Serialize};
|
||||||
use bigdecimal::BigDecimal;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::HashMap;
|
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)]
|
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||||
struct Book {
|
struct Book {
|
||||||
|
@ -24,6 +47,116 @@ struct Book {
|
||||||
opis: Option<String>,
|
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")]
|
#[get("/api/ksiazki")]
|
||||||
async fn get_ksiazki(
|
async fn get_ksiazki(
|
||||||
pool: web::Data<sqlx::PgPool>,
|
pool: web::Data<sqlx::PgPool>,
|
||||||
|
@ -73,23 +206,6 @@ 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}")]
|
#[get("/api/ksiazki/{id}")]
|
||||||
async fn get_ksiazka(
|
async fn get_ksiazka(
|
||||||
pool: web::Data<sqlx::PgPool>,
|
pool: web::Data<sqlx::PgPool>,
|
||||||
|
@ -125,11 +241,176 @@ async fn get_ksiazka(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/check-auth")]
|
#[get("/api/check-auth")]
|
||||||
async fn check_auth() -> impl Responder {
|
async fn check_auth(req: HttpRequest) -> impl Responder {
|
||||||
HttpResponse::Ok().json(json!({
|
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,
|
"authenticated": false,
|
||||||
"user": null
|
"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]
|
#[actix_web::main]
|
||||||
|
@ -158,18 +439,24 @@ async fn main() -> std::io::Result<()> {
|
||||||
header::AUTHORIZATION,
|
header::AUTHORIZATION,
|
||||||
header::ACCEPT,
|
header::ACCEPT,
|
||||||
header::HeaderName::from_static("content-type"),
|
header::HeaderName::from_static("content-type"),
|
||||||
]);
|
])
|
||||||
|
.supports_credentials();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(pool.clone())) // Dodaj pulę jako globalny stan
|
.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(get_ksiazki)
|
||||||
.service(get_ksiazka)
|
.service(get_ksiazka)
|
||||||
.service(auth::rejestracja)
|
.service(rejestracja)
|
||||||
.service(auth::login)
|
.service(login)
|
||||||
|
.service(get_cart)
|
||||||
|
.service(add_to_cart) // Dodaj
|
||||||
|
.service(checkout) // Dodaj
|
||||||
|
.service(check_auth)
|
||||||
|
.service(get_order_history)
|
||||||
.service(
|
.service(
|
||||||
Files::new("/images", "./static/images") // Nowy endpoint dla obrazków
|
Files::new("/images", "./static/images")
|
||||||
.show_files_listing(),
|
.show_files_listing(),
|
||||||
)
|
)
|
||||||
.service(Files::new("/", "./static").index_file("index.html"))
|
.service(Files::new("/", "./static").index_file("index.html"))
|
||||||
|
@ -178,3 +465,4 @@ async fn main() -> std::io::Result<()> {
|
||||||
.run()
|
.run()
|
||||||
.await
|
.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 {
|
.auth-container a {
|
||||||
color: var(--accent-blue) !important;
|
color: var(--accent-blue) !important;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
@ -53,9 +55,11 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container {
|
.auth-container {
|
||||||
background: rgba(7, 44, 36, 0.9);
|
max-width: 500px;
|
||||||
border: 2px solid #2a6b5e;
|
margin: 2rem auto;
|
||||||
backdrop-filter: blur(5px);
|
padding: 2rem;
|
||||||
|
border: 1px solid #2a6b5e;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container h2 {
|
.auth-container h2 {
|
||||||
|
@ -66,10 +70,9 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
#books-container {
|
#books-container {
|
||||||
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem 2%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-card {
|
.book-card {
|
||||||
|
@ -145,9 +148,10 @@ footer a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-card {
|
.dark-card {
|
||||||
background: #09342b;
|
background-color: #1a1a1a;
|
||||||
border: 1px solid #1c4d42;
|
border: 1px solid #333;
|
||||||
transition: transform 0.3s ease;
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-card:hover {
|
.dark-card:hover {
|
||||||
|
@ -212,3 +216,25 @@ footer a {
|
||||||
color: #93B8B1;
|
color: #93B8B1;
|
||||||
opacity: 0.7;
|
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">
|
<main class="container py-5">
|
||||||
<h2 class="text-center mb-5 neon-title">NOWOŚCI</h2>
|
<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 -->
|
<!-- Dynamicznie ładowane książki -->
|
||||||
<div class="col-12 text-center">
|
<div class="col-12 text-center">
|
||||||
<div class="spinner-border text-danger" role="status">
|
<div class="spinner-border text-danger" role="status">
|
||||||
|
|
|
@ -25,6 +25,40 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
document.getElementById('book-price').textContent = `Cena: ${book.cena} PLN`;
|
document.getElementById('book-price').textContent = `Cena: ${book.cena} PLN`;
|
||||||
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"
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('Błąd:', error);
|
console.error('Błąd:', error);
|
||||||
bookDetails.innerHTML = `
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
updateNavVisibility();
|
updateNavVisibility();
|
||||||
setInterval(updateNavVisibility, 1000); // Aktualizuj co sekundę
|
checkAuthStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateNavVisibility() {
|
function updateNavVisibility() {
|
||||||
|
@ -16,35 +16,35 @@ 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) => {
|
document.getElementById('logoutLink')?.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
localStorage.removeItem('imie');
|
localStorage.removeItem('imie');
|
||||||
|
updateNavVisibility();
|
||||||
window.location.href = '/';
|
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) => {
|
document.getElementById('loginForm')?.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const email = document.getElementById('loginEmail').value;
|
const email = document.getElementById('loginEmail').value;
|
||||||
const password = document.getElementById('loginPassword').value;
|
const password = document.getElementById('loginPassword').value;
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch('/login', {
|
const response = await fetch('/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -57,10 +57,15 @@ document.getElementById('loginForm')?.addEventListener('submit', async (e) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
localStorage.setItem('imie', data.imie);
|
localStorage.setItem('imie', data.imie);
|
||||||
|
updateNavVisibility();
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'Logowanie nieudane');
|
alert(data.message || 'Logowanie nieudane');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Błąd logowania:', error);
|
||||||
|
alert('Wystąpił błąd podczas logowania');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('registerForm')?.addEventListener('submit', async (e) => {
|
document.getElementById('registerForm')?.addEventListener('submit', async (e) => {
|
||||||
|
@ -75,6 +80,7 @@ document.getElementById('registerForm')?.addEventListener('submit', async (e) =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch('/rejestracja', {
|
const response = await fetch('/rejestracja', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -90,11 +96,15 @@ document.getElementById('registerForm')?.addEventListener('submit', async (e) =>
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
alert(data || 'Rejestracja nieudana');
|
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 {
|
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}`);
|
if (!response.ok) throw new Error(`Błąd HTTP: ${response.status}`);
|
||||||
const books = await response.json();
|
const books = await response.json();
|
||||||
renderBooks(books);
|
renderBooks(books);
|
||||||
|
@ -104,32 +114,19 @@ 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) {
|
function renderBooks(books) {
|
||||||
const container = document.getElementById('books-container');
|
const container = document.getElementById('books-container');
|
||||||
if (!container) return;
|
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 => `
|
container.innerHTML = books.map(book => `
|
||||||
<div class="col-md-4 mb-4">
|
<div class="col-md-4 mb-4">
|
||||||
<div class="book-card card h-100">
|
<div class="book-card card h-100">
|
||||||
|
@ -144,9 +141,54 @@ function renderBooks(books) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).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) {
|
function showError(message) {
|
||||||
|
@ -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>
|
</div>
|
||||||
<button type="submit" class="btn btn-gothic w-100">ZAREJESTRUJ SIĘ</button>
|
<button type="submit" class="btn btn-gothic w-100">ZAREJESTRUJ SIĘ</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="login-link">
|
||||||
|
Masz już konto? <a href="/login.html" class="text-accent">Zaloguj się</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-5 py-3 bg-dark">
|
<footer class="mt-5 py-3 bg-dark">
|
||||||
|
|
Loading…
Add table
Reference in a new issue