This commit is contained in:
Lheorvine 2025-05-25 16:25:22 +02:00
parent 100bda3bab
commit b7f3a43495
15 changed files with 949 additions and 142 deletions

1
.env
View file

@ -1 +1,2 @@
DATABASE_URL="postgres://postgres:secret@localhost/ksiegarnia"
SQLX_LOG=info

109
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View 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
);

View file

@ -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"),
}
}

View file

@ -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
View 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>

View file

@ -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);
}

View file

@ -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">

View file

@ -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
View 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');
}
});

View file

@ -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
View 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
View 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>

View file

@ -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">