Compare commits

...

10 commits

Author SHA1 Message Date
07cda1b5ad beta #3 2025-06-01 19:01:12 +02:00
26c7ac9341 cleanup # 2 2025-06-01 18:36:49 +02:00
d5c2c1a78d cleanup 2025-06-01 18:29:47 +02:00
6492b4c171 Beta #2 2025-06-01 18:27:15 +02:00
5e0a3cfabe Beta 2025-06-01 17:41:00 +02:00
e19f942547 polish #6 2025-06-01 11:09:37 +02:00
36c36c8afb cookies 2025-05-30 20:02:17 +02:00
3957d621a4 refactor html 2025-05-30 19:04:31 +02:00
1adcd8617a refactoring 2025-05-30 18:00:52 +02:00
5aee4f61bb delete in cart 2025-05-30 15:40:23 +02:00
28 changed files with 2200 additions and 1433 deletions

2
Cargo.lock generated
View file

@ -1457,7 +1457,7 @@ dependencies = [
[[package]]
name = "ksiegarnia"
version = "0.1.0"
version = "0.9.0"
dependencies = [
"actix-cors",
"actix-files",

View file

@ -1,6 +1,6 @@
[package]
name = "ksiegarnia"
version = "0.1.0"
version = "0.9.0"
edition = "2021"
[dependencies]

View file

@ -0,0 +1 @@
ALTER TABLE zamowienia ADD COLUMN typ_dostawy VARCHAR(20) NOT NULL DEFAULT 'shipping';

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

Binary file not shown.

View file

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

@ -0,0 +1,67 @@
use actix_web::{get, web, HttpResponse, Responder};
use crate::models::Book;
use sqlx::PgPool;
use std::collections::HashMap;
#[get("/api/ksiazki")]
pub async fn get_books(
pool: web::Data<PgPool>,
query: web::Query<HashMap<String, String>>,
) -> impl Responder {
let search_term = query.get("search").map(|s| s.as_str()).unwrap_or("");
let sort_by = query.get("sort").map(|s| s.as_str()).unwrap_or("default");
let 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
View 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
View file

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

View file

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

View file

@ -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">&copy; 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>

View file

@ -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">&copy; 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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">&copy; 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>

View file

@ -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">&copy; 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>

View file

@ -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">&copy; 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>

View file

@ -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">&copy; 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>