Finish main functionality
This commit is contained in:
@@ -29,21 +29,19 @@ bp = Blueprint("main", __name__)
|
|||||||
def save_search(user: User, search_name: str, query_params: str) -> bool:
|
def save_search(user: User, search_name: str, query_params: str) -> bool:
|
||||||
"""Save a search for a user. Mock implementation."""
|
"""Save a search for a user. Mock implementation."""
|
||||||
# Initialize saved_searches if None
|
# Initialize saved_searches if None
|
||||||
if not hasattr(user, "saved_searches") or user.saved_searches is None:
|
|
||||||
user.saved_searches = {}
|
|
||||||
|
|
||||||
user.saved_searches[search_name] = query_params
|
user.saved_searches = user.saved_searches | {search_name: query_params} # noqa: PLR6104
|
||||||
|
print(f"{user.saved_searches=}")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def delete_saved_search(user: User, search_name: str) -> bool:
|
def delete_saved_search(user: User, search_name: str) -> bool:
|
||||||
"""Delete a saved search for a user. Mock implementation."""
|
"""Delete a saved search for a user. Mock implementation."""
|
||||||
if not user or not hasattr(user, "saved_searches") or not user.saved_searches:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if search_name in user.saved_searches:
|
if search_name in user.saved_searches:
|
||||||
del user.saved_searches[search_name]
|
user.saved_searches = {
|
||||||
|
k: v for k, v in user.saved_searches.items() if k != search_name
|
||||||
|
} # needs to be a new object to trigger SQLAlchemy change detection
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -103,20 +101,12 @@ def book_detail(book_id: int) -> ResponseReturnValue:
|
|||||||
flash("Book not found", "error")
|
flash("Book not found", "error")
|
||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
# Get user-specific data
|
for reading in book.readings:
|
||||||
viewing_user = session.get("viewing_as_user")
|
print(
|
||||||
user_data = {}
|
f"Reading: {reading}, user: {reading.user.username if reading.user else 'N/A'}, dropped: {reading.dropped}, finished: {reading.finished}, end_date: {reading.end_date}"
|
||||||
|
)
|
||||||
|
|
||||||
if viewing_user:
|
return render_template("book/detail.html.j2", book=book)
|
||||||
# TODO: Get reading status, wishlist status, etc.
|
|
||||||
# This will need additional library functions
|
|
||||||
user_data = {
|
|
||||||
"is_wishlisted": False,
|
|
||||||
"current_reading": None,
|
|
||||||
"reading_history": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template("book/detail.html.j2", book=book, user_data=user_data)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/book/new", methods=["GET", "POST"])
|
@bp.route("/book/new", methods=["GET", "POST"])
|
||||||
@@ -130,14 +120,26 @@ def create_book() -> ResponseReturnValue:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Get current viewing user as owner
|
# Get current viewing user as owner
|
||||||
viewing_user = g.get("viewing_as_user")
|
viewing_user = g.get("viewing_user")
|
||||||
|
|
||||||
|
# Process textarea inputs for authors and genres
|
||||||
|
authors = [
|
||||||
|
author.strip()
|
||||||
|
for author in request.form.get("authors", "").split("\n")
|
||||||
|
if author.strip()
|
||||||
|
]
|
||||||
|
genres = [
|
||||||
|
genre.strip()
|
||||||
|
for genre in request.form.get("genres", "").split("\n")
|
||||||
|
if genre.strip()
|
||||||
|
]
|
||||||
|
|
||||||
# Create book with submitted data
|
# Create book with submitted data
|
||||||
book = library.create_book(
|
book = library.create_book(
|
||||||
title=title,
|
title=title,
|
||||||
owner_id=viewing_user.id if viewing_user else None,
|
owner_id=viewing_user.id if viewing_user else None,
|
||||||
authors=request.form.getlist("authors"),
|
authors=authors,
|
||||||
genres=request.form.getlist("genres"),
|
genres=genres,
|
||||||
isbn=request.form.get("isbn"),
|
isbn=request.form.get("isbn"),
|
||||||
publisher=request.form.get("publisher"),
|
publisher=request.form.get("publisher"),
|
||||||
edition=request.form.get("edition"),
|
edition=request.form.get("edition"),
|
||||||
@@ -167,12 +169,24 @@ def update_book(book_id: int) -> ResponseReturnValue:
|
|||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Process textarea inputs for authors and genres
|
||||||
|
authors = [
|
||||||
|
author.strip()
|
||||||
|
for author in request.form.get("authors", "").split("\n")
|
||||||
|
if author.strip()
|
||||||
|
]
|
||||||
|
genres = [
|
||||||
|
genre.strip()
|
||||||
|
for genre in request.form.get("genres", "").split("\n")
|
||||||
|
if genre.strip()
|
||||||
|
]
|
||||||
|
|
||||||
# Update book with form data
|
# Update book with form data
|
||||||
library.update_book(
|
library.update_book(
|
||||||
book_id=book_id,
|
book_id=book_id,
|
||||||
title=request.form.get("title"),
|
title=request.form.get("title"),
|
||||||
authors=request.form.getlist("authors"),
|
authors=authors,
|
||||||
genres=request.form.getlist("genres"),
|
genres=genres,
|
||||||
isbn=request.form.get("isbn"),
|
isbn=request.form.get("isbn"),
|
||||||
publisher=request.form.get("publisher"),
|
publisher=request.form.get("publisher"),
|
||||||
edition=request.form.get("edition"),
|
edition=request.form.get("edition"),
|
||||||
@@ -192,23 +206,26 @@ def update_book(book_id: int) -> ResponseReturnValue:
|
|||||||
return redirect(url_for("main.book_detail", book_id=book_id))
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/book/<int:book_id>/delete", methods=["POST"])
|
@bp.route("/book/<int:book_id>/delete", methods=["GET", "POST"])
|
||||||
def delete_book(book_id: int) -> ResponseReturnValue:
|
def delete_book(book_id: int) -> ResponseReturnValue:
|
||||||
"""Delete a book."""
|
"""Delete a book (GET shows confirmation, POST performs deletion)."""
|
||||||
book = library.get_book(book_id)
|
book = library.get_book(book_id)
|
||||||
if not book:
|
if not book:
|
||||||
flash("Book not found", "error")
|
flash("Book not found", "error")
|
||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
try:
|
if request.method == "POST":
|
||||||
title = book.title
|
# Perform the actual deletion
|
||||||
library.delete_book(book_id)
|
try:
|
||||||
flash(f"Book '{title}' deleted successfully!", "success")
|
title = book.title
|
||||||
|
library.delete_book(book_id)
|
||||||
|
flash(f"Book '{title}' deleted successfully!", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error deleting book: {e}", "error")
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
except Exception as e:
|
# Show confirmation page
|
||||||
flash(f"Error deleting book: {e}", "error")
|
return render_template("book/delete_confirm.html.j2", book=book)
|
||||||
|
|
||||||
return redirect(url_for("main.index"))
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/import", methods=["GET", "POST"])
|
@bp.route("/import", methods=["GET", "POST"])
|
||||||
@@ -222,7 +239,7 @@ def import_book() -> ResponseReturnValue:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Get current viewing user as owner
|
# Get current viewing user as owner
|
||||||
viewing_user = g.get("viewing_as_user")
|
viewing_user = g.get("viewing_user")
|
||||||
|
|
||||||
# Import book from ISBN
|
# Import book from ISBN
|
||||||
book = library.import_book_from_isbn(
|
book = library.import_book_from_isbn(
|
||||||
@@ -259,17 +276,20 @@ def set_viewing_user(username: str = "") -> ResponseReturnValue:
|
|||||||
@bp.route("/saved-search", methods=["POST"])
|
@bp.route("/saved-search", methods=["POST"])
|
||||||
def save_search_route() -> ResponseReturnValue:
|
def save_search_route() -> ResponseReturnValue:
|
||||||
"""Save a search for the current user."""
|
"""Save a search for the current user."""
|
||||||
viewing_user = session.get("viewing_as_user")
|
viewing_user = g.get("viewing_user")
|
||||||
if not viewing_user:
|
if not viewing_user:
|
||||||
flash("You must select a user to save searches", "error")
|
flash("You must select a user to save searches", "error")
|
||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
search_name = request.form.get("name", "").strip()
|
search_name = request.form.get("name", "").strip()
|
||||||
query_params = request.form.get("query_params", "")
|
query_params = request.form.get("query_params", "")
|
||||||
|
print(
|
||||||
|
f"Saving search for user {viewing_user.username}: {search_name} -> {query_params}"
|
||||||
|
)
|
||||||
|
|
||||||
if not search_name:
|
if not search_name:
|
||||||
flash("Search name is required", "error")
|
flash("Search name is required", "error")
|
||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index", q=query_params))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = save_search(viewing_user, search_name, query_params)
|
success = save_search(viewing_user, search_name, query_params)
|
||||||
@@ -280,27 +300,146 @@ def save_search_route() -> ResponseReturnValue:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Error saving search: {e}", "error")
|
flash(f"Error saving search: {e}", "error")
|
||||||
|
|
||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index", q=query_params))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/saved-search", methods=["DELETE"])
|
@bp.route("/book/<int:book_id>/reading/start", methods=["POST"])
|
||||||
def delete_saved_search_route() -> ResponseReturnValue:
|
def start_reading_route(book_id: int) -> ResponseReturnValue:
|
||||||
"""Delete a saved search."""
|
"""Start reading a book."""
|
||||||
viewing_user = session.get("viewing_as_user")
|
viewing_user = g.get("viewing_user")
|
||||||
if not viewing_user:
|
if not viewing_user:
|
||||||
return {"error": "No user selected"}, 400
|
flash("You must select a user to start reading", "error")
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
data = request.get_json()
|
|
||||||
search_name = data.get("name") if data else None
|
|
||||||
|
|
||||||
if not search_name:
|
|
||||||
return {"error": "Search name required"}, 400
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = delete_saved_search(viewing_user, search_name)
|
library.start_reading(book_id=book_id, user_id=viewing_user.id)
|
||||||
if success:
|
flash("Started reading!", "success")
|
||||||
return {"success": True}
|
|
||||||
else:
|
|
||||||
return {"error": "Search not found"}, 404
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}, 500
|
flash(f"Error starting reading: {e}", "error")
|
||||||
|
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/book/<int:book_id>/reading/finish", methods=["POST"])
|
||||||
|
def finish_reading_route(book_id: int) -> ResponseReturnValue:
|
||||||
|
"""Finish reading a book."""
|
||||||
|
viewing_user = g.get("viewing_user")
|
||||||
|
if not viewing_user:
|
||||||
|
flash("You must select a user to finish reading", "error")
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find current reading for this user and book
|
||||||
|
current_readings = library.get_current_readings(user_id=viewing_user.id)
|
||||||
|
current_reading = next(
|
||||||
|
(r for r in current_readings if r.book_id == book_id), None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not current_reading:
|
||||||
|
flash("No active reading session found", "error")
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
library.finish_reading(reading_id=current_reading.id)
|
||||||
|
flash("Finished reading!", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error finishing reading: {e}", "error")
|
||||||
|
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/book/<int:book_id>/reading/drop", methods=["POST"])
|
||||||
|
def drop_reading_route(book_id: int) -> ResponseReturnValue:
|
||||||
|
"""Drop reading a book."""
|
||||||
|
viewing_user = g.get("viewing_user")
|
||||||
|
if not viewing_user:
|
||||||
|
flash("You must select a user to drop reading", "error")
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find current reading for this user and book
|
||||||
|
current_readings = library.get_current_readings(user_id=viewing_user.id)
|
||||||
|
current_reading = next(
|
||||||
|
(r for r in current_readings if r.book_id == book_id), None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not current_reading:
|
||||||
|
flash("No active reading session found", "error")
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
library.drop_reading(reading_id=current_reading.id)
|
||||||
|
flash("Dropped reading", "info")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error dropping reading: {e}", "error")
|
||||||
|
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/book/<int:book_id>/wishlist/add", methods=["POST"])
|
||||||
|
def add_to_wishlist_route(book_id: int) -> ResponseReturnValue:
|
||||||
|
"""Add book to wishlist."""
|
||||||
|
viewing_user = g.get("viewing_user")
|
||||||
|
if not viewing_user:
|
||||||
|
flash("You must select a user to add to wishlist", "error")
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
library.add_to_wishlist(book_id=book_id, user_id=viewing_user.id)
|
||||||
|
flash("Added to wishlist!", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error adding to wishlist: {e}", "error")
|
||||||
|
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/book/<int:book_id>/wishlist/remove", methods=["POST"])
|
||||||
|
def remove_from_wishlist_route(book_id: int) -> ResponseReturnValue:
|
||||||
|
"""Remove book from wishlist."""
|
||||||
|
viewing_user = g.get("viewing_user")
|
||||||
|
if not viewing_user:
|
||||||
|
flash("You must select a user to remove from wishlist", "error")
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
removed = library.remove_from_wishlist(book_id=book_id, user_id=viewing_user.id)
|
||||||
|
if removed:
|
||||||
|
flash("Removed from wishlist", "info")
|
||||||
|
else:
|
||||||
|
flash("Book was not in wishlist", "warning")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error removing from wishlist: {e}", "error")
|
||||||
|
|
||||||
|
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/saved-search/<search_name>/delete", methods=["GET", "POST"])
|
||||||
|
def delete_saved_search_route(search_name: str) -> ResponseReturnValue:
|
||||||
|
"""Delete a saved search (GET shows confirmation, POST performs deletion)."""
|
||||||
|
viewing_user = g.get("viewing_user")
|
||||||
|
if not viewing_user:
|
||||||
|
flash("You must select a user to manage saved searches", "error")
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
|
# Check if search exists
|
||||||
|
saved_searches = viewing_user.saved_searches or {}
|
||||||
|
if search_name not in saved_searches:
|
||||||
|
flash(f"Saved search '{search_name}' not found", "error")
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
# Perform the actual deletion
|
||||||
|
try:
|
||||||
|
success = delete_saved_search(viewing_user, search_name)
|
||||||
|
if success:
|
||||||
|
flash(f"Saved search '{search_name}' deleted successfully!", "success")
|
||||||
|
else:
|
||||||
|
flash("Error deleting saved search", "error")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error deleting saved search: {e}", "error")
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
|
# Show confirmation page
|
||||||
|
return render_template(
|
||||||
|
"components/delete_search_confirm.html.j2",
|
||||||
|
search_name=search_name,
|
||||||
|
search_params=saved_searches[search_name],
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en"
|
<html lang="en"
|
||||||
x-init="$el.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')">
|
x-init="$el.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@@ -46,7 +46,8 @@
|
|||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<!-- Flash Messages -->
|
<!-- Flash Messages -->
|
||||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show"
|
||||||
|
role="alert">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,10 +81,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'components/import_modal.html.j2' %}
|
||||||
|
{% include 'components/save_search_modal.html.j2' %}
|
||||||
|
|
||||||
<!-- Bootstrap JS -->
|
<!-- Bootstrap JS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<!-- Import Modal -->
|
|
||||||
{% include 'components/import_modal.html.j2' %}
|
|
||||||
</html>
|
</html>
|
||||||
47
src/hxbooks/templates/book/delete_confirm.html.j2
Normal file
47
src/hxbooks/templates/book/delete_confirm.html.j2
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends "base.html.j2" %}
|
||||||
|
|
||||||
|
{% block title %}Delete {{ book.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<a href="/book/{{ book.id }}" class="btn btn-outline-secondary me-3">← Back</a>
|
||||||
|
<h1 class="h3 mb-0 text-danger">🗑️ Delete Book</h1>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="mb-0">⚠️ Confirm Deletion</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-3">Are you sure you want to delete this book?</p>
|
||||||
|
|
||||||
|
<div class="bg-light p-3 rounded mb-4">
|
||||||
|
<h6 class="fw-bold">{{ book.title }}</h6>
|
||||||
|
{% if book.authors %}
|
||||||
|
<p class="text-muted small mb-1">by {{ book.authors | join(', ') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if book.isbn %}
|
||||||
|
<p class="text-muted small mb-0">ISBN: {{ book.isbn }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<strong>Warning:</strong> This action cannot be undone. All reading history and data associated with
|
||||||
|
this book will also be deleted.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<form action="/book/{{ book.id }}/delete" method="POST" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-danger">🗑️ Delete Book</button>
|
||||||
|
</form>
|
||||||
|
<a href="/book/{{ book.id }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
<h1 class="h3 mb-0">{{ book.title }}</h1>
|
<h1 class="h3 mb-0">{{ book.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-outline-danger" onclick="confirmDelete({{ book.id }})">
|
<a href="/book/{{ book.id }}/delete" class="btn btn-outline-danger">
|
||||||
🗑️ Delete
|
🗑️ Delete
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<form id="book-form" action="/book/{{ book.id }}/edit" method="POST">
|
<form id="book-form" action="/book/{{ book.id }}/edit" method="POST">
|
||||||
{% include 'components/book_form.html.j2' %}
|
{% include 'components/book_form.html.j2' %}
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button type="submit" class="btn btn-primary">💾 Save Changes</button>
|
<button type="submit" class="btn btn-primary">💾 Save Changes</button>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User-Specific Data Sidebar -->
|
<!-- User-Specific Data Sidebar -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
{% if session.get('viewing_as_user') %}
|
{% if session.get('viewing_as_user') %}
|
||||||
@@ -55,29 +55,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(bookId) {
|
// Simple form change detection
|
||||||
if (confirm('Are you sure you want to delete this book? This action cannot be undone.')) {
|
let originalFormData = new FormData(document.getElementById('book-form'));
|
||||||
const form = document.createElement('form');
|
let hasChanges = false;
|
||||||
form.method = 'POST';
|
|
||||||
form.action = '/book/' + bookId + '/delete';
|
|
||||||
document.body.appendChild(form);
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple form change detection
|
document.getElementById('book-form').addEventListener('input', function () {
|
||||||
let originalFormData = new FormData(document.getElementById('book-form'));
|
hasChanges = true;
|
||||||
let hasChanges = false;
|
});
|
||||||
|
|
||||||
document.getElementById('book-form').addEventListener('input', function() {
|
document.getElementById('book-form').addEventListener('submit', function () {
|
||||||
hasChanges = true;
|
hasChanges = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', function (e) {
|
||||||
|
if (hasChanges) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeunload', function(e) {
|
|
||||||
if (hasChanges) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.returnValue = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -2,13 +2,11 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label for="title" class="form-label">Title *</label>
|
<label for="title" class="form-label">Title *</label>
|
||||||
<input type="text" class="form-control" id="title" name="title"
|
<input type="text" class="form-control" id="title" name="title" value="{{ book.title if book else '' }}" required>
|
||||||
value="{{ book.title if book else '' }}" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="isbn" class="form-label">ISBN</label>
|
<label for="isbn" class="form-label">ISBN</label>
|
||||||
<input type="text" class="form-control" id="isbn" name="isbn"
|
<input type="text" class="form-control" id="isbn" name="isbn" value="{{ book.isbn if book else '' }}">
|
||||||
value="{{ book.isbn if book else '' }}">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -16,14 +14,14 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="authors" class="form-label">Authors</label>
|
<label for="authors" class="form-label">Authors</label>
|
||||||
<textarea class="form-control" id="authors" name="authors" rows="2"
|
<textarea class="form-control" id="authors" name="authors" rows="2"
|
||||||
placeholder="One author per line">{% if book and book.authors %}{{ book.authors | join('\n') }}{% endif %}</textarea>
|
placeholder="One author per line">{% if book and book.authors %}{{ book.authors | join('\n') }}{% endif %}</textarea>
|
||||||
<div class="form-text">Enter one author per line</div>
|
<div class="form-text">Enter one author per line</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="genres" class="form-label">Genres</label>
|
<label for="genres" class="form-label">Genres</label>
|
||||||
<textarea class="form-control" id="genres" name="genres" rows="2"
|
<textarea class="form-control" id="genres" name="genres" rows="2"
|
||||||
placeholder="One genre per line">{% if book and book.genres %}{{ book.genres | join('\n') }}{% endif %}</textarea>
|
placeholder="One genre per line">{% if book and book.genres %}{{ book.genres | join('\n') }}{% endif %}</textarea>
|
||||||
<div class="form-text">Enter one genre per line</div>
|
<div class="form-text">Enter one genre per line</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,86 +30,49 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="first_published" class="form-label">Year Published</label>
|
<label for="first_published" class="form-label">Year Published</label>
|
||||||
<input type="number" class="form-control" id="first_published" name="first_published"
|
<input type="number" class="form-control" id="first_published" name="first_published"
|
||||||
value="{{ book.first_published if book and book.first_published else '' }}" min="1000" max="2030">
|
value="{{ book.first_published if book and book.first_published else '' }}" min="1000" max="2030">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="publisher" class="form-label">Publisher</label>
|
<label for="publisher" class="form-label">Publisher</label>
|
||||||
<input type="text" class="form-control" id="publisher" name="publisher"
|
<input type="text" class="form-control" id="publisher" name="publisher"
|
||||||
value="{{ book.publisher if book else '' }}">
|
value="{{ book.publisher if book else '' }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="edition" class="form-label">Edition</label>
|
<label for="edition" class="form-label">Edition</label>
|
||||||
<input type="text" class="form-control" id="edition" name="edition"
|
<input type="text" class="form-control" id="edition" name="edition" value="{{ book.edition if book else '' }}">
|
||||||
value="{{ book.edition if book else '' }}">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="description" class="form-label">Description</label>
|
<label for="description" class="form-label">Description</label>
|
||||||
<textarea class="form-control" id="description" name="description" rows="3">{{ book.description if book else '' }}</textarea>
|
<textarea class="form-control" id="description" name="description"
|
||||||
|
rows="3">{{ book.description if book else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location Information -->
|
<!-- Location Information -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="location_place" class="form-label">Location (Place)</label>
|
<label for="location_place" class="form-label">Location (Place)</label>
|
||||||
<input type="text" class="form-control" id="location_place" name="location_place"
|
<input type="text" class="form-control" id="location_place" name="location_place"
|
||||||
value="{{ book.location_place if book else '' }}"
|
value="{{ book.location_place if book else '' }}" placeholder="Home, Office, etc.">
|
||||||
placeholder="Home, Office, etc.">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="location_bookshelf" class="form-label">Bookshelf</label>
|
<label for="location_bookshelf" class="form-label">Bookshelf</label>
|
||||||
<input type="text" class="form-control" id="location_bookshelf" name="location_bookshelf"
|
<input type="text" class="form-control" id="location_bookshelf" name="location_bookshelf"
|
||||||
value="{{ book.location_bookshelf if book else '' }}"
|
value="{{ book.location_bookshelf if book else '' }}" placeholder="Living room, Bedroom, etc.">
|
||||||
placeholder="Living room, Bedroom, etc.">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="location_shelf" class="form-label">Shelf Number</label>
|
<label for="location_shelf" class="form-label">Shelf Number</label>
|
||||||
<input type="number" class="form-control" id="location_shelf" name="location_shelf"
|
<input type="number" class="form-control" id="location_shelf" name="location_shelf"
|
||||||
value="{{ book.location_shelf if book and book.location_shelf else '' }}" min="1">
|
value="{{ book.location_shelf if book and book.location_shelf else '' }}" min="1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="notes" class="form-label">Personal Notes</label>
|
<label for="notes" class="form-label">Personal Notes</label>
|
||||||
<textarea class="form-control" id="notes" name="notes" rows="3"
|
<textarea class="form-control" id="notes" name="notes" rows="3"
|
||||||
placeholder="Your personal notes about this book...">{{ book.notes if book else '' }}</textarea>
|
placeholder="Your personal notes about this book...">{{ book.notes if book else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Processing form data for authors and genres -->
|
|
||||||
<script>
|
|
||||||
// Convert newline-separated text to arrays on form submit
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const form = document.querySelector('form');
|
|
||||||
if (form) {
|
|
||||||
form.addEventListener('submit', function() {
|
|
||||||
// Split authors by newlines and create hidden inputs
|
|
||||||
const authorsText = document.getElementById('authors').value;
|
|
||||||
const authors = authorsText.split('\n').map(a => a.trim()).filter(a => a);
|
|
||||||
|
|
||||||
authors.forEach(function(author) {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'hidden';
|
|
||||||
input.name = 'authors';
|
|
||||||
input.value = author;
|
|
||||||
form.appendChild(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Split genres by newlines and create hidden inputs
|
|
||||||
const genresText = document.getElementById('genres').value;
|
|
||||||
const genres = genresText.split('\n').map(g => g.trim()).filter(g => g);
|
|
||||||
|
|
||||||
genres.forEach(function(genre) {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'hidden';
|
|
||||||
input.name = 'genres';
|
|
||||||
input.value = genre;
|
|
||||||
form.appendChild(input);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html.j2" %}
|
||||||
|
|
||||||
|
{% block title %}Delete Saved Search{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<a href="/" class="btn btn-outline-secondary me-3">← Back</a>
|
||||||
|
<h1 class="h3 mb-0 text-danger">🗑️ Delete Saved Search</h1>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0">⚠️ Confirm Deletion</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-3">Are you sure you want to delete this saved search?</p>
|
||||||
|
|
||||||
|
<div class="bg-light p-3 rounded mb-4">
|
||||||
|
<h6 class="fw-bold">🔍 {{ search_name }}</h6>
|
||||||
|
<p class="text-muted small mb-0">Search: {{ search_params }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<form action="/saved-search/{{ search_name }}/delete" method="POST" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-warning">🗑️ Delete Search</button>
|
||||||
|
</form>
|
||||||
|
<a href="/" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,43 +1,53 @@
|
|||||||
<!-- Reading Status Component - TODO: Integrate with actual library functions -->
|
<!-- Reading Status Component -->
|
||||||
|
{% if g.viewing_user %}
|
||||||
|
{% set user_readings = book.readings | selectattr('user_id', 'equalto', g.viewing_user.id) | list %}
|
||||||
|
{% set current_reading = user_readings | selectattr('end_date', 'none') | selectattr('dropped', 'false') | first %}
|
||||||
|
{% set reading_history = user_readings | selectattr('end_date') | sort(attribute='end_date', reverse=true) %}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h6 class="text-muted mb-2">📖 Reading Status</h6>
|
<h6 class="text-muted mb-2">📖 Reading Status</h6>
|
||||||
|
|
||||||
<!-- Current Reading Status -->
|
<!-- Current Reading Status -->
|
||||||
{% if user_data.get('current_reading') %}
|
{% if current_reading %}
|
||||||
<div class="alert alert-info py-2">
|
<div class="alert alert-info py-2">
|
||||||
<strong>Currently Reading</strong><br>
|
<strong>Currently Reading</strong><br>
|
||||||
<small>Started: {{ user_data.current_reading.start_date.strftime('%B %d, %Y') }}</small>
|
<small>Started: {{ current_reading.start_date.strftime('%B %d, %Y') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-success btn-sm me-2">✓ Finish Reading</button>
|
<form action="/book/{{ book.id }}/reading/finish" method="POST" class="d-inline">
|
||||||
<button class="btn btn-outline-secondary btn-sm">⏸ Drop Reading</button>
|
<button type="submit" class="btn btn-success btn-sm me-2">✓ Finish Reading</button>
|
||||||
|
</form>
|
||||||
|
<form action="/book/{{ book.id }}/reading/drop" method="POST" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-outline-secondary btn-sm">⏸ Drop Reading</button>
|
||||||
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Not currently reading -->
|
<!-- Not currently reading -->
|
||||||
{% if user_data.get('reading_history') %}
|
{% if reading_history %}
|
||||||
<p class="text-muted small mb-2">Previously read</p>
|
<p class="text-muted small mb-2">Previously read</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted small mb-2">Not read yet</p>
|
<p class="text-muted small mb-2">Not read yet</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class="btn btn-primary btn-sm">▶️ Start Reading</button>
|
<form action="/book/{{ book.id }}/reading/start" method="POST" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">▶️ Start Reading</button>
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Reading History Summary -->
|
<!-- Reading History Summary -->
|
||||||
{% if user_data.get('reading_history') %}
|
{% if reading_history %}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<small class="text-muted">Reading History:</small>
|
<small class="text-muted">Reading History:</small>
|
||||||
{% for reading in user_data.reading_history[:3] %}
|
{% for reading in reading_history[:3] %}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<small>
|
<small>
|
||||||
{% if reading.finished %}
|
{% if not reading.dropped %} ✓ Finished {% else %} ⏸ Dropped {% endif %}
|
||||||
✓ Finished {{ reading.end_date.strftime('%m/%d/%Y') }}
|
{{ reading.start_date.strftime('%m/%d/%Y') }} - {{ reading.end_date.strftime('%m/%d/%Y') }}
|
||||||
{% if reading.rating %} - ⭐{{ reading.rating }}/5{% endif %}
|
- ⭐{{ reading.rating or "-" }}/5
|
||||||
{% elif reading.dropped %}
|
|
||||||
⏸ Dropped {{ reading.end_date.strftime('%m/%d/%Y') }}
|
|
||||||
{% else %}
|
|
||||||
📖 Started {{ reading.start_date.strftime('%m/%d/%Y') }}
|
|
||||||
{% endif %}
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if reading_history | length > 3 %}
|
||||||
|
<small class="text-muted">... and {{ reading_history | length - 3 }} more</small>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
24
src/hxbooks/templates/components/save_search_modal.html.j2
Normal file
24
src/hxbooks/templates/components/save_search_modal.html.j2
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!-- Save Search Modal -->
|
||||||
|
<div class="modal fade" id="save-search-modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Save Current Search</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form action="/saved-search" method="POST">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="search-name" class="form-label">Search Name</label>
|
||||||
|
<input type="text" class="form-control" id="search-name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="query_params" value="{{ request.args.q }}">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -48,63 +48,22 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h6 class="text-muted mb-0">Saved Searches</h6>
|
<h6 class="text-muted mb-0">Saved Searches</h6>
|
||||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#save-search-modal">
|
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#save-search-modal">
|
||||||
<i class="bi bi-plus"></i>
|
➕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for search_name, search_params in saved_searches.items() %}
|
{% for search_name, search_params in saved_searches.items() %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<a href="/?{{ search_params | urlencode }}" class="flex-grow-1 text-decoration-none">
|
<a href="/?q={{ search_params | urlencode }}" class="flex-grow-1 text-decoration-none">
|
||||||
<i class="bi bi-search me-2"></i> {{ search_name }}
|
🔍 {{ search_name }}
|
||||||
|
</a>
|
||||||
|
<a href="/saved-search/{{ search_name }}/delete" class="btn btn-sm btn-outline-danger">
|
||||||
|
🗑️
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteSavedSearch('{{ search_name }}')">
|
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Save Search Modal -->
|
|
||||||
<div class="modal fade" id="save-search-modal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Save Current Search</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<form action="/saved-search" method="POST">
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="search-name" class="form-label">Search Name</label>
|
|
||||||
<input type="text" class="form-control" id="search-name" name="name" required>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="query_params" value="{{ request.query_string.decode() }}">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-primary">Save Search</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function deleteSavedSearch(searchName) {
|
|
||||||
if (confirm(`Delete saved search '${searchName}'?`)) {
|
|
||||||
fetch('/saved-search', {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name: searchName })
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
<!-- Wishlist Status Component - TODO: Integrate with actual library functions -->
|
<!-- Wishlist Status Component -->
|
||||||
|
{% if g.viewing_user %}
|
||||||
|
{% set user_wishlist = book.wished_by | selectattr('user_id', 'equalto', g.viewing_user.id) | first %}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h6 class="text-muted mb-2">💝 Wishlist</h6>
|
<h6 class="text-muted mb-2">💝 Wishlist</h6>
|
||||||
|
|
||||||
{% if user_data.get('is_wishlisted') %}
|
{% if user_wishlist %}
|
||||||
<div class="alert alert-warning py-2">
|
<div class="alert alert-warning py-2">
|
||||||
<small>Added to wishlist: {{ user_data.wishlist_date.strftime('%B %d, %Y') if user_data.get('wishlist_date') else 'Unknown date' }}</small>
|
<small>Added to wishlist: {{ user_wishlist.wishlisted_date.strftime('%B %d, %Y') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-outline-danger btn-sm">💔 Remove from Wishlist</button>
|
<form action="/book/{{ book.id }}/wishlist/remove" method="POST" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm">💔 Remove from Wishlist</button>
|
||||||
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted small mb-2">Not in wishlist</p>
|
<p class="text-muted small mb-2">Not in wishlist</p>
|
||||||
<button class="btn btn-outline-primary btn-sm">💖 Add to Wishlist</button>
|
<form action="/book/{{ book.id }}/wishlist/add" method="POST" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-outline-primary btn-sm">💖 Add to Wishlist</button>
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
Reference in New Issue
Block a user