Compare commits
10 commits
a13a13b296
...
07cda1b5ad
Author | SHA1 | Date | |
---|---|---|---|
07cda1b5ad | |||
26c7ac9341 | |||
d5c2c1a78d | |||
6492b4c171 | |||
5e0a3cfabe | |||
e19f942547 | |||
36c36c8afb | |||
3957d621a4 | |||
1adcd8617a | |||
5aee4f61bb |
28 changed files with 2200 additions and 1433 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1457,7 +1457,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ksiegarnia"
|
||||
version = "0.1.0"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-files",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ksiegarnia"
|
||||
version = "0.1.0"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
1
migrations/20250531163129_dostawa.sql
Normal file
1
migrations/20250531163129_dostawa.sql
Normal file
|
@ -0,0 +1 @@
|
|||
ALTER TABLE zamowienia ADD COLUMN typ_dostawy VARCHAR(20) NOT NULL DEFAULT 'shipping';
|
3
migrations/20250601094238_zmień_dostawe.sql
Normal file
3
migrations/20250601094238_zmień_dostawe.sql
Normal file
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE zamowienia ALTER COLUMN typ_dostawy SET DEFAULT 'shipping';
|
||||
|
||||
UPDATE zamowienia SET typ_dostawy = 'shipping' WHERE typ_dostawy IS NULL;
|
BIN
src/.main.rs.swp
BIN
src/.main.rs.swp
Binary file not shown.
176
src/auth.rs
176
src/auth.rs
|
@ -1,39 +1,26 @@
|
|||
use actix_web::{post, web, HttpResponse, Responder};
|
||||
use actix_web::{get, post, web, HttpResponse, Responder, HttpRequest, cookie::{Cookie, SameSite}};
|
||||
use serde_json::json;
|
||||
use crate::models::{RegistrationData, LoginData, LoginResponse};
|
||||
use crate::error::AppError;
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegistrationData {
|
||||
pub email: String,
|
||||
pub haslo: String,
|
||||
pub imie: String,
|
||||
#[serde(rename = "confirmPassword")]
|
||||
pub confirm_password: String,
|
||||
}
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[post("/rejestracja")]
|
||||
pub async fn rejestracja(
|
||||
pub async fn register(
|
||||
form: web::Json<RegistrationData>,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> impl Responder {
|
||||
// Walidacja hasła
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
if form.haslo.len() < 8 {
|
||||
return HttpResponse::BadRequest().body("Hasło musi mieć minimum 8 znaków");
|
||||
return Err(AppError::BadRequest("Hasło musi mieć minimum 8 znaków".to_string()));
|
||||
}
|
||||
|
||||
// Sprawdzenie, czy hasła się zgadzają
|
||||
if form.haslo != form.confirm_password {
|
||||
return HttpResponse::BadRequest().body("Hasła nie są identyczne");
|
||||
return Err(AppError::BadRequest("Hasła nie są identyczne".to_string()));
|
||||
}
|
||||
|
||||
// Hashowanie hasła
|
||||
let hashed_password = match hash(&form.haslo, DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
let hashed_password = hash(&form.haslo, DEFAULT_COST).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
|
||||
|
||||
// Zapisz do bazy danych
|
||||
match sqlx::query!(
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO uzytkownicy (email, haslo, imie)
|
||||
VALUES ($1, $2, $3)
|
||||
|
@ -44,57 +31,122 @@ pub async fn rejestracja(
|
|||
)
|
||||
.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()
|
||||
}
|
||||
.map_err(|e| {
|
||||
if e.to_string().contains("duplicate key value") {
|
||||
AppError::BadRequest("Email jest już zarejestrowany".to_string())
|
||||
} else {
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginData {
|
||||
pub email: String,
|
||||
pub haslo: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
pub imie: String,
|
||||
Ok(HttpResponse::Created().body("Konto utworzone pomyślnie"))
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
pub async fn login(
|
||||
form: web::Json<LoginData>,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> impl Responder {
|
||||
let user = match sqlx::query!(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user = 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(),
|
||||
};
|
||||
.map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?
|
||||
.ok_or_else(|| AppError::Unauthorized("Nieprawidłowe dane".to_string()))?;
|
||||
|
||||
match 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,
|
||||
if verify(&form.haslo, &user.haslo).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))? {
|
||||
let token = format!("user-{}-token", user.id);
|
||||
|
||||
let cookie = Cookie::build("auth_token", &token)
|
||||
.path("/")
|
||||
.max_age(actix_web::cookie::time::Duration::days(7))
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Lax)
|
||||
.finish();
|
||||
|
||||
let mut response = HttpResponse::Ok()
|
||||
.json(LoginResponse {
|
||||
token: token.clone(),
|
||||
imie: user.imie,
|
||||
})
|
||||
},
|
||||
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
|
||||
});
|
||||
|
||||
response.add_cookie(&cookie)
|
||||
.map_err(|e| {
|
||||
log::error!("Błąd ustawiania ciasteczka: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(AppError::Unauthorized("Nieprawidłowe hasło".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
pub async fn logout() -> impl Responder {
|
||||
let cookie = Cookie::build("auth_token", "")
|
||||
.path("/")
|
||||
.max_age(actix_web::cookie::time::Duration::seconds(0))
|
||||
.http_only(true)
|
||||
.finish();
|
||||
|
||||
let mut response = HttpResponse::Ok().json(json!({"status": "success"}));
|
||||
|
||||
if let Err(e) = response.add_cookie(&cookie) {
|
||||
log::error!("Błąd usuwania ciasteczka: {}", e);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub async fn validate_token(req: &HttpRequest) -> Result<i32, AppError> {
|
||||
let cookie = req.cookie("auth_token")
|
||||
.ok_or_else(|| AppError::Unauthorized("Unauthorized".to_string()))?;
|
||||
|
||||
let token = cookie.value();
|
||||
|
||||
if token.starts_with("user-") {
|
||||
let user_id = token.replace("user-", "").replace("-token", "").parse()
|
||||
.map_err(|_| AppError::Unauthorized("Invalid token".to_string()))?;
|
||||
Ok(user_id)
|
||||
} else {
|
||||
Err(AppError::Unauthorized("Unauthorized".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/check-auth")]
|
||||
pub async fn check_auth(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> impl Responder {
|
||||
let user_id = validate_token(&req).await;
|
||||
|
||||
match user_id {
|
||||
Ok(user_id) => {
|
||||
match sqlx::query!(
|
||||
"SELECT imie FROM uzytkownicy WHERE id = $1",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(pool.get_ref())
|
||||
.await {
|
||||
Ok(user) => HttpResponse::Ok().json(json!({
|
||||
"authenticated": true,
|
||||
"user": {
|
||||
"id": user_id,
|
||||
"imie": user.imie
|
||||
}
|
||||
})),
|
||||
Err(_) => HttpResponse::Ok().json(json!({
|
||||
"authenticated": false,
|
||||
"user": null
|
||||
}))
|
||||
}
|
||||
},
|
||||
Err(_) => HttpResponse::Ok().json(json!({
|
||||
"authenticated": false,
|
||||
"user": null
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
67
src/books.rs
Normal file
67
src/books.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use actix_web::{get, web, HttpResponse, Responder};
|
||||
use crate::models::Book;
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[get("/api/ksiazki")]
|
||||
pub async fn get_books(
|
||||
pool: web::Data<PgPool>,
|
||||
query: web::Query<HashMap<String, String>>,
|
||||
) -> impl Responder {
|
||||
let search_term = query.get("search").map(|s| s.as_str()).unwrap_or("");
|
||||
let sort_by = query.get("sort").map(|s| s.as_str()).unwrap_or("default");
|
||||
|
||||
let base_query = "SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, COALESCE(opis, 'Brak opisu') as opis FROM ksiazki".to_string();
|
||||
|
||||
let where_clause = if !search_term.is_empty() {
|
||||
" WHERE LOWER(tytul) LIKE LOWER($1) OR LOWER(autor) LIKE LOWER($1)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let order_clause = match sort_by {
|
||||
"price_asc" => " ORDER BY cena ASC",
|
||||
"price_desc" => " ORDER BY cena DESC",
|
||||
"title_asc" => " ORDER BY tytul ASC",
|
||||
"author_asc" => " ORDER BY autor ASC",
|
||||
_ => " ORDER BY tytul ASC",
|
||||
};
|
||||
|
||||
let query_str = format!("{}{}{}", base_query, where_clause, order_clause);
|
||||
|
||||
let mut query_builder = sqlx::query_as::<_, Book>(&query_str);
|
||||
|
||||
if !search_term.is_empty() {
|
||||
query_builder = query_builder.bind(format!("%{}%", search_term));
|
||||
}
|
||||
|
||||
match query_builder.fetch_all(pool.get_ref()).await {
|
||||
Ok(books) => HttpResponse::Ok().json(books),
|
||||
Err(_) => HttpResponse::InternalServerError().body("Błąd serwera"),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/ksiazki/{id}")]
|
||||
pub async fn get_book(
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<i32>,
|
||||
) -> impl Responder {
|
||||
let id = path.into_inner();
|
||||
|
||||
match sqlx::query_as!(
|
||||
Book,
|
||||
r#"
|
||||
SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, COALESCE(opis, 'Brak opisu') as opis
|
||||
FROM ksiazki
|
||||
WHERE id = $1
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(pool.get_ref())
|
||||
.await {
|
||||
Ok(Some(book)) => HttpResponse::Ok().json(book),
|
||||
Ok(None) => HttpResponse::NotFound().body("Książka nie znaleziona"),
|
||||
Err(_) => HttpResponse::InternalServerError().body("Błąd serwera"),
|
||||
}
|
||||
}
|
||||
|
199
src/cart.rs
Normal file
199
src/cart.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
use actix_web::{get, post, delete, web, HttpResponse, HttpRequest};
|
||||
use crate::models::{CartItem, CartItemResponse, CheckoutRequest};
|
||||
use crate::error::AppError;
|
||||
use sqlx::PgPool;
|
||||
use crate::auth::validate_token;
|
||||
use bigdecimal::BigDecimal;
|
||||
use serde_json::json;
|
||||
use log;
|
||||
use std::str::FromStr;
|
||||
use crate::models::CartQuantityUpdate;
|
||||
|
||||
#[get("/api/cart")]
|
||||
pub async fn get_cart(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = validate_token(&req).await?;
|
||||
|
||||
let cart_items = sqlx::query_as!(
|
||||
CartItemResponse,
|
||||
r#"
|
||||
SELECT
|
||||
k.book_id as "book_id!",
|
||||
k.quantity as "quantity!",
|
||||
b.tytul as "tytul!",
|
||||
b.cena as "cena!",
|
||||
COALESCE('/images/' || NULLIF(b.obraz_url, ''), '/images/placeholder.jpg') as "obraz_url!"
|
||||
FROM koszyk k
|
||||
JOIN ksiazki b ON k.book_id = b.id
|
||||
WHERE k.user_id = $1
|
||||
ORDER BY b.tytul"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_all(pool.get_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Błąd bazy danych: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(cart_items))
|
||||
}
|
||||
|
||||
#[post("/api/add-to-cart")]
|
||||
pub async fn add_to_cart(
|
||||
cart_item: web::Json<CartItem>,
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = validate_token(&req).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| {
|
||||
log::error!("Błąd bazy danych: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
|
||||
}
|
||||
|
||||
#[delete("/api/remove-from-cart/{book_id}")]
|
||||
pub async fn remove_from_cart(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
book_id: web::Path<i32>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = validate_token(&req).await?;
|
||||
let book_id = book_id.into_inner();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM koszyk WHERE user_id = $1 AND book_id = $2",
|
||||
user_id,
|
||||
book_id
|
||||
)
|
||||
.execute(pool.get_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Błąd bazy danych: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
|
||||
}
|
||||
|
||||
#[post("/api/checkout")]
|
||||
pub async fn checkout(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
data: web::Json<CheckoutRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = validate_token(&req).await?;
|
||||
|
||||
let mut transaction = pool.begin().await
|
||||
.map_err(|e| {
|
||||
log::error!("Błąd rozpoczynania transakcji: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
let total_str = format!("{:.2}", data.total);
|
||||
let total_bigdecimal = BigDecimal::from_str(&total_str)
|
||||
.map_err(|_| AppError::BadRequest("Invalid total value".to_string()))?;
|
||||
|
||||
let order_record = sqlx::query!(
|
||||
"INSERT INTO zamowienia (user_id, suma_totalna, typ_dostawy)
|
||||
VALUES ($1, $2, $3) RETURNING id",
|
||||
user_id,
|
||||
total_bigdecimal,
|
||||
data.delivery_type
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Błąd tworzenia zamówienia: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
let order_id = order_record.id;
|
||||
|
||||
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| {
|
||||
log::error!("Błąd wyszukiwania książki: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
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| {
|
||||
log::error!("Błąd dodawania pozycji zamówienia: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM koszyk WHERE user_id = $1",
|
||||
user_id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Błąd czyszczenia koszyka: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
transaction.commit().await.map_err(|e| {
|
||||
log::error!("Błąd zatwierdzania transakcji: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
|
||||
}
|
||||
|
||||
#[post("/api/update-cart-quantity")]
|
||||
pub async fn update_cart_quantity(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
data: web::Json<CartQuantityUpdate>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = validate_token(&req).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE koszyk SET quantity = quantity + $1
|
||||
WHERE user_id = $2 AND book_id = $3",
|
||||
data.change,
|
||||
user_id,
|
||||
data.book_id
|
||||
)
|
||||
.execute(pool.get_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Błąd bazy danych: {}", e);
|
||||
AppError::InternalServerError("Błąd serwera".to_string())
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
|
||||
}
|
30
src/error.rs
Normal file
30
src/error.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use actix_web::{HttpResponse, ResponseError};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppError {
|
||||
BadRequest(String),
|
||||
Unauthorized(String),
|
||||
InternalServerError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
AppError::BadRequest(msg) => write!(f, "BadRequest: {}", msg),
|
||||
AppError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
|
||||
AppError::InternalServerError(msg) => write!(f, "InternalServerError: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for AppError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self {
|
||||
AppError::BadRequest(msg) => HttpResponse::BadRequest().body(msg.clone()),
|
||||
AppError::Unauthorized(msg) => HttpResponse::Unauthorized().body(msg.clone()),
|
||||
AppError::InternalServerError(msg) => HttpResponse::InternalServerError().body(msg.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
526
src/main.rs
526
src/main.rs
|
@ -1,496 +1,24 @@
|
|||
use actix_web::{Error, post, get, web, App, HttpResponse, HttpServer, Responder, HttpRequest};
|
||||
mod models;
|
||||
mod auth;
|
||||
mod books;
|
||||
mod cart;
|
||||
mod profile;
|
||||
mod error;
|
||||
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use actix_cors::Cors;
|
||||
use actix_files::Files;
|
||||
use dotenv::dotenv;
|
||||
use env_logger::{Builder, Env};
|
||||
use env_logger::Builder;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use bigdecimal::BigDecimal;
|
||||
use chrono::{TimeZone, DateTime, Utc, NaiveDateTime};
|
||||
use sqlx::FromRow;
|
||||
use actix_web::http::header;
|
||||
use sqlx::Row;
|
||||
use bigdecimal::FromPrimitive;
|
||||
use std::convert::Infallible;
|
||||
|
||||
mod auth;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RegistrationData {
|
||||
email: String,
|
||||
haslo: String,
|
||||
imie: String,
|
||||
#[serde(rename = "confirmPassword")]
|
||||
confirm_password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginData {
|
||||
email: String,
|
||||
haslo: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LoginResponse {
|
||||
token: String,
|
||||
imie: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||
struct Book {
|
||||
id: i32,
|
||||
tytul: String,
|
||||
autor: String,
|
||||
cena: BigDecimal,
|
||||
obraz_url: Option<String>,
|
||||
opis: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CartItem {
|
||||
book_id: i32,
|
||||
quantity: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OrderWithItems {
|
||||
id: i32,
|
||||
data_zamowienia: NaiveDateTime,
|
||||
suma_totalna: BigDecimal,
|
||||
status: Option<String>,
|
||||
items: Vec<OrderItem>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Serialize)]
|
||||
struct OrderItem {
|
||||
tytul: String,
|
||||
autor: String,
|
||||
ilosc: i32,
|
||||
cena: BigDecimal,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CheckoutRequest {
|
||||
items: Vec<CartItem>,
|
||||
total: f64,
|
||||
}
|
||||
|
||||
async fn validate_token(token: Option<&str>) -> Result<i32, actix_web::Error> {
|
||||
let raw_token = token.ok_or(actix_web::error::ErrorUnauthorized("Unauthorized"))?;
|
||||
|
||||
// Usuń prefiks "Bearer "
|
||||
let token = raw_token.trim_start_matches("Bearer ").trim();
|
||||
|
||||
if token.starts_with("user-") {
|
||||
let user_id = token.replace("user-", "").replace("-token", "").parse()
|
||||
.map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?;
|
||||
Ok(user_id)
|
||||
} else {
|
||||
Err(actix_web::error::ErrorUnauthorized("Unauthorized"))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/rejestracja")]
|
||||
async fn rejestracja(
|
||||
form: web::Json<RegistrationData>,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> impl Responder {
|
||||
// Walidacja hasła
|
||||
if form.haslo.len() < 8 {
|
||||
return HttpResponse::BadRequest().body("Hasło musi mieć minimum 8 znaków");
|
||||
}
|
||||
|
||||
// Sprawdzenie, czy hasła się zgadzają
|
||||
if form.haslo != form.confirm_password {
|
||||
return HttpResponse::BadRequest().body("Hasła nie są identyczne");
|
||||
}
|
||||
|
||||
// Hashowanie hasła
|
||||
let hashed_password = match bcrypt::hash(&form.haslo, bcrypt::DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
// Zapisz do bazy danych
|
||||
match sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO uzytkownicy (email, haslo, imie)
|
||||
VALUES ($1, $2, $3)
|
||||
"#,
|
||||
form.email,
|
||||
hashed_password,
|
||||
form.imie
|
||||
)
|
||||
.execute(pool.get_ref())
|
||||
.await
|
||||
{
|
||||
Ok(_) => HttpResponse::Created().body("Konto utworzone pomyślnie"),
|
||||
Err(e) => {
|
||||
if e.to_string().contains("duplicate key value") {
|
||||
HttpResponse::Conflict().body("Email jest już zarejestrowany")
|
||||
} else {
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
async fn login(
|
||||
form: web::Json<LoginData>,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> impl Responder {
|
||||
let user = match sqlx::query!(
|
||||
"SELECT id, haslo, imie FROM uzytkownicy WHERE email = $1",
|
||||
form.email
|
||||
)
|
||||
.fetch_optional(pool.get_ref())
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return HttpResponse::Unauthorized().body("Nieprawidłowe dane"),
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
match bcrypt::verify(&form.haslo, &user.haslo) {
|
||||
Ok(true) => {
|
||||
// W praktyce użyj JWT lub innego mechanizmu autentykacji
|
||||
let dummy_token = format!("user-{}-token", user.id);
|
||||
HttpResponse::Ok().json(LoginResponse {
|
||||
token: dummy_token,
|
||||
imie: user.imie,
|
||||
})
|
||||
},
|
||||
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/ksiazki")]
|
||||
async fn get_ksiazki(
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
web::Query(params): web::Query<HashMap<String, String>>,
|
||||
) -> impl Responder {
|
||||
let search_term = params.get("search").map(|s| s.as_str()).unwrap_or("");
|
||||
let sort_by = params.get("sort").map(|s| s.as_str()).unwrap_or("default");
|
||||
|
||||
// Poprawione zapytanie bazowe
|
||||
let mut base_query = "SELECT
|
||||
id,
|
||||
tytul,
|
||||
autor,
|
||||
cena,
|
||||
COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url,
|
||||
COALESCE(opis, 'Brak opisu') as opis
|
||||
FROM ksiazki".to_string();
|
||||
|
||||
// Warunek WHERE
|
||||
let where_clause = if !search_term.is_empty() {
|
||||
" WHERE LOWER(tytul) LIKE LOWER($1) OR LOWER(autor) LIKE LOWER($1)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
// Poprawna kolejność klauzul
|
||||
let order_clause = match sort_by {
|
||||
"price_asc" => " ORDER BY cena ASC",
|
||||
"price_desc" => " ORDER BY cena DESC",
|
||||
"title_asc" => " ORDER BY tytul ASC",
|
||||
"author_asc" => " ORDER BY autor ASC",
|
||||
_ => " ORDER BY tytul ASC" // Domyślne sortowanie
|
||||
};
|
||||
|
||||
// Łączymy części zapytania w odpowiedniej kolejności
|
||||
let query = format!("{}{}{}", base_query, where_clause, order_clause);
|
||||
|
||||
let mut query_builder = sqlx::query_as::<_, Book>(&query);
|
||||
|
||||
if !search_term.is_empty() {
|
||||
query_builder = query_builder.bind(format!("%{}%", search_term));
|
||||
}
|
||||
|
||||
match query_builder.fetch_all(pool.get_ref()).await {
|
||||
Ok(books) => HttpResponse::Ok().json(books),
|
||||
Err(e) => {
|
||||
log::error!("Błąd bazy danych: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(json!({"error": "Błąd serwera"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/ksiazki/{id}")]
|
||||
async fn get_ksiazka(
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
path: web::Path<i32>,
|
||||
) -> impl Responder {
|
||||
let id = path.into_inner();
|
||||
|
||||
match sqlx::query_as!(
|
||||
Book,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
tytul,
|
||||
autor,
|
||||
cena,
|
||||
COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url,
|
||||
COALESCE(opis, 'Brak opisu') as opis
|
||||
FROM ksiazki
|
||||
WHERE id = $1
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(pool.get_ref())
|
||||
.await
|
||||
{
|
||||
Ok(Some(book)) => HttpResponse::Ok().json(book),
|
||||
Ok(None) => HttpResponse::NotFound().json(json!({"error": "Książka nie znaleziona"})),
|
||||
Err(e) => {
|
||||
log::error!("Błąd bazy danych: {}", e);
|
||||
HttpResponse::InternalServerError().json(json!({"error": "Błąd serwera"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UserInfo {
|
||||
id: i32,
|
||||
imie: String,
|
||||
}
|
||||
|
||||
#[get("/api/check-auth")]
|
||||
async fn check_auth(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<sqlx::PgPool>, // Dodajemy pool jako parametr
|
||||
) -> impl Responder {
|
||||
let token = req.headers().get("Authorization")
|
||||
.and_then(|h| h.to_str().ok());
|
||||
|
||||
match validate_token(token).await {
|
||||
Ok(user_id) => {
|
||||
match sqlx::query!(
|
||||
"SELECT imie FROM uzytkownicy WHERE id = $1",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(pool.get_ref()) // Używamy pool z parametru
|
||||
.await {
|
||||
Ok(u) => HttpResponse::Ok().json(json!({
|
||||
"authenticated": true,
|
||||
"user": {
|
||||
"id": user_id,
|
||||
"imie": u.imie
|
||||
}
|
||||
})),
|
||||
Err(_) => HttpResponse::Ok().json(json!({
|
||||
"authenticated": false,
|
||||
"user": null
|
||||
}))
|
||||
}
|
||||
},
|
||||
Err(_) => HttpResponse::Ok().json(json!({
|
||||
"authenticated": false,
|
||||
"user": null
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct CartItemResponse {
|
||||
book_id: i32,
|
||||
quantity: i32,
|
||||
tytul: String,
|
||||
cena: BigDecimal,
|
||||
#[serde(rename = "obraz_url")]
|
||||
obraz_url: String, // Zmiana z Option<String> na String
|
||||
}
|
||||
|
||||
#[get("/api/cart")]
|
||||
async fn get_cart(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user_id = validate_token(get_token(&req)).await?;
|
||||
|
||||
let cart_items = sqlx::query_as!(
|
||||
CartItemResponse,
|
||||
r#"SELECT
|
||||
k.book_id as "book_id!",
|
||||
k.quantity as "quantity!",
|
||||
b.tytul as "tytul!",
|
||||
b.cena as "cena!",
|
||||
COALESCE('/images/' || NULLIF(b.obraz_url, ''), '/images/placeholder.jpg') as "obraz_url!"
|
||||
FROM koszyk k
|
||||
JOIN ksiazki b ON k.book_id = b.id
|
||||
WHERE k.user_id = $1
|
||||
ORDER BY b.tytul"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_all(pool.get_ref())
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(cart_items))
|
||||
}
|
||||
|
||||
fn get_token(req: &HttpRequest) -> Option<&str> {
|
||||
req.headers()
|
||||
.get("Authorization")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
}
|
||||
|
||||
#[post("/api/add-to-cart")]
|
||||
async fn add_to_cart(
|
||||
cart_item: web::Json<CartItem>,
|
||||
req: HttpRequest,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let token = req.headers().get("Authorization")
|
||||
.and_then(|h| h.to_str().ok());
|
||||
|
||||
let user_id = validate_token(token).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO koszyk (user_id, book_id, quantity)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, book_id)
|
||||
DO UPDATE SET quantity = koszyk.quantity + $3",
|
||||
user_id,
|
||||
cart_item.book_id,
|
||||
cart_item.quantity
|
||||
)
|
||||
.execute(pool.get_ref())
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
|
||||
}
|
||||
|
||||
#[get("/api/order-history")]
|
||||
async fn get_order_history(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user_id = validate_token(get_token(&req)).await?;
|
||||
|
||||
let orders = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
z.id as "id!",
|
||||
z.data_zamowienia as "data_zamowienia!",
|
||||
z.suma_totalna as "suma_totalna!",
|
||||
z.status,
|
||||
pz.ilosc as "ilosc!",
|
||||
pz.cena as "item_price!",
|
||||
k.tytul as "tytul!",
|
||||
k.autor as "autor!"
|
||||
FROM zamowienia z
|
||||
JOIN pozycje_zamowienia pz ON z.id = pz.zamowienie_id
|
||||
JOIN ksiazki k ON pz.book_id = k.id
|
||||
WHERE z.user_id = $1
|
||||
ORDER BY z.id DESC, k.tytul ASC
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_all(pool.get_ref())
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
|
||||
let mut grouped_orders = HashMap::new();
|
||||
for record in orders {
|
||||
let entry = grouped_orders.entry(record.id).or_insert(OrderWithItems {
|
||||
id: record.id,
|
||||
data_zamowienia: record.data_zamowienia,
|
||||
suma_totalna: record.suma_totalna,
|
||||
status: record.status,
|
||||
items: Vec::new(),
|
||||
});
|
||||
|
||||
entry.items.push(OrderItem {
|
||||
tytul: record.tytul,
|
||||
autor: record.autor,
|
||||
ilosc: record.ilosc,
|
||||
cena: record.item_price,
|
||||
});
|
||||
}
|
||||
|
||||
let result: Vec<OrderWithItems> = grouped_orders.into_values().collect();
|
||||
Ok(HttpResponse::Ok().json(result))
|
||||
}
|
||||
|
||||
#[post("/api/checkout")]
|
||||
async fn checkout(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<sqlx::PgPool>,
|
||||
data: web::Json<CheckoutRequest>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let user_id = validate_token(get_token(&req)).await?;
|
||||
|
||||
let mut transaction = pool.begin().await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
|
||||
// 1. Utwórz zamówienie
|
||||
let order_id = sqlx::query!(
|
||||
"INSERT INTO zamowienia (user_id, suma_totalna)
|
||||
VALUES ($1, $2) RETURNING id",
|
||||
user_id,
|
||||
BigDecimal::from_f64(data.total).ok_or_else(||
|
||||
actix_web::error::ErrorBadRequest("Invalid total value"))?
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?
|
||||
.id;
|
||||
|
||||
// 2. Dodaj pozycje zamówienia
|
||||
for item in &data.items {
|
||||
let book = sqlx::query!(
|
||||
"SELECT cena FROM ksiazki WHERE id = $1",
|
||||
item.book_id
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO pozycje_zamowienia (zamowienie_id, book_id, ilosc, cena)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
order_id,
|
||||
item.book_id,
|
||||
item.quantity,
|
||||
book.cena
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
}
|
||||
|
||||
// 3. Wyczyść koszyk
|
||||
sqlx::query!(
|
||||
"DELETE FROM koszyk WHERE user_id = $1",
|
||||
user_id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
|
||||
transaction.commit().await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({"status": "success"})))
|
||||
}
|
||||
use std::env;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Inicjalizacja loggera
|
||||
Builder::from_env(Env::default().default_filter_or("debug")).init();
|
||||
|
||||
// Ładowanie zmiennych środowiskowych
|
||||
Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
|
||||
dotenv().ok();
|
||||
let database_url = std::env::var("DATABASE_URL")
|
||||
.expect("DATABASE_URL must be set in .env");
|
||||
|
||||
// Utwórz pulę połączeń
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set in .env");
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&database_url)
|
||||
|
@ -500,12 +28,11 @@ async fn main() -> std::io::Result<()> {
|
|||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allow_any_origin()
|
||||
.allowed_methods(vec!["GET", "POST"])
|
||||
.allowed_methods(vec!["GET", "POST", "DELETE"])
|
||||
.allowed_headers(vec![
|
||||
header::CONTENT_TYPE,
|
||||
header::AUTHORIZATION,
|
||||
header::ACCEPT,
|
||||
header::HeaderName::from_static("content-type"),
|
||||
actix_web::http::header::CONTENT_TYPE,
|
||||
actix_web::http::header::AUTHORIZATION,
|
||||
actix_web::http::header::ACCEPT,
|
||||
])
|
||||
.supports_credentials();
|
||||
|
||||
|
@ -513,15 +40,18 @@ async fn main() -> std::io::Result<()> {
|
|||
.app_data(web::Data::new(pool.clone()))
|
||||
.wrap(cors)
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.service(get_ksiazki)
|
||||
.service(get_ksiazka)
|
||||
.service(rejestracja)
|
||||
.service(login)
|
||||
.service(get_cart)
|
||||
.service(add_to_cart) // Dodaj
|
||||
.service(checkout) // Dodaj
|
||||
.service(check_auth)
|
||||
.service(get_order_history)
|
||||
.service(books::get_books)
|
||||
.service(books::get_book)
|
||||
.service(auth::register)
|
||||
.service(auth::login)
|
||||
.service(auth::logout)
|
||||
.service(auth::check_auth)
|
||||
.service(cart::get_cart)
|
||||
.service(cart::add_to_cart)
|
||||
.service(cart::remove_from_cart)
|
||||
.service(cart::checkout)
|
||||
.service(cart::update_cart_quantity)
|
||||
.service(profile::get_order_history)
|
||||
.service(
|
||||
Files::new("/images", "./static/images")
|
||||
.show_files_listing(),
|
||||
|
|
81
src/models.rs
Normal file
81
src/models.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use bigdecimal::BigDecimal;
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegistrationData {
|
||||
pub email: String,
|
||||
pub haslo: String,
|
||||
pub imie: String,
|
||||
#[serde(rename = "confirmPassword")]
|
||||
pub confirm_password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginData {
|
||||
pub email: String,
|
||||
pub haslo: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
pub imie: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Serialize)]
|
||||
pub struct Book {
|
||||
pub id: i32,
|
||||
pub tytul: String,
|
||||
pub autor: String,
|
||||
pub cena: BigDecimal,
|
||||
pub obraz_url: Option<String>,
|
||||
pub opis: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CartItem {
|
||||
pub book_id: i32,
|
||||
pub quantity: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CartItemResponse {
|
||||
pub book_id: i32,
|
||||
pub quantity: i32,
|
||||
pub tytul: String,
|
||||
pub cena: BigDecimal,
|
||||
#[serde(rename = "obraz_url")]
|
||||
pub obraz_url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, FromRow)]
|
||||
pub struct OrderItem {
|
||||
pub tytul: String,
|
||||
pub autor: String,
|
||||
pub ilosc: i32,
|
||||
pub cena: BigDecimal,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OrderWithItems {
|
||||
pub id: i32,
|
||||
pub data_zamowienia: NaiveDateTime,
|
||||
pub suma_totalna: BigDecimal,
|
||||
pub status: Option<String>,
|
||||
pub items: Vec<OrderItem>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CartQuantityUpdate {
|
||||
pub book_id: i32,
|
||||
pub change: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CheckoutRequest {
|
||||
pub items: Vec<CartItem>,
|
||||
pub total: f64,
|
||||
pub delivery_type: String,
|
||||
}
|
58
src/profile.rs
Normal file
58
src/profile.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use actix_web::{get, web, HttpResponse, HttpRequest};
|
||||
use sqlx::PgPool;
|
||||
use crate::error::AppError;
|
||||
use crate::auth::validate_token;
|
||||
use crate::models::{OrderItem, OrderWithItems};
|
||||
|
||||
#[get("/api/order-history")]
|
||||
pub async fn get_order_history(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = validate_token(&req).await?;
|
||||
|
||||
let orders = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
z.id as "id!",
|
||||
z.data_zamowienia as "data_zamowienia!",
|
||||
z.suma_totalna as "suma_totalna!",
|
||||
z.status,
|
||||
pz.ilosc as "ilosc!",
|
||||
pz.cena as "item_price!",
|
||||
k.tytul as "tytul!",
|
||||
k.autor as "autor!"
|
||||
FROM zamowienia z
|
||||
JOIN pozycje_zamowienia pz ON z.id = pz.zamowienie_id
|
||||
JOIN ksiazki k ON pz.book_id = k.id
|
||||
WHERE z.user_id = $1
|
||||
ORDER BY z.id DESC, k.tytul ASC
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_all(pool.get_ref())
|
||||
.await
|
||||
.map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
|
||||
|
||||
let mut grouped_orders: std::collections::HashMap<i32, OrderWithItems> = std::collections::HashMap::new();
|
||||
for record in orders {
|
||||
let entry = grouped_orders.entry(record.id).or_insert(OrderWithItems {
|
||||
id: record.id,
|
||||
data_zamowienia: record.data_zamowienia,
|
||||
suma_totalna: record.suma_totalna,
|
||||
status: record.status,
|
||||
items: Vec::new(),
|
||||
});
|
||||
|
||||
entry.items.push(OrderItem {
|
||||
tytul: record.tytul,
|
||||
autor: record.autor,
|
||||
ilosc: record.ilosc,
|
||||
cena: record.item_price,
|
||||
});
|
||||
}
|
||||
|
||||
let result: Vec<OrderWithItems> = grouped_orders.into_values().collect();
|
||||
Ok(HttpResponse::Ok().json(result))
|
||||
}
|
||||
|
|
@ -2,59 +2,65 @@
|
|||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Dark Athenaeum</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenæum - Szczegóły książki</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.googleapis.com/css2?family=Lobster&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.cdnfonts.com/css/old-english-text-mt" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<form class="d-flex me-lg-3 flex-grow-1" id="searchForm">
|
||||
<input class="me-2"
|
||||
type="search"
|
||||
placeholder="Szukaj..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<a class="navbar-brand mx-lg-auto order-lg-1" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<div class="d-flex align-items-center order-lg-2">
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button id="theme-toggle" class="btn">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-5">
|
||||
<div class="row" id="book-details">
|
||||
<div class="col-md-4">
|
||||
<img id="book-cover" class="img-fluid mb-4" alt="Okładka książki">
|
||||
<main class="container my-5">
|
||||
<div class="row g-4" id="book-details">
|
||||
<div class="col-lg-4 col-md-5">
|
||||
<div class="cover-container mb-4">
|
||||
<img id="book-cover" class="img-fluid rounded shadow" alt="Okładka książki">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<h1 id="book-title" class="mb-4"></h1>
|
||||
<h3 id="book-author" class="mb-3"></h3>
|
||||
<p id="book-price" class="fs-4 mb-4"></p>
|
||||
<div class="card dark-card mb-4">
|
||||
<div class="col-lg-8 col-md-7">
|
||||
<h1 id="book-title" class="mb-3 fw-bold"></h1>
|
||||
<h3 id="book-author" class="mb-4 text-muted"></h3>
|
||||
<p id="book-price" class="fs-2 mb-4 text-primary fw-bold"></p>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-3">Opis</h4>
|
||||
<p id="book-description"></p>
|
||||
<p id="book-description" class="card-text"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn">Dodaj do koszyka</button>
|
||||
|
||||
<button class="btn btn-add-to-cart">
|
||||
<i class="bi bi-cart-plus me-2"></i> Dodaj do koszyka
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
@ -72,11 +78,16 @@
|
|||
<a href="https://sykorax.eu/">Współpraca</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted">© 2023 Dark Athenæum. Wszelkie prawa zastrzeżone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<script src="/js/book.js"></script>
|
||||
<script type="module" src="/js/utils.js"></script>
|
||||
<script type="module" src="/js/auth.js"></script>
|
||||
<script type="module" src="/js/theme.js"></script>
|
||||
<script type="module" src="/js/book.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
115
static/cart.html
115
static/cart.html
|
@ -1,59 +1,85 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- static/cart.html -->
|
||||
<html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenaeum</title>
|
||||
<title>Dark Athenæum - Koszyk</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 rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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">
|
||||
<link href="https://fonts.googleapis.com/css2?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">
|
||||
<!-- Lewa strona - wyszukiwanie -->
|
||||
<form class="d-flex me-lg-3 flex-grow-1" id="searchForm">
|
||||
<input class="form-control me-2 dark-input"
|
||||
type="search"
|
||||
placeholder="Szukaj książek..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<!-- Środek - logo -->
|
||||
<a class="navbar-brand mx-lg-auto order-lg-1" href="/">DARK ATHENAEUM</a>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button id="theme-toggle" class="btn">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Prawa strona - przyciski -->
|
||||
<div class="d-flex align-items-center order-lg-2">
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="nav-link" href="/login.html">Logowanie</a>
|
||||
<a class="nav-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links">
|
||||
<a class="nav-link" href="/profile.html">Profil</a>
|
||||
<a class="nav-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="nav-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
<main class="container my-5">
|
||||
<h1 class="mb-4 fw-bold">Twój koszyk</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div id="cart-items" class="mb-4">
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-3">Podsumowanie</h4>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span>Wartość produktów:</span>
|
||||
<span class="text-primary" id="products-value">0.00 PLN</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mb-3 align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="localPickup" checked>
|
||||
<label class="form-check-label" for="localPickup">
|
||||
Dostawa (12.99 PLN)
|
||||
</label>
|
||||
</div>
|
||||
<span id="delivery-value">12.99 PLN</span>
|
||||
</div>
|
||||
|
||||
<main class="container py-5">
|
||||
<h2 class="neon-title mb-4">Twój koszyk</h2>
|
||||
<div id="cart-items" class="row g-4"></div>
|
||||
<div class="text-center mt-5">
|
||||
<button id="checkoutBtn" class="btn btn-gothic btn-lg">
|
||||
<i class="bi bi-wallet2"></i> Złóż zamówienie
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between fw-bold fs-5">
|
||||
<span>Do zapłaty:</span>
|
||||
<span class="text-primary" id="total-value">0.00 PLN</span>
|
||||
</div>
|
||||
<button id="checkoutBtn" class="btn mt-4 py-3">
|
||||
<i class="bi bi-wallet2 me-2"></i> Złóż zamówienie
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
|
@ -69,9 +95,16 @@
|
|||
<a href="https://sykorax.eu/">Współpraca</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted">© 2023 Dark Athenæum. Wszelkie prawa zastrzeżone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/cart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="module" src="/js/utils.js"></script>
|
||||
<script type="module" src="/js/auth.js"></script>
|
||||
<script type="module" src="/js/theme.js"></script>
|
||||
<script type="module" src="/js/cart.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,372 +1,448 @@
|
|||
/* static/css/styles.css */
|
||||
/* Główne zmienne kolorystyczne */
|
||||
:root {
|
||||
--green1: #193630;
|
||||
--green2: #294640;
|
||||
--green3: #395650;
|
||||
--green4: #496660;
|
||||
--green5: #597670;
|
||||
--gold1: #D3BB8A;
|
||||
--gold2: #E3CB9A;
|
||||
--gold3: #F3DBAA;
|
||||
--cyan: #60AEC5;
|
||||
--dark-primary: #1a2e1a; /* ciemnozielony */
|
||||
--light-primary: #f8f9fa; /* jasny */
|
||||
--gold: #d4af37; /* złoty */
|
||||
--dark-text: #f5f5f5;
|
||||
--light-text: #212529;
|
||||
--dark-secondary: #0d1c0d;
|
||||
--light-secondary: #e9ecef;
|
||||
--dark-card: #142814;
|
||||
--light-card: #ffffff;
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
background-color: var(--green3);
|
||||
color: var(--gold2);
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: var(--green2);
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
nav form input {
|
||||
background-color: var(--green1);
|
||||
padding-left: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
width: 40%;
|
||||
transition: width 0.4s ease-in-out;
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
nav form input:focus {
|
||||
padding-left: 13px;
|
||||
border: 3px solid var(--green1);
|
||||
width: 100%;
|
||||
body.dark-theme {
|
||||
background-color: var(--dark-primary);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
body.dark-theme .navbar {
|
||||
background-color: var(--dark-secondary);
|
||||
border-bottom: 1px solid rgba(212, 175, 55, 0.2);
|
||||
}
|
||||
|
||||
body.dark-theme .card {
|
||||
background-color: var(--dark-card);
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
body.dark-theme .form-control,
|
||||
body.dark-theme .form-select {
|
||||
background-color: var(--dark-card);
|
||||
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
body.dark-theme .form-control:focus,
|
||||
body.dark-theme .form-select:focus {
|
||||
border-color: var(--gold);
|
||||
box-shadow: 0 0 0 0.25rem rgba(212, 175, 55, 0.25);
|
||||
}
|
||||
|
||||
body.dark-theme .auth-container {
|
||||
background-color: var(--dark-card);
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
}
|
||||
|
||||
body.dark-theme footer {
|
||||
background-color: var(--dark-secondary);
|
||||
border-top: 1px solid rgba(212, 175, 55, 0.2);
|
||||
}
|
||||
|
||||
body.dark-theme .order-card {
|
||||
background-color: #142814;
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
body.dark-theme .order-card .card-header {
|
||||
background-color: #0d1c0d;
|
||||
border-bottom: 1px solid rgba(212, 175, 55, 0.2);
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
body.dark-theme .order-card .list-group-item {
|
||||
background-color: #1a2e1a;
|
||||
border: 1px solid rgba(212, 175, 55, 0.1);
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
body.dark-theme .order-card .list-group-item strong {
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
body.light-theme {
|
||||
background-color: var(--light-primary);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
body.light-theme .navbar {
|
||||
background-color: var(--light-secondary);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.light-theme .card {
|
||||
background-color: var(--light-card);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
body.light-theme .form-control,
|
||||
body.light-theme .form-select {
|
||||
background-color: var(--light-card);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
body.light-theme .form-control:focus,
|
||||
body.light-theme .form-select:focus {
|
||||
border-color: var(--gold);
|
||||
box-shadow: 0 0 0 0.25rem rgba(212, 175, 55, 0.25);
|
||||
}
|
||||
|
||||
body.light-theme .auth-container {
|
||||
background-color: var(--light-card);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.light-theme footer {
|
||||
background-color: var(--light-secondary);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 1rem 0;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-family: 'Lobster', cursive;
|
||||
font-size: 3rem;
|
||||
text-shadow: 2px 2px var(--green1);
|
||||
color: var(--gold1);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: 'Old English Text MT', serif;
|
||||
font-size: 2rem;
|
||||
color: var(--gold) !important;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.navbar-link {
|
||||
color: var(--gold1);
|
||||
color: var(--gold) !important;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
transition: opacity 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar-brand:hover, .navbar-link:hover {
|
||||
color: var(--gold3);
|
||||
.navbar-link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
footer {
|
||||
font-weight: bold;
|
||||
background-color: var(--green2);
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
font-size: 12px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--gold1);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--gold);
|
||||
color: var(--gold) !important;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 0;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: linear-gradient(145deg, #08352c 0%, #062a23 100%);
|
||||
border: 1px solid #1c4d42;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
.btn:hover {
|
||||
background-color: var(--gold);
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.card a {
|
||||
color: var(--text-gold);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-gothic {
|
||||
background-color: #1c4d42;
|
||||
color: var(--text-gold);
|
||||
border: 1px solid #2a6b5e;
|
||||
}
|
||||
|
||||
.btn-gothic:hover {
|
||||
background-color: #2a6b5e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
max-width: 500px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
border: 2px solid var(--green5);
|
||||
border-radius: 8px;
|
||||
background-color: var(--green2);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-family: 'Lobster', cursive;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: 'Lobster', cursive;
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
font-family: 'Lobster', cursive;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 2px solid var(--green1);
|
||||
}
|
||||
|
||||
.auth-container button, #book-details button {
|
||||
background-color: var(--green3);
|
||||
border: 2px solid var(--green1);
|
||||
color: var(--gold3);
|
||||
}
|
||||
|
||||
.auth-container button:hover, #book-details button:hover {
|
||||
background-color: var(--green1);
|
||||
border-color: var(--gold2);
|
||||
color: var(--gold1);
|
||||
}
|
||||
|
||||
.auth-container a {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.auth-container a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#books-container {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.book-card {
|
||||
width: 100%; /* Wykorzystaj całą dostępną szerokość */
|
||||
height: 100%;
|
||||
background: var(--green1);
|
||||
border: 2px solid var(--green1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 5/8; /* Nowoczesne podejście do proporcji */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
transition: transform 0.3s ease;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#profile {
|
||||
background: var(--green1);
|
||||
border: 1px solid var(--gold1);
|
||||
border-radius: 8px;
|
||||
color: var(--gold2);
|
||||
}
|
||||
|
||||
.order {
|
||||
background: var(--green2);
|
||||
border: 1px solid var(--gold1);
|
||||
}
|
||||
|
||||
.card {
|
||||
color: var(--gold2);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border: 0;
|
||||
color: var(--gold2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--gold2);
|
||||
}
|
||||
|
||||
.book-cover:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
.book-title {
|
||||
font-size: 1rem;
|
||||
line-height: 1.3;
|
||||
height: 3.2em; /* Na 2 linie tekstu */
|
||||
margin: 0.5rem 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding: 0 0.25rem;
|
||||
color: var(--gold1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.book-title a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
color: var(--text-gold);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--gold1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--gold3);
|
||||
}
|
||||
|
||||
.text-accent {
|
||||
color: var(--accent-blue) !important;
|
||||
}
|
||||
|
||||
#book-description {
|
||||
line-height: 1.8;
|
||||
.btn-add-to-cart {
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
background: var(--green1);
|
||||
border: 1px solid var(--gold2);
|
||||
border-radius: 4px;
|
||||
.btn-gothic {
|
||||
font-family: 'Old English Text MT', serif;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#searchInput {
|
||||
height: 50px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* W pliku styles.css */
|
||||
.navbar-nav {
|
||||
gap: 1.5rem !important; /* Odstępy między linkami */
|
||||
}
|
||||
|
||||
footer a {
|
||||
padding: 0.5rem 1rem; /* Większy obszar klikalny */
|
||||
display: inline-block; /* Lepsze wyrównanie */
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
height: 350px;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
}
|
||||
|
||||
#searchInput::placeholder {
|
||||
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;
|
||||
}
|
||||
.btn-outline-gothic {
|
||||
font-family: 'Old English Text MT', serif;
|
||||
letter-spacing: 1px;
|
||||
border: 2px solid var(--gold);
|
||||
color: var(--gold) !important;
|
||||
}
|
||||
|
||||
.btn-outline-gothic:hover {
|
||||
background-color: #0ff;
|
||||
color: #000;
|
||||
box-shadow: 0 0 15px #0ff;
|
||||
background-color: var(--gold);
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
background-color: #2a2a2a;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
.card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.order-item:last-child {
|
||||
margin-bottom: 0;
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
.card-img-top {
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Pokazywanie elementów */
|
||||
.visible {
|
||||
display: flex !important;
|
||||
.auth-container {
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
.text-gothic {
|
||||
font-family: 'Old English Text MT', serif;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.anonymous-links,
|
||||
.user-links {
|
||||
display: none;
|
||||
footer {
|
||||
padding: 2rem 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.anonymous-links.visible,
|
||||
.user-links.visible {
|
||||
display: flex !important;
|
||||
gap: 1rem;
|
||||
footer a {
|
||||
color: var(--gold);
|
||||
text-decoration: none;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.navbar-brand {
|
||||
position: static;
|
||||
transform: none;
|
||||
order: 0 !important;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
#searchForm {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsywność formularza wyszukiwania */
|
||||
@media (min-width: 992px) {
|
||||
#searchForm {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
position: absolute;
|
||||
}
|
||||
.text-primary {
|
||||
color: var(--gold) !important;
|
||||
}
|
||||
|
||||
a {
|
||||
.border-primary {
|
||||
border-color: var(--gold) !important;
|
||||
}
|
||||
|
||||
/* Styl dla książek */
|
||||
.books-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr); /* 5 kolumn */
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.book-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.book-cover-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.book-cover-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 160%; /* 5:8 ratio (5/8=0.625 -> 1/0.625=1.6) */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.book-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.book-overlay h5 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.book-overlay p {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.book-overlay .price {
|
||||
font-weight: bold;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.book-cover-link:hover .book-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.book-cover-link:hover .book-cover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.books-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.books-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.books-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.books-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.book-cover-container {
|
||||
padding-top: 140%; /* Slightly different ratio on mobile */
|
||||
}
|
||||
}
|
||||
|
||||
#cart-items .card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.decrease-btn, .increase-btn, .remove-btn {
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.decrease-btn:hover, .increase-btn:hover, .remove-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.quantity-display {
|
||||
display: inline-block;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quantity-change {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border-radius: 0;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.books-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
.row.mb-4 {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.col-md-4, .col-md-5, .col-md-3 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
animation: spin 0.75s linear infinite;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
border: 1px solid rgba(220, 53, 69, 0.2);
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: rgba(13, 202, 240, 0.1);
|
||||
border: 1px solid rgba(13, 202, 240, 0.2);
|
||||
color: #0dcaf0;
|
||||
}
|
||||
|
||||
.bi {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 6rem !important;
|
||||
padding-bottom: 6rem !important;
|
||||
}
|
||||
|
||||
.display-3 {
|
||||
font-family: 'Old English Text MT', serif;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
#order-history .card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
|
|
@ -7,56 +7,68 @@
|
|||
<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 rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<form class="d-flex me-lg-3 flex-grow-1" id="searchForm">
|
||||
<input class="me-2"
|
||||
type="search"
|
||||
placeholder="Szukaj..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<a class="navbar-brand mx-lg-auto order-lg-1" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<div class="d-flex align-items-center order-lg-2">
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i>
|
||||
</a>
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-5">
|
||||
<h2 class="text-center mb-5">POPULARNE</h2>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-xxl-10"> <!-- Adjust max width if needed -->
|
||||
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-4 justify-content-center" id="books-container">
|
||||
<!-- Dynamicznie ładowane książki -->
|
||||
<div class="col-12 text-center">
|
||||
<div class="spinner-border text-danger" role="status">
|
||||
<span class="visually-hidden">Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button id="theme-toggle" class="btn">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container my-5">
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col-md-4">
|
||||
<h1 class="display-5 fw-bold">Księgarnia dla wymagających</h1>
|
||||
<p class="lead">Odkryj unikalne dzieła literackie w mrocznej atmosferze</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<input class="form-control dark-input"
|
||||
type="search"
|
||||
placeholder="Szukaj książek..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 text-end">
|
||||
<select class="form-select dark-input" id="sortSelect">
|
||||
<option value="default">Sortuj domyślnie</option>
|
||||
<option value="price_asc">Cena: od najniższej</option>
|
||||
<option value="price_desc">Cena: od najwyższej</option>
|
||||
<option value="title_asc">Tytuł A-Z</option>
|
||||
<option value="author_asc">Autor A-Z</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="books-grid" id="books-container">
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,7 +77,7 @@
|
|||
<footer>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center text-center">
|
||||
<div class="col-auto mb-2 mb-md-0">
|
||||
<div class="col-auto mb-2 mb-md-0">
|
||||
<a href="https://sykorax.eu/">O Nas</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
|
@ -75,11 +87,16 @@
|
|||
<a href="https://sykorax.eu/">Współpraca</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted">© 2023 Dark Athenæum. Wszelkie prawa zastrzeżone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<script type="module" src="/js/utils.js"></script>
|
||||
<script type="module" src="/js/auth.js"></script>
|
||||
<script type="module" src="/js/theme.js"></script>
|
||||
<script type="module" src="/js/books.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
141
static/js/auth.js
Normal file
141
static/js/auth.js
Normal file
|
@ -0,0 +1,141 @@
|
|||
import { setTheme } from './theme.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
const logoutLink = document.getElementById('logoutLink');
|
||||
|
||||
checkAuthStatus();
|
||||
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', handleLogin);
|
||||
}
|
||||
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', handleRegister);
|
||||
}
|
||||
|
||||
if (logoutLink) {
|
||||
logoutLink.addEventListener('click', handleLogout);
|
||||
}
|
||||
});
|
||||
|
||||
async function checkAuthStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/check-auth', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const authLinks = document.querySelector('.auth-links');
|
||||
|
||||
if (authLinks) {
|
||||
const anonymousLinks = authLinks.querySelector('.anonymous-links');
|
||||
const userLinks = authLinks.querySelector('.user-links');
|
||||
|
||||
if (anonymousLinks && userLinks) {
|
||||
if (data.authenticated) {
|
||||
anonymousLinks.style.display = 'none';
|
||||
userLinks.style.display = 'flex';
|
||||
} else {
|
||||
anonymousLinks.style.display = 'flex';
|
||||
userLinks.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd sprawdzania statusu uwierzytelnienia:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, haslo: password }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem('userName', data.imie);
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Błąd logowania: ${errorData.message || 'Nieznany błąd'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd logowania:', error);
|
||||
alert('Wystąpił błąd podczas logowania');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('registerName').value;
|
||||
const email = document.getElementById('registerEmail').value;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const confirmPassword = document.getElementById('registerConfirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
alert('Hasła nie są identyczne!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/rejestracja', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
haslo: password,
|
||||
imie: name,
|
||||
confirm_password: confirmPassword
|
||||
}),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Konto zostało utworzone! Możesz się zalogować.');
|
||||
window.location.href = '/login.html';
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Błąd rejestracji: ${errorData.message || 'Nieznany błąd'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd rejestracji:', error);
|
||||
alert('Wystąpił błąd podczas rejestracji');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const response = await fetch('/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
localStorage.removeItem('userName');
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
alert('Wystąpił problem podczas wylogowywania');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd wylogowywania:', error);
|
||||
alert('Wystąpił błąd podczas wylogowywania');
|
||||
}
|
||||
}
|
|
@ -1,72 +1,88 @@
|
|||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
import { formatCurrency } from "./utils.js"
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const bookId = urlParams.get('id');
|
||||
const bookDetails = document.getElementById('book-details');
|
||||
|
||||
if (!bookId) {
|
||||
bookDetails.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<h2 class="text-danger">Nieprawidłowe ID książki</h2>
|
||||
<a href="/" class="btn mt-3">Powrót do strony głównej</a>
|
||||
</div>
|
||||
|
||||
if (bookId) {
|
||||
loadBookDetails(bookId);
|
||||
|
||||
const addToCartBtn = document.querySelector('.btn-add-to-cart');
|
||||
if (addToCartBtn) {
|
||||
addToCartBtn.addEventListener('click', () => addToCart(bookId));
|
||||
}
|
||||
} else {
|
||||
document.getElementById('book-details').innerHTML = `
|
||||
<div class="alert alert-danger">Nie znaleziono książki</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadBookDetails(bookId) {
|
||||
try {
|
||||
const response = await fetch(`/api/ksiazki/${bookId}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Błąd ładowania szczegółów książki');
|
||||
}
|
||||
|
||||
const book = await response.json();
|
||||
displayBookDetails(book);
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error);
|
||||
document.getElementById('book-details').innerHTML = `
|
||||
<div class="alert alert-danger">Wystąpił błąd podczas ładowania szczegółów książki</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function displayBookDetails(book) {
|
||||
const price = formatCurrency(book.cena);
|
||||
|
||||
document.getElementById('book-title').textContent = book.tytul;
|
||||
document.getElementById('book-author').textContent = book.autor;
|
||||
document.getElementById('book-price').textContent = price;
|
||||
document.getElementById('book-description').textContent = book.opis;
|
||||
|
||||
const bookCover = document.getElementById('book-cover');
|
||||
if (bookCover) {
|
||||
bookCover.src = book.obraz_url;
|
||||
bookCover.alt = `Okładka książki: ${book.tytul}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function addToCart(bookId) {
|
||||
const response = await fetch('/api/check-auth');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.authenticated) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/ksiazki/${bookId}`);
|
||||
if (!response.ok) {
|
||||
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').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');
|
||||
}
|
||||
const response = await fetch('/api/add-to-cart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
book_id: parseInt(bookId),
|
||||
quantity: 1
|
||||
}),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Książka została dodana do koszyka!');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Błąd: ${errorData.message || 'Nie udało się dodać do koszyka'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error);
|
||||
bookDetails.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<h2 class="text-danger">Błąd ładowania książki</h2>
|
||||
<p>${error.message}</p>
|
||||
<a href="/" class="btn mt-3">Powrót do strony głównej</a>
|
||||
</div>
|
||||
`;
|
||||
alert('Wystąpił błąd podczas dodawania do koszyka');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
145
static/js/books.js
Normal file
145
static/js/books.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { formatCurrency } from './utils.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const booksContainer = document.getElementById('books-container');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
|
||||
loadBooks();
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', debounce(loadBooks, 300));
|
||||
}
|
||||
|
||||
if (sortSelect) {
|
||||
sortSelect.addEventListener('change', loadBooks);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadBooks() {
|
||||
const booksContainer = document.getElementById('books-container');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
|
||||
if (!booksContainer) return;
|
||||
|
||||
booksContainer.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const searchTerm = searchInput ? searchInput.value : '';
|
||||
const sortBy = sortSelect ? sortSelect.value : 'default';
|
||||
|
||||
const response = await fetch(`/api/ksiazki?search=${encodeURIComponent(searchTerm)}&sort=${sortBy}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Błąd ładowania książek');
|
||||
}
|
||||
|
||||
const books = await response.json();
|
||||
displayBooks(books);
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error);
|
||||
booksContainer.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="alert alert-danger">Wystąpił błąd podczas ładowania książek</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function displayBooks(books) {
|
||||
const booksContainer = document.getElementById('books-container');
|
||||
|
||||
if (!booksContainer) return;
|
||||
|
||||
if (books.length === 0) {
|
||||
booksContainer.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<p>Brak książek spełniających kryteria wyszukiwania</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
booksContainer.innerHTML = '';
|
||||
|
||||
books.forEach(book => {
|
||||
const price = formatCurrency(book.cena);
|
||||
|
||||
const bookCard = `
|
||||
<div class="book-card">
|
||||
<a href="/book.html?id=${book.id}" class="book-cover-link">
|
||||
<div class="book-cover-container">
|
||||
<img src="${book.obraz_url}" class="book-cover" alt="${book.tytul}">
|
||||
<div class="book-overlay">
|
||||
<h5>${book.tytul}</h5>
|
||||
<p>${book.autor}</p>
|
||||
<p class="price">${price}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-primary w-100 mt-2 add-to-cart-btn" data-book-id="${book.id}">
|
||||
<i class="bi bi-cart-plus me-1"></i> Dodaj do koszyka
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
booksContainer.innerHTML += bookCard;
|
||||
});
|
||||
|
||||
document.querySelectorAll('.add-to-cart-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const bookId = this.dataset.bookId;
|
||||
addToCart(bookId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function addToCart(bookId) {
|
||||
const response = await fetch('/api/check-auth');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.authenticated) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/add-to-cart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
book_id: parseInt(bookId),
|
||||
quantity: 1
|
||||
}),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Książka została dodana do koszyka!');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Błąd: ${errorData.message || 'Nie udało się dodać do koszyka'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error);
|
||||
alert('Wystąpił błąd podczas dodawania do koszyka');
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
|
@ -1,135 +1,290 @@
|
|||
async function loadCart() {
|
||||
try {
|
||||
const response = await fetch('/api/cart', {
|
||||
headers: getAuthHeaders()
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const cartItemsContainer = document.getElementById('cart-items');
|
||||
const checkoutBtn = document.getElementById('checkoutBtn');
|
||||
const localPickupCheckbox = document.getElementById('localPickup');
|
||||
|
||||
loadCart();
|
||||
|
||||
if (localPickupCheckbox) {
|
||||
localPickupCheckbox.addEventListener('change', function() {
|
||||
const cartItems = Array.from(document.querySelectorAll('.card.mb-3')).map(card => {
|
||||
const bookId = card.querySelector('.decrease-btn').dataset.bookId;
|
||||
const quantity = parseInt(card.querySelector('.quantity-display').textContent);
|
||||
const cena = parseFloat(card.querySelector('.card-text').textContent.replace(' PLN', ''));
|
||||
const tytul = card.querySelector('.card-title').textContent;
|
||||
const obraz_url = card.querySelector('img').src;
|
||||
|
||||
return {
|
||||
book_id: parseInt(bookId),
|
||||
quantity,
|
||||
cena,
|
||||
tytul,
|
||||
obraz_url
|
||||
};
|
||||
});
|
||||
|
||||
updateSummary(cartItems);
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
if (checkoutBtn) {
|
||||
checkoutBtn.addEventListener('click', handleCheckout);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('checkoutBtn').addEventListener('click', async () => {
|
||||
async function loadCart() {
|
||||
const cartItemsContainer = document.getElementById('cart-items');
|
||||
|
||||
if (!cartItemsContainer) return;
|
||||
|
||||
cartItemsContainer.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Pobierz aktualną zawartość koszyka
|
||||
const cartResponse = await fetch('/api/cart', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
const response = await fetch('/api/cart', {
|
||||
credentials: 'include'
|
||||
});
|
||||
const cartItems = await cartResponse.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Błąd HTTP: ${response.status}`);
|
||||
}
|
||||
|
||||
const cartItems = await response.json();
|
||||
displayCartItems(cartItems);
|
||||
|
||||
const localPickupCheckbox = document.getElementById('localPickup');
|
||||
if (localPickupCheckbox) {
|
||||
localPickupCheckbox.checked = false;
|
||||
}
|
||||
|
||||
updateSummary(cartItems);
|
||||
} catch (error) {
|
||||
console.error('Błąd ładowania koszyka:', error);
|
||||
cartItemsContainer.innerHTML = `
|
||||
<div class="alert alert-danger">Wystąpił błąd podczas ładowania koszyka: ${error.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Przygotuj dane do zamówienia
|
||||
function displayCartItems(cartItems) {
|
||||
const cartItemsContainer = document.getElementById('cart-items');
|
||||
|
||||
if (!cartItemsContainer) return;
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
cartItemsContainer.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
Twój koszyk jest pusty
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
cartItemsContainer.innerHTML = '';
|
||||
|
||||
cartItems.forEach(item => {
|
||||
const itemElement = `
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-2">
|
||||
<img src="${item.obraz_url}" class="img-fluid rounded-start" alt="${item.tytul}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${item.tytul}</h5>
|
||||
<p class="card-text">${item.cena} PLN</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-center justify-content-end pe-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="btn btn-sm btn-outline-secondary decrease-btn" data-book-id="${item.book_id}">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
<span class="mx-2 quantity-display">${item.quantity}</span>
|
||||
<button class="btn btn-sm btn-outline-secondary increase-btn" data-book-id="${item.book_id}">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger ms-2 remove-btn" data-book-id="${item.book_id}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
cartItemsContainer.innerHTML += itemElement;
|
||||
});
|
||||
|
||||
document.querySelectorAll('.decrease-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => updateCartItemQuantity(btn.dataset.bookId, -1));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.increase-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => updateCartItemQuantity(btn.dataset.bookId, 1));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.remove-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => removeCartItem(btn.dataset.bookId));
|
||||
});
|
||||
}
|
||||
|
||||
async function updateCartItemQuantity(bookId, change) {
|
||||
try {
|
||||
const response = await fetch('/api/update-cart-quantity', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
book_id: parseInt(bookId),
|
||||
change: change
|
||||
}),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const quantityElement = document.querySelector(`.decrease-btn[data-book-id="${bookId}"]`).nextElementSibling;
|
||||
const currentQuantity = parseInt(quantityElement.textContent);
|
||||
const newQuantity = currentQuantity + change;
|
||||
|
||||
if (newQuantity <= 0) {
|
||||
const cartItem = quantityElement.closest('.card');
|
||||
cartItem.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
cartItem.remove();
|
||||
updateSummaryFromDOM();
|
||||
}, 300);
|
||||
|
||||
await fetch(`/api/remove-from-cart/${bookId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
} else {
|
||||
quantityElement.textContent = newQuantity;
|
||||
updateSummaryFromDOM();
|
||||
}
|
||||
} else {
|
||||
alert('Błąd aktualizacji ilości');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error);
|
||||
alert('Wystąpił błąd podczas aktualizacji ilości');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummaryFromDOM() {
|
||||
const cartItems = Array.from(document.querySelectorAll('.card.mb-3')).map(card => {
|
||||
const bookId = card.querySelector('.decrease-btn').dataset.bookId;
|
||||
const quantity = parseInt(card.querySelector('.quantity-display').textContent);
|
||||
const cena = parseFloat(card.querySelector('.card-text').textContent.replace(' PLN', ''));
|
||||
const tytul = card.querySelector('.card-title').textContent;
|
||||
const obraz_url = card.querySelector('img').src;
|
||||
|
||||
return {
|
||||
book_id: parseInt(bookId),
|
||||
quantity,
|
||||
cena,
|
||||
tytul,
|
||||
obraz_url
|
||||
};
|
||||
});
|
||||
|
||||
updateSummary(cartItems);
|
||||
}
|
||||
|
||||
function updateSummary(cartItems) {
|
||||
const localPickupCheckbox = document.getElementById('localPickup');
|
||||
const productsValueEl = document.getElementById('products-value');
|
||||
const deliveryValueEl = document.getElementById('delivery-value');
|
||||
const totalValueEl = document.getElementById('total-value');
|
||||
|
||||
if (!productsValueEl || !deliveryValueEl || !totalValueEl) return;
|
||||
|
||||
let productsValue = 0;
|
||||
if (cartItems && cartItems.length > 0) {
|
||||
productsValue = cartItems.reduce((total, item) => {
|
||||
return total + (item.cena * item.quantity);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
productsValueEl.textContent = productsValue.toFixed(2) + ' PLN';
|
||||
|
||||
const deliveryCost = localPickupCheckbox && localPickupCheckbox.checked ? 12.99 : 0;
|
||||
deliveryValueEl.textContent = deliveryCost.toFixed(2) + ' PLN';
|
||||
|
||||
const totalValue = productsValue + deliveryCost;
|
||||
totalValueEl.textContent = totalValue.toFixed(2) + ' PLN';
|
||||
}
|
||||
|
||||
async function removeCartItem(bookId) {
|
||||
try {
|
||||
const response = await fetch(`/api/remove-from-cart/${bookId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadCart();
|
||||
} else {
|
||||
alert('Błąd usuwania z koszyka');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error);
|
||||
alert('Wystąpił błąd podczas usuwania z koszyka');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCheckout() {
|
||||
const cartItemsContainer = document.getElementById('cart-items');
|
||||
const localPickupCheckbox = document.getElementById('localPickup');
|
||||
|
||||
if (!cartItemsContainer || cartItemsContainer.innerHTML.includes('Twój koszyk jest pusty')) {
|
||||
alert('Twój koszyk jest pusty!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/cart', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Błąd ładowania koszyka');
|
||||
}
|
||||
|
||||
const cartItems = await response.json();
|
||||
|
||||
const checkoutData = {
|
||||
items: cartItems.map(item => ({
|
||||
book_id: item.book_id,
|
||||
quantity: item.quantity
|
||||
})),
|
||||
total: cartItems.reduce((sum, item) =>
|
||||
sum + (parseFloat(item.cena) * item.quantity), 0)
|
||||
total: parseFloat(document.getElementById('total-value').textContent),
|
||||
delivery_type: localPickupCheckbox.checked ? "local" : "shipping"
|
||||
};
|
||||
|
||||
// Wyślij zamówienie
|
||||
const response = await fetch('/api/checkout', {
|
||||
|
||||
const checkoutResponse = await fetch('/api/checkout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(checkoutData)
|
||||
body: JSON.stringify(checkoutData),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Błąd podczas składania zamówienia');
|
||||
|
||||
window.location.href = '/thankyou.html';
|
||||
|
||||
if (checkoutResponse.ok) {
|
||||
window.location.href = '/thankyou.html';
|
||||
} else {
|
||||
const errorData = await checkoutResponse.json();
|
||||
alert(`Błąd składania zamówienia: ${errorData.message || 'Nieznany błąd'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Nie udało się złożyć zamówienia: ' + error.message);
|
||||
console.error('Błąd:', error);
|
||||
alert('Wystąpił błąd podczas składania zamówienia');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
(() => {
|
||||
// Sprawdź czy jesteśmy na stronie głównej
|
||||
const booksContainer = document.getElementById('books-container');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
|
||||
if (!booksContainer || !searchInput || !sortSelect) return;
|
||||
|
||||
// Funkcje specyficzne dla strony głównej
|
||||
const createBookCard = (book) => `
|
||||
<div class="col">
|
||||
<div class="book-card h-100">
|
||||
|
@ -43,11 +40,9 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Event listeners tylko dla strony głównej
|
||||
searchInput.addEventListener('input', loadBooks);
|
||||
sortSelect.addEventListener('change', loadBooks);
|
||||
|
||||
// Inicjalizacja
|
||||
document.addEventListener('DOMContentLoaded', loadBooks);
|
||||
})();
|
||||
|
||||
|
@ -83,7 +78,6 @@ async function updateAuthUI() {
|
|||
}
|
||||
}
|
||||
|
||||
// Obsługa wylogowania
|
||||
function setupLogout() {
|
||||
document.getElementById('logoutLink')?.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -92,7 +86,6 @@ function setupLogout() {
|
|||
});
|
||||
}
|
||||
|
||||
// Inicjalizacja na każdej stronie
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateAuthUI();
|
||||
setupLogout();
|
||||
|
@ -235,7 +228,6 @@ function renderBooks(books) {
|
|||
</div>
|
||||
`).join('');
|
||||
|
||||
// Dodaj obsługę przycisków "Dodaj do koszyka"
|
||||
document.querySelectorAll('.add-to-cart').forEach(button => {
|
||||
button.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -291,18 +283,15 @@ function showError(message) {
|
|||
`;
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
|
|
@ -1,64 +1,93 @@
|
|||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/order-history', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Błąd ładowania historii');
|
||||
|
||||
const orders = await response.json();
|
||||
const container = document.getElementById('order-history');
|
||||
container.innerHTML = '';
|
||||
|
||||
orders.forEach((order, index) => {
|
||||
const orderNumber = orders.length - index;
|
||||
|
||||
const orderDate = new Date(order.data_zamowienia).toLocaleDateString();
|
||||
const itemsList = order.items.map(item => `
|
||||
<div class="order card mb-2">
|
||||
<div class="card-body">
|
||||
<h6 class="card-subtitle mb-2">${item.tytul}</h6>
|
||||
<p class="card-text">Autor: ${item.autor}</p>
|
||||
<p class="card-text">Ilość: ${item.ilosc} × ${item.cena} PLN</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const orderHTML = `
|
||||
<div class="mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Zamówienie #${orderNumber}</h5>
|
||||
<p class="card-text">Data: ${orderDate}</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Pozycje:</h6>
|
||||
${itemsList}
|
||||
<hr class="bg-secondary">
|
||||
<div class="d-flex justify-content-between">
|
||||
<p class="fw-bold">Suma całkowita:</p>
|
||||
<p class="fw-bold">${order.suma_totalna} PLN</p>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<p>Status:</p>
|
||||
<p>${order.status || 'Przyjęto do realizacji'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', orderHTML);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Nie udało się załadować historii zamówień');
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadOrderHistory();
|
||||
});
|
||||
|
||||
async function loadOrderHistory() {
|
||||
const orderHistoryContainer = document.getElementById('order-history');
|
||||
|
||||
if (!orderHistoryContainer) return;
|
||||
|
||||
orderHistoryContainer.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/order-history', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Błąd ładowania historii zamówień');
|
||||
}
|
||||
|
||||
const orders = await response.json();
|
||||
displayOrderHistory(orders);
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error);
|
||||
orderHistoryContainer.innerHTML = `
|
||||
<div class="alert alert-danger">Wystąpił błąd podczas ładowania historii zamówień</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function displayOrderHistory(orders) {
|
||||
const orderHistoryContainer = document.getElementById('order-history');
|
||||
|
||||
if (!orderHistoryContainer) return;
|
||||
|
||||
if (orders.length === 0) {
|
||||
orderHistoryContainer.innerHTML = `
|
||||
<div class="alert alert-info">Brak historii zamówień</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
orderHistoryContainer.innerHTML = '';
|
||||
|
||||
orders.forEach(order => {
|
||||
const orderDate = new Date(order.data_zamowienia).toLocaleDateString();
|
||||
|
||||
let deliveryType = order.typ_dostawy || 'shipping';
|
||||
let deliveryText = '';
|
||||
|
||||
if (deliveryType === 'local') {
|
||||
deliveryText = 'Odbiór lokalny';
|
||||
} else if (deliveryType === 'shipping') {
|
||||
deliveryText = 'Dostawa';
|
||||
} else {
|
||||
deliveryText = 'Dostawa';
|
||||
}
|
||||
|
||||
const orderElement = `
|
||||
<div class="card mb-3 order-card">
|
||||
<div class="card-header">
|
||||
<h5>Zamówienie #${order.id} - ${orderDate}</h5>
|
||||
<p class="mb-0">Status: ${order.status || 'W realizacji'}</p>
|
||||
<p class="mb-0">Suma: ${order.suma_totalna} PLN</p>
|
||||
<p class="mb-0">Typ: ${deliveryText}</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Produkty:</h6>
|
||||
<ul class="list-group">
|
||||
${order.items.map(item => `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${item.tytul}</strong> - ${item.autor}
|
||||
</div>
|
||||
<div>
|
||||
${item.ilosc} x ${item.cena} PLN
|
||||
</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
orderHistoryContainer.innerHTML += orderElement;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
42
static/js/theme.js
Normal file
42
static/js/theme.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
export function setTheme(theme) {
|
||||
document.body.classList.remove('dark-theme', 'light-theme');
|
||||
document.body.classList.add(theme + '-theme');
|
||||
|
||||
const themeIcon = document.querySelector('#theme-toggle i');
|
||||
if (themeIcon) {
|
||||
themeIcon.className = theme === 'dark'
|
||||
? 'bi bi-moon-stars-fill'
|
||||
: 'bi bi-sun-fill';
|
||||
}
|
||||
|
||||
updateThemeColors(theme);
|
||||
}
|
||||
|
||||
function updateThemeColors(theme) {
|
||||
const primaryColor = theme === 'dark' ? '#1a2e1a' : '#f8f9fa';
|
||||
const textColor = theme === 'dark' ? '#f5f5f5' : '#212529';
|
||||
const accentColor = '#d4af37';
|
||||
|
||||
document.body.style.backgroundColor = primaryColor;
|
||||
document.body.style.color = textColor;
|
||||
|
||||
const links = document.querySelectorAll('a, .btn');
|
||||
links.forEach(link => {
|
||||
link.style.color = accentColor;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
setTheme(savedTheme);
|
||||
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const currentTheme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
});
|
||||
}
|
||||
});
|
36
static/js/utils.js
Normal file
36
static/js/utils.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
export function showMessage(type, message) {
|
||||
let messageContainer = document.getElementById('message-container');
|
||||
if (!messageContainer) {
|
||||
messageContainer = document.createElement('div');
|
||||
messageContainer.id = 'message-container';
|
||||
messageContainer.style.position = 'fixed';
|
||||
messageContainer.style.top = '20px';
|
||||
messageContainer.style.right = '20px';
|
||||
messageContainer.style.zIndex = '1000';
|
||||
document.body.appendChild(messageContainer);
|
||||
}
|
||||
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
messageElement.role = 'alert';
|
||||
messageElement.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
`;
|
||||
|
||||
messageContainer.appendChild(messageElement);
|
||||
|
||||
setTimeout(() => {
|
||||
messageElement.classList.remove('show');
|
||||
setTimeout(() => messageElement.remove(), 150);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
export function formatCurrency(amount) {
|
||||
return parseFloat(amount).toFixed(2) + ' PLN';
|
||||
}
|
||||
|
||||
export function getUrlParam(name) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get(name);
|
||||
}
|
|
@ -2,62 +2,68 @@
|
|||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Logowanie</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenæum - Logowanie</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 rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<form class="d-flex me-lg-3 flex-grow-1" id="searchForm">
|
||||
<input class="me-2"
|
||||
type="search"
|
||||
placeholder="Szukaj..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<a class="navbar-brand mx-lg-auto order-lg-1" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<div class="d-flex align-items-center order-lg-2">
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button id="theme-toggle" class="btn">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="auth-container">
|
||||
<h2 class="mb-4">LOGOWANIE</h2>
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<input type="email" class="form-control dark-input"
|
||||
placeholder="Email" required id="loginEmail">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control dark-input"
|
||||
placeholder="Hasło" required id="loginPassword">
|
||||
</div>
|
||||
<button type="submit" class="btn w-100">ZALOGUJ SIĘ</button>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
Nie masz konta? <a href="/register.html">Zarejestruj się</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="auth-container p-4">
|
||||
<h2 class="mb-4 text-center">LOGOWANIE</h2>
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<input type="email" class="form-control dark-input"
|
||||
placeholder="Email" required id="loginEmail">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control dark-input"
|
||||
placeholder="Hasło" required id="loginPassword">
|
||||
</div>
|
||||
<button type="submit" class="btn w-100 py-3">
|
||||
<i class="bi bi-box-arrow-in-right me-2"></i> ZALOGUJ SIĘ
|
||||
</button>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
Nie masz konta? <a href="/register.html" class="text-gothic">Zarejestruj się</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
|
@ -72,10 +78,15 @@
|
|||
<a href="https://sykorax.eu/">Współpraca</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted">© 2023 Dark Athenæum. Wszelkie prawa zastrzeżone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="module" src="/js/utils.js"></script>
|
||||
<script type="module" src="/js/auth.js"></script>
|
||||
<script type="module" src="/js/theme.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -3,51 +3,48 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenaeum</title>
|
||||
<title>Dark Athenæum - Profil</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 rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<form class="d-flex me-lg-3 flex-grow-1" id="searchForm">
|
||||
<input class="me-2"
|
||||
type="search"
|
||||
placeholder="Szukaj..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<a class="navbar-brand mx-lg-auto order-lg-1" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<div class="d-flex align-items-center order-lg-2">
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button id="theme-toggle" class="btn">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-5">
|
||||
<h2 class="mb-4">Twój profil</h2>
|
||||
<main class="container my-5">
|
||||
<h1 class="mb-4 fw-bold">Twój profil</h1>
|
||||
|
||||
<div id="profile" class="p-4 mb-4">
|
||||
<h3 class="mb-3">Historia zamówień</h3>
|
||||
<div id="order-history"></div>
|
||||
<div class="card p-4 mb-4">
|
||||
<h3 class="mb-3 border-bottom pb-2">Historia zamówień</h3>
|
||||
<div id="order-history" class="mt-3"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
@ -64,9 +61,16 @@
|
|||
<a href="https://sykorax.eu/">Współpraca</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted">© 2023 Dark Athenæum. Wszelkie prawa zastrzeżone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/profile.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="module" src="/js/utils.js"></script>
|
||||
<script type="module" src="/js/auth.js"></script>
|
||||
<script type="module" src="/js/theme.js"></script>
|
||||
<script type="module" src="/js/profile.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,69 +2,76 @@
|
|||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Rejestracja</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenæum - Rejestracja</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.googleapis.com/css2?family=Lobster&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.cdnfonts.com/css/old-english-text-mt" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<form class="d-flex me-lg-3 flex-grow-1" id="searchForm">
|
||||
<input class="me-2"
|
||||
type="search"
|
||||
placeholder="Szukaj..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<a class="navbar-brand mx-lg-auto order-lg-1" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<div class="d-flex align-items-center order-lg-2">
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button id="theme-toggle" class="btn">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="navbar-link" href="/login.html">Logowanie</a>
|
||||
<a class="navbar-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="auth-container">
|
||||
<h2 class="mb-4">REJESTRACJA</h2>
|
||||
<form id="registerForm">
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control dark-input"
|
||||
placeholder="Imię" required id="registerName">
|
||||
<main class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="auth-container p-4">
|
||||
<h2 class="mb-4 text-center">REJESTRACJA</h2>
|
||||
<form id="registerForm">
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control dark-input"
|
||||
placeholder="Imię" required id="registerName">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="email" class="form-control dark-input"
|
||||
placeholder="Email" required id="registerEmail">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control dark-input"
|
||||
placeholder="Hasło (min. 8 znaków)" required minlength="8" id="registerPassword">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control dark-input"
|
||||
placeholder="Powtórz hasło" required id="registerConfirmPassword">
|
||||
</div>
|
||||
<button type="submit" class="btn w-100 py-3">
|
||||
<i class="bi bi-person-plus me-2"></i> ZAREJESTRUJ SIĘ
|
||||
</button>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
Masz już konto? <a href="/login.html" class="text-gothic">Zaloguj się</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="email" class="form-control dark-input"
|
||||
placeholder="Email" required id="registerEmail">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control dark-input"
|
||||
placeholder="Hasło (min. 8 znaków)" required minlength="8" id="registerPassword">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control dark-input"
|
||||
placeholder="Powtórz hasło" required id="registerConfirmPassword">
|
||||
</div>
|
||||
<button type="submit" class="btn w-100">ZAREJESTRUJ SIĘ</button>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
Masz już konto? <a href="/login.html">Zaloguj się</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
|
@ -79,10 +86,15 @@
|
|||
<a href="https://sykorax.eu/">Współpraca</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted">© 2023 Dark Athenæum. Wszelkie prawa zastrzeżone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="module" src="/js/utils.js"></script>
|
||||
<script type="module" src="/js/auth.js"></script>
|
||||
<script type="module" src="/js/theme.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -2,50 +2,79 @@
|
|||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Podziękowanie - Dark Athenaeum</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dark Athenæum - Dziękujemy</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 rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.cdnfonts.com/css/old-english-text-mt" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?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">
|
||||
<!-- Lewa strona - wyszukiwanie -->
|
||||
<form class="d-flex me-lg-3 flex-grow-1" id="searchForm">
|
||||
<input class="form-control me-2 dark-input"
|
||||
type="search"
|
||||
placeholder="Szukaj książek..."
|
||||
aria-label="Search"
|
||||
id="searchInput">
|
||||
</form>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DARK ATHENÆUM</a>
|
||||
|
||||
<!-- Środek - logo -->
|
||||
<a class="navbar-brand mx-lg-auto order-lg-1" href="/">DARK ATHENAEUM</a>
|
||||
|
||||
<!-- Prawa strona - przyciski -->
|
||||
<div class="d-flex align-items-center order-lg-2">
|
||||
<div class="auth-links">
|
||||
<div class="anonymous-links">
|
||||
<a class="nav-link" href="/login.html">Logowanie</a>
|
||||
<a class="nav-link" href="/register.html">Rejestracja</a>
|
||||
</div>
|
||||
|
||||
<div class="user-links">
|
||||
<a class="nav-link" href="/profile.html">Profil</a>
|
||||
<a class="nav-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="nav-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button id="theme-toggle" class="btn">
|
||||
<i class="bi bi-moon-stars-fill"></i>
|
||||
</button>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="user-links">
|
||||
<a class="navbar-link" href="/profile.html">Profil</a>
|
||||
<a class="navbar-link" href="#" id="logoutLink">Wyloguj</a>
|
||||
<a class="navbar-link" href="/cart.html">
|
||||
<i class="bi bi-basket"></i> Koszyk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-5 text-center">
|
||||
<h1 class="neon-title mb-4">Dziękujemy za zakup!</h1>
|
||||
<p class="inter-font fs-5">Twoje zamówienie zostało pomyślnie zrealizowane.</p>
|
||||
<a href="/profile.html" class="btn btn-gothic">Zobacz historię zamówień</a>
|
||||
<a href="/" class="btn btn-outline-gothic ms-2">Strona główna</a>
|
||||
<main class="container my-5 text-center py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<h1 class="display-3 fw-bold mb-4">Dziękujemy za zakup!</h1>
|
||||
<p class="lead fs-4 mb-5">Twoje zamówienie zostało pomyślnie zrealizowane.</p>
|
||||
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="/profile.html" class="btn btn-lg btn-gothic">
|
||||
<i class="bi bi-clock-history me-2"></i> Historia zamówień
|
||||
</a>
|
||||
<a href="/" class="btn btn-lg btn-outline-gothic">
|
||||
<i class="bi bi-house-door me-2"></i> Strona główna
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center text-center">
|
||||
<div class="col-auto mb-2 mb-md-0">
|
||||
<a href="https://sykorax.eu/">O Nas</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="https://sykorax.eu/">Kontakt</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="https://sykorax.eu/">Współpraca</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted">© 2023 Dark Athenæum. Wszelkie prawa zastrzeżone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="module" src="/js/utils.js"></script>
|
||||
<script type="module" src="/js/auth.js"></script>
|
||||
<script type="module" src="/js/theme.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Add table
Reference in a new issue