Finish main functionality

This commit is contained in:
2026-03-19 02:20:31 +01:00
parent b231452ad0
commit cc03e60a4b
10 changed files with 403 additions and 221 deletions

View File

@@ -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],
)

View File

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

View 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 %}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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