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:
|
||||
"""Save a search for a user. Mock implementation."""
|
||||
# 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()
|
||||
return True
|
||||
|
||||
|
||||
def delete_saved_search(user: User, search_name: str) -> bool:
|
||||
"""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:
|
||||
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()
|
||||
return True
|
||||
return False
|
||||
@@ -103,20 +101,12 @@ def book_detail(book_id: int) -> ResponseReturnValue:
|
||||
flash("Book not found", "error")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
# Get user-specific data
|
||||
viewing_user = session.get("viewing_as_user")
|
||||
user_data = {}
|
||||
for reading in book.readings:
|
||||
print(
|
||||
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:
|
||||
# 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)
|
||||
return render_template("book/detail.html.j2", book=book)
|
||||
|
||||
|
||||
@bp.route("/book/new", methods=["GET", "POST"])
|
||||
@@ -130,14 +120,26 @@ def create_book() -> ResponseReturnValue:
|
||||
|
||||
try:
|
||||
# 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
|
||||
book = library.create_book(
|
||||
title=title,
|
||||
owner_id=viewing_user.id if viewing_user else None,
|
||||
authors=request.form.getlist("authors"),
|
||||
genres=request.form.getlist("genres"),
|
||||
authors=authors,
|
||||
genres=genres,
|
||||
isbn=request.form.get("isbn"),
|
||||
publisher=request.form.get("publisher"),
|
||||
edition=request.form.get("edition"),
|
||||
@@ -167,12 +169,24 @@ def update_book(book_id: int) -> ResponseReturnValue:
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
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
|
||||
library.update_book(
|
||||
book_id=book_id,
|
||||
title=request.form.get("title"),
|
||||
authors=request.form.getlist("authors"),
|
||||
genres=request.form.getlist("genres"),
|
||||
authors=authors,
|
||||
genres=genres,
|
||||
isbn=request.form.get("isbn"),
|
||||
publisher=request.form.get("publisher"),
|
||||
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))
|
||||
|
||||
|
||||
@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:
|
||||
"""Delete a book."""
|
||||
"""Delete a book (GET shows confirmation, POST performs deletion)."""
|
||||
book = library.get_book(book_id)
|
||||
if not book:
|
||||
flash("Book not found", "error")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
try:
|
||||
title = book.title
|
||||
library.delete_book(book_id)
|
||||
flash(f"Book '{title}' deleted successfully!", "success")
|
||||
if request.method == "POST":
|
||||
# Perform the actual deletion
|
||||
try:
|
||||
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:
|
||||
flash(f"Error deleting book: {e}", "error")
|
||||
|
||||
return redirect(url_for("main.index"))
|
||||
# Show confirmation page
|
||||
return render_template("book/delete_confirm.html.j2", book=book)
|
||||
|
||||
|
||||
@bp.route("/import", methods=["GET", "POST"])
|
||||
@@ -222,7 +239,7 @@ def import_book() -> ResponseReturnValue:
|
||||
|
||||
try:
|
||||
# Get current viewing user as owner
|
||||
viewing_user = g.get("viewing_as_user")
|
||||
viewing_user = g.get("viewing_user")
|
||||
|
||||
# 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"])
|
||||
def save_search_route() -> ResponseReturnValue:
|
||||
"""Save a search for the current user."""
|
||||
viewing_user = session.get("viewing_as_user")
|
||||
viewing_user = g.get("viewing_user")
|
||||
if not viewing_user:
|
||||
flash("You must select a user to save searches", "error")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
search_name = request.form.get("name", "").strip()
|
||||
query_params = request.form.get("query_params", "")
|
||||
print(
|
||||
f"Saving search for user {viewing_user.username}: {search_name} -> {query_params}"
|
||||
)
|
||||
|
||||
if not search_name:
|
||||
flash("Search name is required", "error")
|
||||
return redirect(url_for("main.index"))
|
||||
return redirect(url_for("main.index", q=query_params))
|
||||
|
||||
try:
|
||||
success = save_search(viewing_user, search_name, query_params)
|
||||
@@ -280,27 +300,146 @@ def save_search_route() -> ResponseReturnValue:
|
||||
except Exception as e:
|
||||
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"])
|
||||
def delete_saved_search_route() -> ResponseReturnValue:
|
||||
"""Delete a saved search."""
|
||||
viewing_user = session.get("viewing_as_user")
|
||||
@bp.route("/book/<int:book_id>/reading/start", methods=["POST"])
|
||||
def start_reading_route(book_id: int) -> ResponseReturnValue:
|
||||
"""Start reading a book."""
|
||||
viewing_user = g.get("viewing_user")
|
||||
if not viewing_user:
|
||||
return {"error": "No user selected"}, 400
|
||||
|
||||
data = request.get_json()
|
||||
search_name = data.get("name") if data else None
|
||||
|
||||
if not search_name:
|
||||
return {"error": "Search name required"}, 400
|
||||
flash("You must select a user to start reading", "error")
|
||||
return redirect(url_for("main.book_detail", book_id=book_id))
|
||||
|
||||
try:
|
||||
success = delete_saved_search(viewing_user, search_name)
|
||||
if success:
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"error": "Search not found"}, 404
|
||||
library.start_reading(book_id=book_id, user_id=viewing_user.id)
|
||||
flash("Started reading!", "success")
|
||||
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>
|
||||
<html lang="en"
|
||||
<html lang="en"
|
||||
x-init="$el.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')">
|
||||
|
||||
<head>
|
||||
@@ -46,7 +46,8 @@
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Flash Messages -->
|
||||
{% 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 }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@@ -80,10 +81,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'components/import_modal.html.j2' %}
|
||||
{% include 'components/save_search_modal.html.j2' %}
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
<!-- Import Modal -->
|
||||
{% include 'components/import_modal.html.j2' %}
|
||||
</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>
|
||||
</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
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="col-lg-8">
|
||||
<form id="book-form" action="/book/{{ book.id }}/edit" method="POST">
|
||||
{% include 'components/book_form.html.j2' %}
|
||||
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<button type="submit" class="btn btn-primary">💾 Save Changes</button>
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- User-Specific Data Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
{% if session.get('viewing_as_user') %}
|
||||
@@ -55,29 +55,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmDelete(bookId) {
|
||||
if (confirm('Are you sure you want to delete this book? This action cannot be undone.')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/book/' + bookId + '/delete';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
// Simple form change detection
|
||||
let originalFormData = new FormData(document.getElementById('book-form'));
|
||||
let hasChanges = false;
|
||||
|
||||
// Simple form change detection
|
||||
let originalFormData = new FormData(document.getElementById('book-form'));
|
||||
let hasChanges = false;
|
||||
document.getElementById('book-form').addEventListener('input', function () {
|
||||
hasChanges = true;
|
||||
});
|
||||
|
||||
document.getElementById('book-form').addEventListener('input', function() {
|
||||
hasChanges = true;
|
||||
});
|
||||
document.getElementById('book-form').addEventListener('submit', function () {
|
||||
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>
|
||||
{% endblock %}
|
||||
@@ -2,13 +2,11 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-8">
|
||||
<label for="title" class="form-label">Title *</label>
|
||||
<input type="text" class="form-control" id="title" name="title"
|
||||
value="{{ book.title if book else '' }}" required>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{ book.title if book else '' }}" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="isbn" class="form-label">ISBN</label>
|
||||
<input type="text" class="form-control" id="isbn" name="isbn"
|
||||
value="{{ book.isbn if book else '' }}">
|
||||
<input type="text" class="form-control" id="isbn" name="isbn" value="{{ book.isbn if book else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,14 +14,14 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="authors" class="form-label">Authors</label>
|
||||
<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>
|
||||
<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>
|
||||
<div class="form-text">Enter one author per line</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="genres" class="form-label">Genres</label>
|
||||
<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>
|
||||
<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>
|
||||
<div class="form-text">Enter one genre per line</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,86 +30,49 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="first_published" class="form-label">Year Published</label>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="publisher" class="form-label">Publisher</label>
|
||||
<input type="text" class="form-control" id="publisher" name="publisher"
|
||||
value="{{ book.publisher if book else '' }}">
|
||||
<input type="text" class="form-control" id="publisher" name="publisher"
|
||||
value="{{ book.publisher if book else '' }}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="edition" class="form-label">Edition</label>
|
||||
<input type="text" class="form-control" id="edition" name="edition"
|
||||
value="{{ book.edition if book else '' }}">
|
||||
<input type="text" class="form-control" id="edition" name="edition" value="{{ book.edition if book else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
|
||||
<!-- Location Information -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="location_place" class="form-label">Location (Place)</label>
|
||||
<input type="text" class="form-control" id="location_place" name="location_place"
|
||||
value="{{ book.location_place if book else '' }}"
|
||||
placeholder="Home, Office, etc.">
|
||||
<input type="text" class="form-control" id="location_place" name="location_place"
|
||||
value="{{ book.location_place if book else '' }}" placeholder="Home, Office, etc.">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="location_bookshelf" class="form-label">Bookshelf</label>
|
||||
<input type="text" class="form-control" id="location_bookshelf" name="location_bookshelf"
|
||||
value="{{ book.location_bookshelf if book else '' }}"
|
||||
placeholder="Living room, Bedroom, etc.">
|
||||
<input type="text" class="form-control" id="location_bookshelf" name="location_bookshelf"
|
||||
value="{{ book.location_bookshelf if book else '' }}" placeholder="Living room, Bedroom, etc.">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="location_shelf" class="form-label">Shelf Number</label>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="mb-3">
|
||||
<label for="notes" class="form-label">Personal Notes</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3"
|
||||
placeholder="Your personal notes about this book...">{{ book.notes if book else '' }}</textarea>
|
||||
</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>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3"
|
||||
placeholder="Your personal notes about this book...">{{ book.notes if book else '' }}</textarea>
|
||||
</div>
|
||||
@@ -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">
|
||||
<h6 class="text-muted mb-2">📖 Reading Status</h6>
|
||||
|
||||
|
||||
<!-- Current Reading Status -->
|
||||
{% if user_data.get('current_reading') %}
|
||||
{% if current_reading %}
|
||||
<div class="alert alert-info py-2">
|
||||
<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>
|
||||
<button class="btn btn-success btn-sm me-2">✓ Finish Reading</button>
|
||||
<button class="btn btn-outline-secondary btn-sm">⏸ Drop Reading</button>
|
||||
<form action="/book/{{ book.id }}/reading/finish" method="POST" class="d-inline">
|
||||
<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 %}
|
||||
<!-- Not currently reading -->
|
||||
{% if user_data.get('reading_history') %}
|
||||
{% if reading_history %}
|
||||
<p class="text-muted small mb-2">Previously read</p>
|
||||
{% else %}
|
||||
<p class="text-muted small mb-2">Not read yet</p>
|
||||
{% 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 %}
|
||||
|
||||
|
||||
<!-- Reading History Summary -->
|
||||
{% if user_data.get('reading_history') %}
|
||||
{% if reading_history %}
|
||||
<div class="mt-3">
|
||||
<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">
|
||||
<small>
|
||||
{% if reading.finished %}
|
||||
✓ Finished {{ reading.end_date.strftime('%m/%d/%Y') }}
|
||||
{% if reading.rating %} - ⭐{{ reading.rating }}/5{% endif %}
|
||||
{% elif reading.dropped %}
|
||||
⏸ Dropped {{ reading.end_date.strftime('%m/%d/%Y') }}
|
||||
{% else %}
|
||||
📖 Started {{ reading.start_date.strftime('%m/%d/%Y') }}
|
||||
{% endif %}
|
||||
{% if not reading.dropped %} ✓ Finished {% else %} ⏸ Dropped {% endif %}
|
||||
{{ reading.start_date.strftime('%m/%d/%Y') }} - {{ reading.end_date.strftime('%m/%d/%Y') }}
|
||||
- ⭐{{ reading.rating or "-" }}/5
|
||||
</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if reading_history | length > 3 %}
|
||||
<small class="text-muted">... and {{ reading_history | length - 3 }} more</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% 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">
|
||||
<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">
|
||||
<i class="bi bi-plus"></i>
|
||||
➕
|
||||
</button>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for search_name, search_params in saved_searches.items() %}
|
||||
<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">
|
||||
<i class="bi bi-search me-2"></i> {{ search_name }}
|
||||
<a href="/?q={{ search_params | urlencode }}" class="flex-grow-1 text-decoration-none">
|
||||
🔍 {{ search_name }}
|
||||
</a>
|
||||
<a href="/saved-search/{{ search_name }}/delete" class="btn btn-sm btn-outline-danger">
|
||||
🗑️
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteSavedSearch('{{ search_name }}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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>
|
||||
</div>
|
||||
@@ -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">
|
||||
<h6 class="text-muted mb-2">💝 Wishlist</h6>
|
||||
|
||||
{% if user_data.get('is_wishlisted') %}
|
||||
|
||||
{% if user_wishlist %}
|
||||
<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>
|
||||
<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 %}
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user