Book Tracking and Management

Django application for tracking books, notes, reviews, reading progress, user profiles, and book discovery.

Python · Django · JavaScript · SQLite · Open Library API

README.md
# CS50W Book Tracker Project - Revised Edition - Library Demo

## Overview

This version of the CS50W Book Tracker represents a demo, refined and secure iteration of my original project. Designed to enrich the reading experience, it allows users to manage their reading lists, track progress, exchange insights, and discover new books. Integrating Python, Django, and JavaScript, this web application is dynamic, interactive, and optimized for mobile devices.

## Revision Background

The decision to recreate this project stemmed from a desire to apply enhanced coding practices, improve security measures (notably around sensitive information like the Django `SECRET_KEY`), and streamline the codebase for better maintainability and scalability. This repository serves as a clean, updated showcase of my capabilities, reflecting both my technical growth and my commitment to best practices in software development.

## Distinctiveness and Complexity

### Distinctiveness

What sets the CS50W Book Tracker apart is its focus on personalized book management and community interaction. It transcends the typical functionalities of e-commerce or social platforms by emphasizing personal reading journeys and fostering a book-loving community.

### Complexity

Key features demonstrating the project's complexity include:

- **Dynamic Book Management**: Facilitates personal curation of book collections and progress tracking.
- **ISBN-Based Book Addition**: Leverages the Open Library API for easy addition and detail retrieval of books via ISBN.
- **User Profiles and Progress Tracking**: Offers personalized profiles, reading progress indicators, and customizable settings.
- **Community Engagement**: Encourages sharing reviews and viewing others' reading lists, fostering community interaction.
- **AJAX-Driven Interactivity**: Enhances user experience with live searches, updates, and submissions without page reloads.
- **Responsive Design**: Guarantees a seamless experience across different devices.

## File Structure and Contents

- `library_demo_project/`: Main project directory for settings and root URL configurations.
- `library_demo_app/`: Application directory hosting models, views, templates, and static files.
    - `models.py`, `views.py`, `urls.py`: Core files for database models, application logic, and URL routing.
    - `templates/`: Houses HTML templates.
    - `static/`: Contains CSS, JavaScript, and other static assets.
    - `media/`: Stores user-uploaded or API-fetched content like book covers.
- `requirements.txt`: Lists the necessary Python packages.
- `README.md`: Documentation of the project.

## Running the Application

To get the Library Demo (CS50W Book Tracker) up and running:
1. Ensure required packages are installed: `pip install -r requirements.txt`.
2. Apply database migrations: `python manage.py migrate`.
3. Start the Django server: `python manage.py runserver`.
4. Visit `http://localhost:8000` in your browser to explore the app.

## Additional Information

- **Responsive Design**: Tested across multiple devices for consistent user experience.
- **Privacy and Security**: Prioritizes the confidentiality and integrity of user data.
- **User-Centric Features**: Developed with a focus on user needs and engagement.

This revised edition showcases the skills and knowledge gained from the CS50W course, emphasizing my full-stack development capabilities. It was not only a technical challenge but also an invaluable learning experience, furthering my understanding of web development best practices and secure coding.

I'm proud of this project as it represents a significant step in my coding journey, highlighting my dedication to continuous improvement and innovation in the realm of web development.
library_demo_app/models.py
from django.conf import settings
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    profile_photo = models.ImageField(upload_to='profile_photos/', null=True, blank=True)

    def __str__(self):
        return self.user.username
    
class Genre(models.Model):
    name = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    isbn = models.CharField(max_length=13)
    pages = models.IntegerField(default=0)
    genres = models.ManyToManyField(Genre, blank=True) 
    publication_date = models.DateField(blank=True, null=True)
    cover_image = models.ImageField(upload_to='covers/', blank=True, null=True)
    summary = models.TextField(blank=True) 

    def __str__(self):
        return self.title
    
class UserBook(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    read = models.BooleanField(default=False)
    progress = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)])
    
    # Whether a book should be hidden from public profile 
    hide_from_profile = models.BooleanField(default=False)


    def __str__(self):
        return f"{self.user.username} - {self.book.title}"

class Review(models.Model):
    user_book = models.ForeignKey(UserBook, on_delete=models.CASCADE, related_name='reviews')
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    rating = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
    comment = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"Review by {self.user.username} for {self.user_book.book.title}"
    
class ReadingChallenge(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    goal = models.IntegerField(default=50)  # User can change 
    progress = models.IntegerField(default=0)  # Tracks number of books read

    def __str__(self):
        return f"{self.user.username}'s Reading Challenge"
    
class QuickNote(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    note = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user.username}'s Note"
library_demo_app/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Book, Profile, Review, UserBook, QuickNote

class ExtendedSignUpForm(UserCreationForm):
    email = forms.EmailField(max_length=254, help_text='Required. Inform a valid email address.')
    bio = forms.CharField(max_length=500, required=False)
    location = forms.CharField(max_length=100, required=False)
    birth_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}), required=False)

    def __init__(self, *args, **kwargs):
        super(ExtendedSignUpForm, self).__init__(*args, **kwargs)
        self.fields['password1'].widget.attrs['id'] = 'id_password1'
        self.fields['password2'].widget.attrs['id'] = 'id_password2'

    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2', 'bio', 'location', 'birth_date') 

    def save(self, commit=True):
        user = super().save(commit=False)
        if commit:
            user.save()
            Profile.objects.create(
                user=user,
                bio=self.cleaned_data['bio'],
                location=self.cleaned_data['location'],
                birth_date=self.cleaned_data['birth_date'],
            )
        return user
    
class ReviewForm(forms.ModelForm):
    rating = forms.ChoiceField(choices=[(num, num) for num in range(1, 6)], widget=forms.RadioSelect)

    class Meta:
        model = Review
        fields = ['rating', 'comment']

class BookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = ['title', 'author', 'isbn', 'pages', 'genres', 'publication_date', 'cover_image', 'summary']
        widgets = {
            'publication_date': forms.DateInput(attrs={'type': 'date'}),
            'genres': forms.CheckboxSelectMultiple(),
        }

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ['bio', 'location', 'birth_date', 'profile_photo']
        widgets = {
            'birth_date': forms.DateInput(attrs={'type': 'date'}),
        }

class UserBookForm(forms.ModelForm):
    class Meta:
        model = UserBook
        fields = ['read', 'progress']

class QuickNoteForm(forms.ModelForm):
    class Meta:
        model = QuickNote
        fields = ['note']   
library_demo_app/views.py
import requests
import os
import io
import imgkit
import random
import json
from PIL import Image
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, logout
from .forms import ExtendedSignUpForm, BookForm, ProfileForm, ReviewForm, QuickNoteForm
from .models import Book, UserBook, Review, Genre, ReadingChallenge, User, Profile, QuickNote
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.contrib import messages
from django.contrib.auth.views import LoginView as AuthLoginView
from django.core.files.base import ContentFile
from django.core.files.temp import NamedTemporaryFile
from django.template.loader import render_to_string
from django.http import HttpResponse
from django.contrib.staticfiles import finders #Reference
from django.templatetags.static import static #Refrence
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError



class CustomLoginView(AuthLoginView):
    def form_valid(self, form):
        messages.success(self.request, 'You have successfully logged in.')
        return super().form_valid(form)
    
@require_http_methods(["POST"])
def custom_logout(request):
    logout(request)
    return redirect('home')
    
@require_http_methods(["POST"])
def validate_user_password(request):

    data = json.loads(request.body)
    password = data.get('password')

    try:
        # Use Django's password validators
        validate_password(password)
        return JsonResponse({'valid': True})
    except ValidationError as e:
        # Return the error messages if password is invalid
        return JsonResponse({'valid': False, 'errors': e.messages})

def general_book_search(request):
    query = request.GET.get('q', '')
    books = None

    if query:
        books = Book.objects.filter(
            title__icontains=query) | Book.objects.filter(
            author__icontains=query) | Book.objects.filter(
            isbn__icontains=query)
    else:
        books = Book.objects.none() # books is an empty QuerySet, not None

    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        results = [{"title": book.title, "id": book.id} for book in books]
        return JsonResponse({"results": results}, content_type="application/json")
    
    return render(request, 'tracker/general_book_search.html', {'books': books, 'query': query})

def explore_books(request):
    books = list(Book.objects.all()) # Retrieve all books from the database
    random.shuffle(books)
    return render(request, 'tracker/explore_books.html', {'books': books})

@login_required
def book_list(request):
    # Fetch the UserBook instances for the current user
    user_books = UserBook.objects.filter(user=request.user)

    # Passing user_books directly to the template
    return render(request, 'tracker/book_list.html', {'user_books': user_books})

@require_POST
@login_required
def add_to_my_list(request, book_id):
    book = get_object_or_404(Book, id=book_id)

    # Check for duplicates
    if UserBook.objects.filter(user=request.user, book=book).exists():
        message = "You already have this book in your list."
        if request.headers.get('x-requested-with') == 'XMLHttpRequest':
            return JsonResponse({"status": "error", "message": message})
        messages.error(request, message)
        return redirect('general_book_search')

    # Create a new UserBook object and save the reference to it
    new_user_book = UserBook.objects.create(user=request.user, book=book)
    message = "Book added to your list."

    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        return JsonResponse({
            "status": "added", 
            "message": "Book added to your list.", 
            "user_book_id": new_user_book.id
        })

    messages.success(request, message)
    return redirect('book_list')


def home(request):
    # Get a random set of books 
    books = list(Book.objects.all())
    random_books = random.sample(books, min(len(books), 10)) if books else []

    reading_challenge = None
    if request.user.is_authenticated:
        user = request.user
        reading_challenge, created = ReadingChallenge.objects.get_or_create(user=user)
        progress = UserBook.objects.filter(user=user, read=True).count()
        reading_challenge.progress = progress
        reading_challenge.save()

    return render(request, 'tracker/home.html', {
        'reading_challenge': reading_challenge,
        'random_books': random_books # Show random books in template
    })

def signup(request):
    if request.method == 'POST':
        form = ExtendedSignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            messages.success(request, 'You have successfully signed up.')
            return redirect('home')
    else:
        form = ExtendedSignUpForm()
    return render(request, 'tracker/signup.html', {'form': form})

@login_required
def dashboard(request):
    in_progress_books = UserBook.objects.filter(user=request.user, read=False, progress__gt=0)[:5]
    current_user_profile = request.user.profile
    profiles = Profile.objects.exclude(user=request.user)  # Exclude the current user's profile
    user_profiles = list(User.objects.exclude(id=request.user.id).exclude(username='').exclude(is_superuser=True))
    random_profiles = random.sample(user_profiles, min(len(user_profiles), 2))

    quick_notes = QuickNote.objects.filter(user=request.user).order_by('-created_at')
    if request.method == 'POST':
        form = QuickNoteForm(request.POST)
        if form.is_valid():
            note = form.save(commit=False)
            note.user = request.user
            note.save()
            # Redirect to prevent resubmission if the user refreshes the page
            return redirect('dashboard')
    else:
        form = QuickNoteForm()

        from django.http import JsonResponse


    return render(request, 'tracker/dashboard.html', {
        'in_progress_books': in_progress_books,
        'current_user_profile': current_user_profile,
        'random_profiles': random_profiles,
        'profiles': profiles,
        'quick_notes': quick_notes,
        'note_form': form,
    })

@login_required
def add_quick_note(request):
    if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest':
        form = QuickNoteForm(request.POST)
        if form.is_valid():
            quick_note = form.save(commit=False)
            quick_note.user = request.user
            quick_note.save()
            return JsonResponse({
                'status': 'success',
                'note': quick_note.note,
                'created_at': quick_note.created_at.strftime('%Y-%m-%d %H:%M')
            })
        else:
            return JsonResponse({'status': 'error', 'errors': form.errors.as_json()})
    return JsonResponse({'status': 'failed'}, status=400)

@login_required
def user_profile(request, username):
    profile = get_object_or_404(Profile, user__username=username)
    # Get the user's profile based on the username
    profile_user = User.objects.get(username=username)
    user_books = UserBook.objects.filter(user=profile_user)

    # Determine which books to show the "Add Book" button for
    current_user_book_ids = request.user.userbook_set.all().values_list('book_id', flat=True)
    books_to_add = user_books.exclude(book_id__in=current_user_book_ids)

    #filter out hidden books
    user_books = UserBook.objects.filter(user=profile_user, hide_from_profile=False)

    return render(request, 'tracker/user_profile.html', {
        'profile_user': profile_user,
        'user_books': user_books,
        'books_to_add': books_to_add,
        'current_user_book_ids': current_user_book_ids,
    })

@login_required
@require_POST
def toggle_book_visibility(request, user_book_id):
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        user_book = get_object_or_404(UserBook, pk=user_book_id, user=request.user)

        user_book.hide_from_profile = not user_book.hide_from_profile
        user_book.save()

        return JsonResponse({'success': True, 'new_state': user_book.hide_from_profile})
    else:
        return JsonResponse({'success': False})



def fetch_book_details(isbn):
    url = f'https://openlibrary.org/isbn/{isbn}.json'
    response = requests.get(url)
    if response.ok:
        book_data = response.json()
        # Extract author keys
        author_keys = [author['key'] for author in book_data.get('authors', [])]
        return book_data, author_keys
    else:
        return None, []
    
def fetch_author_details(author_key):
    """
    Fetches the author's details from the Open Library API given the author's key.
    """
    base_url = "https://openlibrary.org"
    author_url = f"{base_url}{author_key}.json"
    response = requests.get(author_url)
    if response.ok:
        author_data = response.json()
        return author_data.get("name")
    else:
        return None  

@login_required
def add_book(request):
    KNOWN_MULTI_WORD_GENRES = [
        'Literary Fiction', 'Historical Fiction', 'Science Fiction',
        'Young Adult (YA)', 'Children\'s Fiction', 'Health & Wellness',
        'Business & Economics', 'True Crime', 'Art & Photography',
        'Religion & Spirituality', 'Graphic Novels'
    ]

    cover_image_url = None

    if request.method == 'POST':
        form = BookForm(request.POST, request.FILES)
        if form.is_valid():
            book_instance = form.save(commit=False)

            # Check if a cover image URL was saved in the session
            session_cover_image_url = request.session.pop('cover_image_url', None)
            if session_cover_image_url:
                response = requests.get(session_cover_image_url)
                if response.status_code == 200:
                    book_instance.cover_image.save(os.path.basename(session_cover_image_url), ContentFile(response.content))

            book_instance.save()
            form.save_m2m()  # Save many-to-many fields like genres
            # Create a UserBook instance
            UserBook.objects.create(user=request.user, book=book_instance, read=False)
            messages.success(request, 'Book added to your list successfully!')
            return redirect('book_list')
        else:
            messages.error(request, 'There was an error adding the book.')

    else:  # GET request
        isbn = request.GET.get('isbn')
        form = BookForm()
        if isbn:
            # Remove dashes from the ISBN
            isbn = isbn.replace('-', '')

            book_data, author_keys = fetch_book_details(isbn)
            if book_data:
                authors = [fetch_author_details(key) for key in author_keys if key]
                genre_instances = process_genres(book_data.get('subjects', []), KNOWN_MULTI_WORD_GENRES)
                cover_id = book_data.get('covers', [None])[0]
                cover_image, cover_image_url = download_and_verify_image(cover_id) if cover_id else (None, None)
                
                initial_data = {
                    'title': book_data.get('title'),
                    'author': ', '.join(authors),
                    'isbn': isbn,
                    'pages': book_data.get('pagination'),
                    'publication_date': book_data.get('publish_date') + "-01-01" if book_data.get('publish_date') else None,
                }
                form = BookForm(initial=initial_data)
                form.fields['genres'].initial = [genre.id for genre in genre_instances]

                if cover_image_url:
                    # Save the cover image URL in the session for later use
                    request.session['cover_image_url'] = cover_image_url
            else:
                messages.error(request, 'No book data found for the provided ISBN.')

    return render(request, 'tracker/add_book.html', {
        'form': form,
        'cover_image_url': cover_image_url,
    })

def process_genres(api_genres, known_multi_word_genres):
    genre_instances = []
    for genre_string in api_genres:
        if genre_string in known_multi_word_genres:
            genre, created = Genre.objects.get_or_create(name=genre_string)
            genre_instances.append(genre)
        else:
            genre_names = genre_string.split(',')
            for genre_name in genre_names:
                genre_name = genre_name.strip()
                if genre_name:
                    genre, created = Genre.objects.get_or_create(name=genre_name)
                    genre_instances.append(genre)
    return genre_instances

def download_and_verify_image(cover_id):
    cover_image_url = f'https://covers.openlibrary.org/b/id/{cover_id}-L.jpg'
    response = requests.get(cover_image_url)
    if response.status_code == 200:
        img_temp = NamedTemporaryFile(delete=True)
        img_temp.write(response.content)
        img_temp.flush()

        try:
            img = Image.open(img_temp.name)
            img.verify()  # Verify that it's a valid image
            img_temp.seek(0)  # Seek to the start of the file
            cover_image = ContentFile(img_temp.read(), name=f"cover_{cover_id}.jpg")
            return cover_image, cover_image_url
        except (IOError, ValueError):
            return None, cover_image_url
        finally:
            img_temp.close()
    else:
        return None, None



@login_required
def book_detail(request, book_id):
    book = get_object_or_404(Book, pk=book_id)
    user_book = UserBook.objects.filter(user=request.user, book=book).first()
    reviews = Review.objects.filter(user_book__book=book)
    
    # Check if the current user has already submitted a review for this book
    has_reviewed = Review.objects.filter(user_book__book=book, user_book__user=request.user).exists()
    
    # Only pass the form if the user has not reviewed
    review_form = ReviewForm() if not has_reviewed else None

    return render(request, 'tracker/book_detail.html', {
        'book': book, 
        'user_book': user_book, 
        'reviews': reviews,
        'form': review_form,  # This will be None if the user has already reviewed
        'has_reviewed': has_reviewed  # Pass this to the template to control form display
    })


@login_required
def edit_book(request, user_book_id):
    user_book = get_object_or_404(UserBook, id=user_book_id, user=request.user)
    if request.method == 'POST':
        form = BookForm(request.POST, request.FILES, instance=user_book.book)
        if form.is_valid():
            form.save()
            return redirect('book_list')
    else:
        form = BookForm(instance=user_book.book)
    return render(request, 'tracker/edit_book.html', {'form': form})

#Renamed in FRONT-END to Remove, backend remains the same; does NOT delete the book from database, just removes it from the user's personal book list
@login_required
def delete_book(request, user_book_id):
    user_book = get_object_or_404(UserBook, id=user_book_id, user=request.user)
    if request.method == 'POST':
        user_book.delete()
        return redirect('book_list')
    return render(request, 'tracker/confirm_delete.html', {'book': user_book.book})


@login_required
def edit_profile(request):
    if request.method == 'POST':
        form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
        if form.is_valid():
            form.save()
            return redirect('dashboard')
    else:
        form = ProfileForm(instance=request.user.profile)

    return render(request, 'tracker/edit_profile.html', {'form': form})

@login_required
def update_read_status(request, user_book_id):
    user_book = get_object_or_404(UserBook, id=user_book_id, user=request.user)
    
    if request.method == 'POST':
        status = request.POST.get('status')
        
        if status == 'Not Started':
            read_status = False
            progress = 0
        elif status == 'In Progress':
            read_status = False
            progress = request.POST.get('progress', 0)
        elif status == 'Completed':
            read_status = True
            progress = 100
        else:
            # Handle unexpected status value
            return JsonResponse({"status": "error", "message": "Invalid read status."}, status=400)

        user_book.read = read_status
        user_book.progress = min(max(int(progress), 0), 100)  # Ensure progress is between 0 and 100
        user_book.save()

        if request.headers.get('x-requested-with') == 'XMLHttpRequest':
            return JsonResponse({"status": "success", "message": "Read status and progress updated."})
        
        messages.success(request, 'Read status and progress updated.')
        return redirect('book_list')
    
    return redirect('book_list')




@login_required
def submit_review(request, user_book_id):
    user_book = get_object_or_404(UserBook, pk=user_book_id, user=request.user)

    if not user_book.read:
        message = "You can only review books you've read."
        if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return JsonResponse({"status": "error", "message": message})
        messages.error(request, message)
        return redirect('book_detail', book_id=user_book.book.id)

    if request.method == 'POST':
        form = ReviewForm(request.POST)
        if form.is_valid():
            review = form.save(commit=False)
            review.user_book = user_book
            review.user = request.user
            review.save()
            review_data = {
                "user": review.user.username,
                "rating": review.rating,
                "comment": review.comment,
                "created_at": review.created_at.strftime('%Y-%m-%d %H:%M:%S') 
            }

            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return JsonResponse({"status": "success", "new_review": review_data})

            messages.success(request, "Review submitted successfully.")
            return redirect('book_detail', book_id=user_book.book.id)

        else:
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                # Handle form errors in AJAX
                return JsonResponse({"status": "error", "errors": form.errors.as_json()})
            # If not an AJAX request, re-render the page with the form and its errors
            return render(request, 'book_detail.html', {'form': form, 'user_book': user_book})

    # If it's not a POST request, redirect to the book detail page.
    return redirect('book_detail', book_id=user_book.book.id)


@login_required
def generate_book_card(request, book_id):
    # Fetch the book using the provided ID
    book = get_object_or_404(Book, id=book_id)

    # Fetch the UserBook instance
    #user_book = UserBook.objects.filter(user=request.user, book=book).first()
    #if not user_book:
        # If no UserBook instance is found, handle this case appropriately
       #return HttpResponse("No review found for this book.", status=404)
    
    """ NOT IN USE, KEPT FOR REFRENCE
    # Fetch the review for the UserBook instance
    review = Review.objects.filter(user_book=user_book).first()
    if not review:
        # If no review is found, handle this case
        # You might want to show a message that there's no review
        return HttpResponse("No review found for this book.", status=404)
    """

    # Check if the book has a cover image and create the absolute URL
    if book.cover_image:
        cover_image_url = request.build_absolute_uri(book.cover_image.url)
    else:
        cover_image_url = None  # Or set a default image URL

    # Render the HTML for the book card
    html = render_to_string('tracker/shareable_book_card.html', {
        'book': book,
        # 'review': review,
        'cover_image_url': cover_image_url,
        'website_name': 'BookTracker.project'
    })

   # Convert HTML to Image and get the image as bytes
    image_bytes = html_to_image(html)  # html_to_image now returns bytes

    # Write the image bytes to the buffer
    buffer = io.BytesIO(image_bytes)

    # Create a response with the image
    response = HttpResponse(buffer.getvalue(), content_type="image/png")
    response['Content-Disposition'] = 'attachment; filename="book_card.png"'
    return response

def html_to_image(html):
    options = {
        'format': 'png',
        'encoding': "UTF-8",
    }

    img = imgkit.from_string(html, False, options=options)
    return img

@login_required
def reading_challenge_view(request):
    user = request.user
    reading_challenge, created = ReadingChallenge.objects.get_or_create(user=user) #REference: created 

    if request.method == 'POST':
        new_goal = int(request.POST.get('new_goal', 0))
        reading_challenge.goal = new_goal
        reading_challenge.save()
        
        progress = UserBook.objects.filter(user=user, read=True).count()

        if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return JsonResponse({
                'status': 'success',
                'new_goal': reading_challenge.goal,
                'progress': progress,
                'congratulations': progress >= reading_challenge.goal
            })

        return redirect('home')

    progress = UserBook.objects.filter(user=user, read=True).count()
    reading_challenge.progress = progress
    reading_challenge.save()

    return render(request, 'tracker/home.html', {
        'reading_challenge': reading_challenge
    })

#
library_demo_app/urls.py
from django.urls import path
from . import views
from django.contrib.auth import views as auth_views
from .views import add_book, edit_profile, update_read_status, general_book_search, add_to_my_list, generate_book_card, reading_challenge_view, explore_books, user_profile

urlpatterns = [
    path('', views.home, name='home'),  # This is the root URL
    path('books/', views.book_list, name='book_list'),  
    path('search/', general_book_search, name='general_book_search'),
    path('signup/', views.signup, name='signup'),
    path('dashboard/', views.dashboard, name='dashboard'),
    path('add_book/', add_book, name='add_book'),
    path('books/<int:book_id>/', views.book_detail, name='book_detail'),
    path('books/edit/<int:user_book_id>/', views.edit_book, name='book_edit'),
    # delete_book Renamed in FRONT-END to Remove, backend remains the same; does NOT delete the book from database
    path('books/delete/<int:user_book_id>/', views.delete_book, name='book_delete'),
    path('profile/edit/', edit_profile, name='edit_profile'),
    path('books/review/<int:user_book_id>/', views.submit_review, name='submit_review'),
    path('books/update_read_status/<int:user_book_id>/', update_read_status, name='update_read_status'),
    path('add-to-my-list/<int:book_id>/', add_to_my_list, name='add_to_my_list'),
    path('generate_book_card/<int:book_id>/', generate_book_card, name='generate_book_card'),
    path('reading_challenge/', reading_challenge_view, name='reading_challenge'),
    path('explore/', explore_books, name='explore_books'),
    path('validate-password/', views.validate_user_password, name='validate_password'),
    path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
    path('books/toggle_visibility/<int:user_book_id>/', views.toggle_book_visibility, name='toggle_book_visibility'),
    path('profile/<str:username>/', views.user_profile, name='user_profile'),
    path('add_quick_note/', views.add_quick_note, name='add_quick_note'),

]
library_demo_project/settings.py
"""
Django settings for library_demo_project project.

Generated by 'django-admin startproject' using Django 5.0.2.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""

from pathlib import Path
import environ
import os
from django.core.management.utils import get_random_secret_key

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/

base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
env_file = os.path.join(base_dir, '.env')

env = environ.Env()

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'booktracker.example.invalid']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'library_demo_app'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'library_demo_project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'tracker', 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'library_demo_project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

CSRF_TRUSTED_ORIGINS = [
    'https://booktracker.example.invalid',
]

CSRF_COOKIE_DOMAIN = 'booktracker.example.invalid'

CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

# Custom settings

LOGIN_REDIRECT_URL = 'home'

LOGOUT_REDIRECT_URL = 'home'

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),]

# Email settings
# Email not setup yet
# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# Recaptcha settings will go here
# RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY')
# RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PRIVATE_KEY')

# Deploy test 3
library_demo_project/urls.py
"""
URL configuration for library_demo_project project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
from library_demo_app.views import CustomLoginView, custom_logout
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('library_demo_app.urls')),
    path('login/', auth_views.LoginView.as_view(template_name='library_demo_app/login.html'), name='login'),
    path('custom-logout/', custom_logout, name='custom_logout'),
    path('accounts/', include('django.contrib.auth.urls')),

]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
library_demo_app/static/main.js

// Main 
document.addEventListener('DOMContentLoaded', function() {
    // Live search listener
    const searchInput = document.getElementById('book-search');
    const resultsContainer = document.getElementById('search-results');

    if (searchInput) {
        searchInput.addEventListener('input', function(e) {
            const query = e.target.value;
            // Hide and clear the container if the query is empty
            if (!query) {
                resultsContainer.style.display = 'none';
                resultsContainer.innerHTML = '';
                return; // Exit the function early
            }

            // Perform the AJAX fetch for the live search
            fetch(`/search/?q=${query}`, {
                headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                }
            })
            .then(response => {
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                return response.json();
            })
            .then(data => {
                // Show the container when there are results
                resultsContainer.style.display = 'block';
                resultsContainer.innerHTML = ''; // Clear existing results

                if (data.results.length === 0) {
                    resultsContainer.innerHTML = '<div>No results found</div>';
                } else {
                    data.results.forEach(book => {
                        // Create a link for each book and append it to the results container
                        const bookLink = document.createElement('a');
                        bookLink.href = `/books/${book.id}/`; // Link to book detail page
                        bookLink.textContent = book.title;
                        bookLink.classList.add('search-result-item');
                        resultsContainer.appendChild(bookLink);
                    });
                }
            })
            .catch(error => {
                console.error('Error:', error);
                // Handle the error, e.g., show an error message to the user
            });
        });
    }

    // Search form submission prevention
    const searchForm = document.querySelector('form');
    if (searchForm) {
        searchForm.addEventListener('submit', function(event) {
            console.log('Search submitted!');
            event.preventDefault();
        });
    }

    // Update reading status function
    const readStatusForm = document.getElementById('read-status-form');

    if (readStatusForm) {
        readStatusForm.addEventListener('submit', function(event) {
            event.preventDefault(); // Prevent the default form submission

            const userBookId = this.querySelector('select[id^="read-status-selector"]').id.split('_')[1];
            const readStatus = this.querySelector(`#read-status-selector_${userBookId}`).value;
            const progressInput = document.querySelector(`#progress_${userBookId}`);
            const progressBar = document.getElementById(`progress-bar_${userBookId}`);
            let progressValue = progressInput ? progressInput.value : 0;

            // Adjust progress based on read status and update the progress bar visually
            switch (readStatus) {
                case 'Not Started':
                    progressValue = 0;
                    progressBar.style.width = '0%';
                    progressInput.classList.remove('show');
                    break;
                case 'In Progress':
                    progressBar.style.width = `${progressValue}%`;
                    progressInput.classList.add('show');
                    break;
                case 'Completed':
                    progressValue = 100;
                    progressBar.style.width = '100%';
                    progressInput.classList.remove('show');
                    break;
            }

            // AJAX call to update the read status
            const formData = new FormData(this);
            formData.append('status', readStatus);
            formData.append('progress', progressValue);
            formData.append('csrfmiddlewaretoken', csrftoken); // Add CSRF token from a global variable

            fetch(`/books/update_read_status/${userBookId}/`, {
                method: 'POST',
                headers: {
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRFToken': csrftoken 
                },
                body: formData // Send the form data
            })
            .then(response => response.json())
            .then(data => {
                if (data.status === 'success') {
                    console.log('Read status and progress updated.');
                    // Show a success message to the user
                } else {
                    console.error('Error updating status:', data.message);
                }
            })
            .catch(error => {
                console.error('There was an error updating the read status:', error);
                // Show an error message to the user
            });
        });
    }
        

        // Add event listeners to the read-status-selector to show/hide progress input
        document.querySelectorAll('select[id^="read-status-selector"]').forEach(function(selector) {
            selector.addEventListener('change', function() {
                const userBookId = this.id.split('_')[1];
                const progressInput = document.querySelector(`#progress_${userBookId}`);
                const progressBar = document.querySelector(`#progress-bar_${userBookId}`);
                const selectedValue = this.value;

                if (selectedValue === 'In Progress') {
                    progressInput.classList.remove('hidden');
                    progressInput.classList.add('visible');
                } else {
                    progressInput.classList.remove('visible');
                    progressInput.classList.add('hidden');
                }

                if (selectedValue === 'Completed') {
                    progressBar.style.width = '100%';
                } else if (selectedValue === 'Not Started') {
                    progressBar.style.width = '0%';
                }
            });
        });

        // Add event listeners to the progress input to update the progress bar dynamically
        document.querySelectorAll('input[id^="progress"]').forEach(function(input) {
            input.addEventListener('input', function() {
                const userBookId = this.id.split('_')[1];
                const progressBar = document.querySelector(`#progress-bar_${userBookId}`);
                progressBar.style.width = this.value + '%';
            });
        });

        // Add to my list 

        document.querySelectorAll('.add-book-btn').forEach(function(button) {
            button.addEventListener('click', function(event) {
                event.preventDefault();
                const bookId = this.dataset.bookId;

                fetch(`/add-to-my-list/${bookId}/`, {
                    method: 'POST',
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest',
                        'X-CSRFToken': csrftoken
                    }
                })
                .then(response => response.json())
                .then(data => {
                    if (data.status === 'added') {
                        console.log(data.message);

                        // Find the container of the action buttons
                        const actionButtonsContainer = this.closest('.action-buttons');
                        if (actionButtonsContainer) {
                            // Assuming data.user_book_id contains the ID of the newly created UserBook
                            const userBookId = data.user_book_id;

                            // Construct the URLs based on your URL patterns
                            const editUrl = `/books/edit/${userBookId}/`;
                            const deleteUrl = `/books/delete/${userBookId}/`;
                            const shareableCardUrl = `/generate_book_card/${bookId}/`;

                            // Update the buttons
                            actionButtonsContainer.innerHTML = 
                            `<a href="${shareableCardUrl}" class="btn btn-secondary action-btn">Generate Shareable Card</a>` +
                            `<a href="${editUrl}" class="btn btn-primary action-btn">Edit</a>` +
                            `<a href="${deleteUrl}" class="btn btn-danger action-btn">Remove</a>`;
                        
                        }
                    } else {
                        console.error(data.message);
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                });
            });
        });


    // Submit review
    document.addEventListener('submit', function(e) {
        if (e.target && e.target.matches('.review-form')) {
            e.preventDefault();
            const userBookId = e.target.dataset.userBookId; // Use getAttribute to ensure you get the correct data

            fetch(`/books/review/${userBookId}/`, {
                method: 'POST',
                body: new FormData(e.target),
                headers: {
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRFToken': csrftoken
                }
            })
            .then(response => {
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                return response.json();
            })
            .then(data => {
                if (data.status === 'success') {
                    // Add the new review to the DOM without reloading the page
                    const reviewsContainer = document.querySelector('.reviews');
                    const newReview = document.createElement('div');
                    newReview.classList.add('review');
                    newReview.innerHTML = `
                        <p><strong>${data.new_review.user}:</strong> ${data.new_review.comment} (Rating: ${data.new_review.rating})</p>
                        <p class="review-date">Date: ${data.new_review.created_at}</p>
                    `;
                    reviewsContainer.appendChild(newReview);

                    // Update the UI to reflect that the user has reviewed
                    e.target.style.display = 'none'; // Use e.target here since it's the form that triggered the submit event
                    const reviewStatus = document.querySelector('.review-status');
                    if (reviewStatus) {
                        reviewStatus.innerHTML = 'You have already submitted a review for this book.';
                    }
                } else {
                    // Handle form errors or other issues
                    console.error(data.errors);
                }
            })
            .catch(error => {
                console.error('Error during fetch:', error);
            });
        }
    });


    // Reading Challenge
    const challengeForm = document.getElementById('reading-challenge-form');
    const congratulationsMessage = document.getElementById('congratulations-message'); // Display message 

    if (challengeForm) {
        challengeForm.addEventListener('submit', function(e) {
            e.preventDefault();
            const formData = new FormData(this);
            fetch('/reading_challenge/', {
                method: 'POST',
                body: formData,
                headers: {
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRFToken': csrftoken  
                }
            })
            .then(response => {
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                return response.json();
            })
            .then(data => {
                if (data.status === 'success') {
                    // Update the goal and progress on the page
                    document.getElementById('reading-goal').textContent = data.new_goal;
                    document.getElementById('reading-progress').textContent = data.progress;
                    if (data.congratulations) {
                        congratulationsMessage.textContent = 'Congratulations on completing your challenge!';
                        congratulationsMessage.style.display = 'block'; // Show the message
                    } else {
                        congratulationsMessage.style.display = 'none'; // Hide the message
                    }
                } else {
                    console.error('Error:', data.message);
                }
            })
            .catch(error => {
                console.error('Error during fetch:', error);
            });
        });
    }


 

        // Swiper
        var swiper = new Swiper('.swiper-container', {
            initialSlide: 3,
            slidesPerView: 3,
            spaceBetween: 30,
            pagination: {
                el: '.swiper-pagination',
                clickable: true,
            },
            
            autoplay: {
                delay: 2500,  // Delay in milliseconds between slides. Adjust as needed.
                disableOnInteraction: false, // Autoplay will not be disabled after user interactions
            },

            speed: 600,

    
            navigation: false,
            
            // Responsive breakpoints
            breakpoints: {
                // when window width is <= 499px
                499: {
                    slidesPerView: 2,
                    spaceBetweenSlides: 30
                },
                // when window width is <= 999px
                999: {
                    slidesPerView: 3,
                    spaceBetweenSlides: 40
                }
            }
        });

    

    // Password validation and matching live

    const password1 = document.getElementById('id_password1');
    const password2 = document.getElementById('id_password2');
    const passwordError = document.getElementById('passwordError');
    
    if (password1 && password2 && passwordError) {
        let lastPasswordStrengthValid = true;
    // Function to validate password strength
    function validatePasswordStrength() {
        fetch('/validate-password/', {
            method: 'POST',
            body: JSON.stringify({ 'password': password1.value }),
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': csrftoken 
            }
        })
        .then(response => response.json())
        .then(data => {
            lastPasswordStrengthValid = data.valid;
            updateErrorMessage();
        })
        .catch(error => {
            console.error('Error:', error);
        });
    }
    
    // Function to update the error message based on password strength and match
    function updateErrorMessage() {
        if (!lastPasswordStrengthValid) {
            // Show password strength error
            passwordError.textContent = "Password is too weak.";
            passwordError.style.display = 'block';
        } else if (password1.value !== password2.value) {
            // Show password mismatch error
            password2.setCustomValidity("Passwords don't match.");
            passwordError.textContent = "Passwords don't match.";
            passwordError.style.display = 'block';
        } else {
            // Clear any error messages
            password2.setCustomValidity('');
            passwordError.textContent = '';
            passwordError.style.display = 'none';
        }
    }
    
    // Event listeners for input on both password fields
    password1.addEventListener('input', function() {
        validatePasswordStrength();
        updateErrorMessage();
    });
    
    password2.addEventListener('input', updateErrorMessage);
}
    
    // Toggle visibilkity on users profile books

    document.querySelectorAll('.toggle-visibility').forEach(function(button) {
        // Set initial text and class based on the current state
        button.textContent = button.getAttribute('data-current-state') === 'true' ? 'Display on Profile' : 'Hide From Profile';
        button.classList.toggle('btn-primary', button.getAttribute('data-current-state') !== 'true');
        button.classList.toggle('btn-danger', button.getAttribute('data-current-state') === 'true');
    
        button.addEventListener('click', function() {
            let bookId = this.getAttribute('data-book-id');
            
            // Current state inside the click event listener
            let currentState = this.getAttribute('data-current-state') === 'true';
    
            // Determine the intended new state (opposite of current)
            let newState = !currentState;
    
            // Update text and class to reflect the intended new state
            this.textContent = newState ? 'Hide From Profile' : 'Display on Profile';
            this.classList.toggle('btn-primary', newState);
            this.classList.toggle('btn-danger', !newState);
    
            fetch(`/books/toggle_visibility/${bookId}/`, {
                method: 'POST',
                headers: {
                    'X-CSRFToken': getCookie('csrftoken'),
                    'Content-Type': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                },
            })
            .then(response => response.json())
            .then(data => {
                if (!data.success) {
                    // Revert text and class if the server update fails
                    newState = currentState; // Revert newState to the original currentState
                } else {
                    // Update currentState to reflect the successful change
                    currentState = newState;
                }
                this.setAttribute('data-current-state', currentState.toString());
                this.textContent = currentState ? 'Display on Profile' : 'Hide From Profile';
                this.classList.toggle('btn-primary', !currentState);
                this.classList.toggle('btn-danger', currentState);
            })
            .catch(error => {
                console.error('Error:', error);
                // Revert text and class in case of an error
                newState = currentState; // Revert newState to the original currentState
                this.setAttribute('data-current-state', currentState.toString());
                this.textContent = currentState ? 'Display on Profile' : 'Hide From Profile';
                this.classList.toggle('btn-primary', !currentState);
                this.classList.toggle('btn-danger', currentState);
            });
        });
    });
    


        // getCookie function remains the same
        

        function getCookie(name) {
            let cookieValue = null;
            if (document.cookie && document.cookie !== '') {
                let cookies = document.cookie.split(';');
                for (let i = 0; i < cookies.length; i++) {
                    let cookie = cookies[i].trim();
                    if (cookie.substring(0, name.length + 1) === (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }
            return cookieValue;
        }

});

document.addEventListener('DOMContentLoaded', function() {

    //Quick Notes Update
    const form = document.getElementById('quick-notes-form');
    const notesList = document.getElementById('quick-notes-list');

    form.addEventListener('submit', function(e) {
        e.preventDefault();

        const formData = new FormData(this);

        fetch('/add_quick_note/', {
            method: 'POST',
            body: formData,
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'X-CSRFToken': csrftoken 
            }
        })
        .then(response => {
            return response.json();
        })
        .then(data => {
            if (data.status === 'success') {
                // Create new note element and add it to the list
                const newNote = document.createElement('div');
                newNote.className = 'quick-note';
                newNote.innerHTML = `
                    <p>${data.note}</p>
                    <span class="timestamp">${data.created_at}</span>
                `;
                notesList.prepend(newNote); // Add the new note at the beginning of the list
            } else {
            }
        })
        .catch(error => console.error('Error:', error));
    });

});


window.onload = function() {

    // Modal setup
    var modal = document.getElementById("demoModal");
    var span = document.getElementsByClassName("close")[0];

    // Display the modal by default
    modal.style.display = "block";

    // When the user clicks on <span> (x), close the modal
    span.onclick = function() {
        modal.style.display = "none";
    };

    // Close modal if the  user clicks anywhere outside of it
    window.onclick = function(event) {
        if (event.target === modal) {
            modal.style.display = "none";
        }
    };
};
library_demo_app/static/style.css
/* 
  Book Tracker Demo Version 1.0
  Originally created for Harvard CS50's Web Programming with Python and JavaScript course.
  For more information on this or more projects, please visit: https://example.invalid
  Thank you for visiting!

  
  Note: The CSS for this project was developed incrementally as the project expanded. 
  Currently, the CSS may lack optimal organization due to its gradual evolution.
  
  Future Plans:
  - Refactor the CSS to improve organization and maintainability.
  - Enhance documentation within the CSS by adding detailed comments.
  - Group related styles together for better coherence.
  - Considerations to modularize the CSS into separate files to streamline style management.
*/


:root {
    /* General colors */
    --deep-navy-blue: #003366;
    --warm-beige: white; 
    --rich-maroon: #800000;
    --soft-gold: #ffd700;
    --light-gray: #eaeaea;
    --lighter-navy-blue: #2c3e50b3;


    /* Hover colors */
    --rich-maroon-hover: #700000; 
    --soft-gold-hover: #e6c200; 
    --deep-navy-blue-hover: #00234d; 
    --warm-beige-hover: #f2f2f2; 
}

html, body {
    height: 100%;
    margin: 0;
    padding: 0;
}

header, footer {
    background-color: var(--deep-navy-blue);
    color: white; 
    text-align: center;
}


.main-content {
    background-color: var(--warm-beige);
    color: black;
}
.button-highlight {
    background-color: var(--rich-maroon);
    color: white;
}
.rating-highlight {
    color: var(--soft-gold);
}

h1, h2, h3, h4, h5, h6 {
    font-family: 'Merriweather', serif;
}

.button-highlight:hover {
    background-color: darken(var(--rich-maroon), 10%);
    cursor: pointer;
}

body {
    font-family: 'Open Sans', sans-serif;
    font-size: 16px;
    line-height: 1.6; 
}

h1 { font-size: 2.5em; } 
h2 { font-size: 2em; }
h3 { font-size: 1.75em; }
h4 { font-size: 1.5em; }
h5 { font-size: 1.25em; }
h6 { font-size: 1em; }

.container {
    max-width: 1200px; 
    margin: 0 auto; 
    padding: 0 15px;
}


/* Shared styles for both header and footer for consistency */
.header-footer-container {
    padding: 1rem 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

header .container, footer .container {
    max-width: 1200px; 
    margin: 0 auto;
    width: 100%;
    padding: 0 1rem; 
}

/* Footer specific styles */
footer {
    background-color: var(--deep-navy-blue);
    color: #fff; 

}

footer .container {
    flex-direction: column; 
    text-align: center; 
}

footer nav ul {
    list-style-type: none; /* Remove bullets */
    padding: 0; 
    margin: 1rem 0;
}

footer nav ul li {
    display: inline-block; 
    margin-right: 20px; /
}

footer nav ul li:last-child {
    margin-right: 0; 
}

footer p {
    margin-top: 1rem; 
}

#le-footer {
    color: var(--warm-beige);
}


@media (max-width: 768px) {
    .header-footer-container {
        flex-direction: column;
    }

    header .container, footer .container {
        padding: 0.5rem;
    }

    footer nav ul li {
        display: block; 
        margin: 0.5rem 0; 
    }
}

/* Logo container */

.logo-container {
    margin: 1rem 0;
    padding: 0.5rem;
    display: flex;
    justify-content: center; 
}

.logo {
    height: 50px; 
    width: auto; /* aspect ratio */
}

.user-info {
    padding: 0.5rem 1rem;
    color: var(--warm-beige);
}



input[type='text'],
input[type='search'],
input[type='email'],
input[type='password'],
textarea,
select {
    border: 1px solid #ccc;
    padding: 10px;
    margin-bottom: 15px; 
    width: 100%; 
    border: 1px solid #ccc; 
    border-radius: 4px; 
    box-sizing: border-box; 
}

input[type='submit'],
button {
    background-color: var(--rich-maroon);
    color: white;
    border: none;
    padding: 10px 20px;
    text-transform: uppercase; 
    letter-spacing: 1px; 
    transition: background-color 0.3s ease; 
}

.text-center { text-align: center; }
.hidden { display: none; }

/* Navbar link styling */
nav ul li {
    display: inline-block; 
    margin-right: 20px; 
}

nav ul li a {
    padding: 10px 15px; 
    color: white; 
    text-decoration: none; 
}

nav ul li a:hover,
nav ul li a:focus {
    background-color: var(--rich-maroon); 
    border-radius: 5px;
}


/* seacrh container*/

.search-container {
    width: 100%; 
    padding: 10px; 
    margin-bottom: 15px; 
    box-sizing: border-box;
}

/* Search Results Container */
#search-results {
    display: none; /* inittially hide container */
    border: 1px solid var(--deep-navy-blue);
    background-color: var(--warm-beige);
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    max-height: 300px;
    overflow-y: auto; 
    font-family: 'Open Sans', sans-serif;
    
}

/* Individual Search Result Item */
.search-result-item {
    display: block;
    margin-top: 5px;
    padding: 8px;
    border-bottom: none;
    color: var(--deep-navy-blue);
    text-decoration: none;
    font-size: 14px;
    transition: background-color 0.3s ease; 
}

/* Hover Effect for Search Result Items */
.search-result-item:hover {
    background-color: var(--soft-gold);
    color: var(--deep-navy-blue);
    cursor: pointer;
}



/* General Link Style */
.link-style {
    color: var(--rich-maroon);
    text-decoration: none; 
}

.link-style:hover {
    text-decoration: underline; 
}



/* Home Container */
.home-container {
    padding: 20px;
    background-color: var(--warm-beige);
    text-align: center;
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
}

/* Home Heading */
.home-heading {
    color: var(--deep-navy-blue);
    margin-bottom: 20px;
}

/* Welcome Back and Prompt Login Messages */
.welcome-back, .prompt-login {
    margin-bottom: 20px;
    font-size: 1.2em;
}

/* Authentication Links */
.auth-links {
    display: inline-block;
    margin-top: 20px;
}

.auth-links a {
    margin-right: 10px;
}

.auth-links span {
    color: var(--deep-navy-blue);
}

  section.hero {
    text-align: center;
    margin-bottom: 3rem;
  }
  
  .btn-hero {
    margin-top: 1rem;
  }
  
  section.reading-challenge {
    background: #f9f9f9;
    padding: 2rem;
    border-radius: 10px;
    margin-bottom: 3rem;
  }
  
  section.intro {
    margin-bottom: 3rem;
  }
  
  section.book-display {
    margin-bottom: 3rem;
  }
  
  .swiper-container {
    margin-top: 2rem;
  }
  
  h2 {
    font-size: 2rem;
    color: var(--deep-navy-blue);
    margin-bottom: 1rem;
  }
  
  p {
    font-size: 1rem;
    line-height: 1.6;
    color: var(--rich-maroon);
  }
  




/* Swiper */ 
.swiper-container {
    width: 100%;
    max-width: 1200px; 
    margin: auto; /* Center the carousel */
    overflow: hidden;
}

/* Styling for each slide within the Swiper */
.swiper-slide {
    font-size: 1em;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: flex-start; 
    align-items: center;
    padding: 15px;
    background: #fff; 
    border: 1px solid #ddd;
    border-radius: 10px; 
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Shadow for 3D effect */
    transition: transform 0.3s, box-shadow 0.3s; 
}

.swiper-slide:hover {
    transform: translateY(-5px); /* raise on hover for 3D effect */
    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); 
}

/* Image within the slide */
.swiper-slide img {
    width: auto;
    max-height: 150px; 
    object-fit: cover;
    border-radius: 4px;
    margin-bottom: 10px;
}

/* Swiper pagination bullets */
.swiper-pagination {
    position: relative;
    bottom: 0; 
    width: 100%; 
    text-align: center; 
}

.swiper-pagination-bullet {
    background: var(--deep-navy-blue);
    opacity: 0.7; 
}

.swiper-pagination-bullet-active {
    opacity: 1; 
}

/* Adjustments for text to prevent overflow */
.swiper-slide h3,
.swiper-slide p {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%; 
}

/* Style override for smaller screens */
@media (max-width: 768px) {
    .swiper-slide {
        width: 90%;
        max-height: none;
    }

    .swiper-slide img {
        max-height: 120px; 
    }

    .swiper-slide h3,
    .swiper-slide p {
        font-size: 0.8em; 
        white-space: normal; /* Allow text to wrap */
    }
}

/* Login Container */
.login-container {
    max-width: 500px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Form Heading */
.form-heading {
    text-align: center;
    color: var(--deep-navy-blue);
    margin-bottom: 30px; 
}

/* Login Form */
.login-form p {
    margin-bottom: 15px; 
}

.login-form label,
.login-form input {
    display: block;
    width: 100%;
    margin-bottom: 5px;
    background-color: var(--warm-beige);
}

.login-form input[type='text'],
.login-form input[type='password'] {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px; 
    background-color: var(--warm-beige);
}

/* Style for the forgot password link */
.forgot-password-link,
.signup-link {
    display: block; 
    margin-top: 15px; 
    color: var(--deep-navy-blue); 
    font-size: 0.9em; 
    text-decoration: none; 
}

.forgot-password-link:hover,
.signup-link:hover {
    text-decoration: none;
    color: var(--rich-maroon); 
}



/* Buttons */
.btn {
    padding: 10px 15px;
    text-align: center;
    border: none;
    border-radius: 5px;
    color: white;
    text-decoration: none; 
    display: inline-block; 
    transition: background-color 0.3s ease;
}

.action-btn {
    margin-right: 3px; 
}

.btn-primary {
    background-color: var(--rich-maroon);
}

.btn-primary:hover, .btn-primary:focus {
    background-color: var(--rich-maroon-hover);
}

.btn-secondary {
    background-color: var(--soft-gold);
}

.btn-secondary:hover, .btn-secondary:focus {
    background-color: var(--soft-gold-hover);
}

.btn-danger {
    background-color: var(--deep-navy-blue);
}

.btn-danger:hover, .btn-danger:focus {
    background-color: var(--deep-navy-blue-hover, #0055a5);
}


/* Responsive */
@media (max-width: 600px) {
    .login-container {
        width: 85%;
        margin: 0 auto; /* Center form on smaller screens */
    }
}


/* Add Book Container */
.add-book-container {
    max-width: 600px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Fetch Book Form */
.fetch-book-form label,
.fetch-book-form .isbn-input {
    display: block;
    width: 100%;
    margin-bottom: 10px;
}

.fetch-book-form .isbn-input {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}


/* Cover Image */
.cover-image {
    display: block;
    max-width: 100px; 
    margin: 20px auto; /* Center the image */
    border: 1px solid #ddd; /* Optional border */
    border-radius: 5px;
}

/* Alerts for Messages */
.alert {
    padding: 10px;
    margin: 10px 0;
    border: 1px solid transparent;
    border-radius: 5px;
}

.alert-info {
    background-color: var(--deep-navy-blue);
    color: white;
}

/* Book Details Form */
.book-details-form p {
    margin-bottom: 15px;
}

.book-details-form label,
.book-details-form input,
.book-details-form textarea,
.book-details-form select {
    display: block;
    width: 100%;
    margin-bottom: 5px;
}

/* Responsive */
@media (max-width: 600px) {
    .add-book-container {
        width: 85%;
        margin: 0 auto;
    }
}


/* Book Detail Container */

/* Overall Layout*/
.book-detail-container {
    max-width: 800px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Information and Styling*/
.book-cover {
    max-width: 200px;
    height: auto;
    margin: 0 auto 20px;
    display: block;
}

.book-title, .book-author, .book-isbn, .book-publication-date, .book-pages, .book-genres, .book-summary, .book-read-status {
    margin-bottom: 15px;
    line-height: 1.5;
}

.book-title {
    font-size: 1.75em;
    color: var(--deep-navy-blue);
}

/* Action Buttons*/
.action-buttons {
    margin-top: 20px;
    text-align: center;
}


/* Review Sections */
.reviews-heading, .submit-review-heading {
    color: var(--deep-navy-blue);
    margin-top: 40px;
    font-size: 24px; 
}

.review {
    padding: 15px;
    margin-bottom: 15px; /* More space between reviews */
    background-color: #f8f9fa;
    border: 1px solid #e1e1e1;
    border-radius: 8px; 
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 
}

.review-date {
    font-size: 0.85em;
    color: #666;
    text-align: right; /* Align date to the right */
    margin-top: 10px; 
}

.no-reviews {
    text-align: center;
    color: #666;
    margin-top: 20px;
}

/* Review Form */
.review-form {
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.review-form label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}

.review-form input[type="radio"] {
    margin-right: 10px;
}

.review-form textarea {
    width: 50%; 
    padding: 8px; 
    border: 1px solid #ccc; 
    border-radius: 4px; 
    min-height: 100px; 
    resize: vertical;
}


/* Add styles for any errors */
.errorlist {
    color: red;
    font-size: 0.85em;
}

.error-visible {
    color: red;
    /* Add any other styles */
}

#passwordError {
    display: none;
    color: #D8000C;
    background-color: #FFD2D2; 
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #D8000C;
    border-radius: 5px;
    font-size: 1rem; 
    text-align: center;
    display: none; 
}

.hidden {
    display: none;
}


.btn-link {
    background-color: transparent;
    color: var(--rich-maroon);
    text-decoration: underline;
}


/* Responsive Design */
@media (max-width: 768px) {
    .book-detail-container {
        width: 90%;
        margin: 20px auto;
    }

    .book-cover {
        max-width: 150px;
    }
}


/* Progress Bar Container */
.progress-bar-container {
    width: 100%;
    background: #ccc;
    margin-top: 10px;
}

/* Progress Bar */
.progress-bar {
    height: 20px;
    background: var(--deep-navy-blue);
    transition: width 0.3s ease; /* Smooth transition for width changes */
}

/* Reading Progress Input - initially hidden, will be displayed via JS */
.reading-progress-input {
    display: none;
}

/* Toggle progress via JS */
.reading-progress-input.show {
    display: block;
}



/* Dashboard Container */
.dashboard-container {
    width: 90%;
    margin: auto;
    padding: 20px;
    background-color: #f4f4f4;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Dashboard Grid */
.dashboard-grid {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 0.6fr;
    grid-gap: 20px;
    overflow: hidden;
}

/* User Profile Section */
.user-profile {
    display: flex;
    flex-direction: column;
    justify-content: space-between; 
    grid-column: 1;
    background-color: #fff;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    text-align: center;
}


.user-profile .profile-photo {
    border-radius: 50%;
    align-self: center;
    width: 100px;
    height: 100px;
    border-radius: 50%;
}

/* Profiles Container */
.profiles-container {
    grid-column: 2/ 4;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    grid-gap: 15px;
}


.profile-card {
    display: flex;
    flex-direction: column;
    justify-content: space-between; 
    background-color: #fff;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.profile-card .profile-photo {
    border-radius: 50%;
    align-self: center;
    width: 80px;
    height: 80px;
    border-radius: 50%;
    margin-bottom: 10px;
}

/* Quick Notes Section: Might need adjusment, TODO */
.quick-notes {
 
    background-color: #fff;
    grid-column: 4;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.quick-notes-list {
    max-height: 500px; 
    overflow-y: auto;
    word-wrap: break-word;
 
}


.continue-reading {
    grid-column: 1 / 5;
    margin-top: 20px;
    background-color: #fff;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.in-progress-books {
    display: flex;
    flex-wrap: wrap;
    gap: 1em;
}

.in-progress-book-card {
    border: 1px solid #ddd;
    border-radius: 5px;
    overflow: hidden;
    width: 180px; 
    text-align: center;
    text-decoration: none;
}

.in-progress-book-card:hover {
    transform: scale(1.03); 
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); 
}


.book-cover-reading {
    max-width: 100%;
    height: auto;
    display: block;
}

/* DELETED FOR aesthetic reasons
.book-title-reading {
    padding: 5px;
    font-size: 1em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
*/

/* Responsive Styles */
@media only screen and (max-width: 768px) {
    /* For tablets and smaller devices */
    .dashboard-grid {
        grid-template-columns: 1fr; /* Stacks the columns on smaller screens */
    }

    .user-profile, .profiles-container, .quick-notes, .continue-reading {
        grid-column: 1 / 2; 
    }

    .profile-card, .user-profile {
        padding: 10px;
    }

    .profile-card .profile-photo, .user-profile .profile-photo {
        width: 60px; 
        height: 60px;
    }
}

@media only screen and (max-width: 480px) {
    /* For mobile devices */
    .dashboard-container {
        width: 100%;
        padding: 10px;
    }

    .profile-card .profile-photo, .user-profile .profile-photo {
        width: 50px; 
        height: 50px;
    }
}


/* Signup Container */
.signup-container {
    max-width: 500px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Form Heading */
.form-heading {
    text-align: center;
    color: var(--deep-navy-blue);
    margin-bottom: 30px;
}

/* Signup Form */
.signup-form p {
    margin-bottom: 15px; 
}

.signup-form label,
.signup-form input {
    display: block;
    width: 100%;
    margin-bottom: 5px;
}

.signup-form input[type='text'],
.signup-form input[type='email'],
.signup-form input[type='password'] {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}



/* Container for the book list */
.book-list-container {
   text-align: center;
    padding: 20px;
  }
  
/* Styling for the books container */
.books {
display: flex;
flex-wrap: wrap;
gap: 20px; 
}
  
 /* Styling for the book list */
.book-list {
    list-style-type: none; 
    padding: 0; 
    margin: 0; 
    width: 100%; 
    display: flex;
    flex-wrap: wrap; 
}

/* Styling for each book item */
.book-item {
    flex: 0 0 calc(50% - 20px); /* 2 items per row */
    border: 1px solid #ddd; 
    padding: 15px;
    margin: 10px;
    border-radius: 5px; 
    background-color: #f9f9f9; 
    box-sizing: border-box; 
}
  
  /* Book cover image styling */
  .book-cover {
    width: 100%;
    height: auto;
    margin-bottom: 10px;
  }
  
/* Responsive */
@media (max-width: 768px) {
    .book-item {
        flex: 0 0 calc(50% - 20px); /* 2 items per row */
    }
}

@media (max-width: 480px) {
    .book-item {
        flex: 0 0 calc(100% - 20px);
    }
}
  
  /* Postdated styling for book details, needs integration TODO */
  .book-details {
    text-align: center;
  }
  
/* Read Status Form */
.read-status-form {
    margin-top: 10px;
}

.read-status-form .form-group,
.read-status-form .progress-bar-container,
.read-status-form button[type="submit"] {
    max-width: 300px; 
    margin: 10px auto; 
    display: block;
}

.read-status-form button[type="submit"] {
    width: 100%; 
    box-sizing: border-box; 
}


/* No Books Message */
.no-books-message {
    text-align: center;
    color: #666;
}


/* Action Links */
.book-actions a {
    margin-right: 10px;
}
/* Responsive Design */
@media (max-width: 768px) {
    .book-cover {
        max-width: 80px;
    }
}
  
/* Confirm Delete Container */
.confirm-delete-container {
    max-width: 500px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
    text-align: center;
}

/* Page Heading */
.page-heading {
    color: var(--deep-navy-blue);
    margin-bottom: 20px;
}

/* Confirmation Text */
.confirmation-text, .note-text {
    margin-bottom: 20px;
    line-height: 1.5;
}

/* Delete Form */
.delete-form {
    margin-bottom: 20px;
}

/* Edit Book Container */
.edit-book-container {
    max-width: 600px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Form Heading */
.form-heading {
    text-align: center;
    color: var(--deep-navy-blue);
    margin-bottom: 30px;
}

/* Edit Book Form */
.edit-book-form p {
    margin-bottom: 15px; 
}

.edit-book-form label,
.edit-book-form input,
.edit-book-form textarea,
.edit-book-form select {
    display: block;
    width: 100%;
    margin-bottom: 5px;
}

.edit-book-form input[type='text'],
.edit-book-form input[type='email'],
.edit-book-form input[type='password'],
.edit-book-form textarea {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}


/* Edit Profile Container */
.edit-profile-container {
    max-width: 600px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Form Heading */
.form-heading {
    text-align: center;
    color: var(--deep-navy-blue);
    margin-bottom: 30px;
}

/* Edit Profile Form */
.edit-profile-form p {
    margin-bottom: 15px; 
}

.edit-profile-form label,
.edit-profile-form input,
.edit-profile-form textarea,
.edit-profile-form select {
    display: block;
    width: 100%;
    margin-bottom: 5px;
}

.edit-profile-form input[type='text'],
.edit-profile-form input[type='email'],
.edit-profile-form input[type='password'],
.edit-profile-form textarea {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}



/* Search Results Container */
.search-results-container {
    padding: 20px;
    background-color: var(--warm-beige);
    
}

/* Page Heading */
.page-heading {
    color: var(--deep-navy-blue);
    text-align: center;
    margin-bottom: 20px;
}

/* INPUTS */

/* Styling for number inputs with custom-number-input class */
input[type='number'].custom-number-input {
    -webkit-appearance: none;
    -moz-appearance: textfield;
    appearance: none;
    border: 1px solid #ccc; 
    border-radius: 4px; 
    padding: 8px 12px;
    font-size: 16px; 
    color: #333; 
    width: 40px; 
}

/* Remove the spin button from custom-number-input */
input[type='number'].custom-number-input::-webkit-inner-spin-button,
input[type='number'].custom-number-input::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* hover and focus states of custom-number-input */
input[type='number'].custom-number-input:hover,
input[type='number'].custom-number-input:focus {
    border-color: #007bff;
    outline: none;
}


.hidden {
    display: none; /* ??? */
}

.visible {
    display: block;
}

/* Styling for select dropdown */
.read-status-selector {
    background-color: white; 
    border: 1px solid #ccc; 
    border-radius: 4px; 
    padding: 8px 12px; 
    font-size: 16px; 
    color: #333; 
    -webkit-appearance: none; 
    -moz-appearance: none; 
    appearance: none; 
    cursor: pointer; 
}

/* Add a custom arrow indicator */
.read-status-selector {
    background: white; 
    background-size: 12px 12px; 
    padding-right: 30px; 
}

/* Styling for the read-status-selector */
.read-status-selector {
    width: 50%;
    margin: 0 auto; 
}

/* Styling for the progress bar container */
.progress-bar-container {
    width: 50%; 
    margin: 0 auto; 
    background-color: #e0e0e0; 
    border-radius: 4px; 
}

/* Styling for the progress bar */
.progress-bar {
    height: 20px;
    background-color: var(--deep-navy-blue); /* Progress bar color */
    border-radius: 4px;
    transition: width 0.3s ease; 
}




/* Explore Container */
.explore-books-container {
    padding: 20px;
}

.books-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 20px;
}

.book-card {
    border: 1px solid #ddd;
    padding: 10px;
    text-align: center;
    transition: transform 0.3s ease, box-shadow 0.3s ease; 
    cursor: pointer; 
    text-decoration: none; 
    color: inherit; /* Color consitency: rest of the card */
}

.book-card:hover {
    transform: scale(1.05); /* Scales book card */
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); 
}

.book-card img {
    max-width: 100%;
    height: auto;
    margin-bottom: 10px;
}

.book-title,
.book-author {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
    display: -webkit-box;
    -webkit-line-clamp: 2;  /* Needs revision */
    -webkit-box-orient: vertical;
    margin: 0; 
    padding: 0; 
    
}

.book-title {
    font-size: 1em; 
    margin-bottom: 0.5em;
}

.book-author {
    font-size: 0.85em; 
    color: var(--rich-maroon);
}


/* Profile COntainer */

.profile-container {
    max-width: 800px;
    margin: auto;
    padding: 20px;
    text-align: center;
}

.profile-card-profile {
    background-color: #f3f3f3;
    border-radius: 10px;
    padding: 20px;
    display: flex;
    align-items: center;
    margin-bottom: 30px;
}

.profile-photo-profile {
    max-width: 150px;
    border-radius: 50%;
    margin-right: 20px;
}

.profile-info p {
    margin: 0;
    line-height: 1.5;
}

.book-list-profile {
    margin-top: 20px;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}

.book-entry-profile {
    background-color: #fff;
    border: 1px solid #ddd;
    padding: 15px;
    border-radius: 8px;
    margin-bottom: 10px;
    flex-basis: calc(25% - 10px); /* 4 books per row */
    box-sizing: border-box;
}

@media (max-width: 1024px) {
    .book-entry-profile {
        flex-basis: calc(50% - 10px);
    }
}

@media (max-width: 768px) {
    .book-entry-profile {
        flex-basis: 100%; 
    }
}

.footer-a {
    color: var(--warm-beige-hover);
    text-decoration: none;
    font-weight: bold;
}

.footer-a:hover {
    color: var(--rich-maroon);
}

/* Modal Styling */

.modal {
    display: none; 
    position: fixed; 
    z-index: 1; 
    left: 0;
    top: 0;
    width: 100%; 
    height: 100%;
    overflow: auto; 
    background-color: rgb(0,0,0); 
    background-color: rgba(0,0,0,0.4);
}

.modal-content {
    background-color: #fefefe;
    margin: 15% auto; 
    padding: 20px;
    border: 1px solid #888;
    width: 80%; 
    border-radius: 10px;
}

.close {
    color: #aaa;
    float: right;
    font-size: 28px;
    font-weight: bold;
}

.close:hover,
.close:focus {
    color: black;
    text-decoration: none;
    cursor: pointer;
}

.modal-a {
    color: var(--deep-navy-blue);
    text-decoration: none;
    font-weight: bold;
}

.modal-a:hover {
    color: var(--rich-maroon);
}

/* Demo credentials, loging.html */

.demo-credentials {
    padding: 5px;
    position: relative; 
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin: 0 auto; 
    width: 80%; 
    top: 20px; 
    max-width: 500px;
    margin: 30px auto;
    padding: 20px;
    background-color: var(--warm-beige);
    border-radius: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.demo-credentials h2 {
    text-align: center;
}

/* Reponsive credential info */
@media (max-width: 600px) {
    .demo-credentials {
        top: 10px;
        width: 85%; 
    }
}

/* End of The Book Tracker Demo Version CSS */
library_demo_app/templates/tracker/dashboard.html
{% extends 'base.html' %}

{% block content %}
<div class="dashboard-container">
    <!-- Display messages if there are any -->
    {% if messages %}
    <ul class="messages">
        {% for message in messages %}
        <li class="{{ message.tags }}">{{ message }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <!-- Main grid container for profiles -->
    <div class="dashboard-grid">
        <!-- Main User Profile Section -->
        <div class="user-profile my-profile">
            {% if user.profile.profile_photo %}
                <img src="{{ user.profile.profile_photo.url }}" alt="Profile photo" class="profile-photo" />
            {% endif %}
            <h3>My Dashboard</h3>
            {% if user.profile %}
            <p class="bio"><strong>Bio:</strong> {{ user.profile.bio }}</p>
            <p class="location"><strong>Location:</strong> {{ user.profile.location }}</p>
            <p class="birthdate"><strong>Birth Date:</strong> {{ user.profile.birth_date|date:"Y-m-d" }}</p>
            {% endif %}
            <!-- Links to user actions -->
            <div class="book-links">
                <a href="{% url 'book_list' %}" class="btn btn-primary">My Books</a>
                <a href="{% url 'add_book' %}" class="btn btn-secondary">New Book</a>
                <a href="{% url 'edit_profile' %}" class="btn btn-primary">Edit Profile</a>
            </div>
        </div>
    
        <!-- Grid container for other profiles -->
        <div class="profiles-container">
            {% for profile_user in random_profiles %}
            <!-- Individual profile card -->
            <div class="profile-card">
                {% if profile_user.profile.profile_photo %}
                    <img src="{{ profile_user.profile.profile_photo.url }}" alt="Profile photo" class="profile-photo" />
                {% endif %}
                <h4>{{ profile_user.username }}</h4>
                <p class="bio"><strong>Bio:</strong> {{ profile_user.profile.bio }}</p>
                <p class="location"><strong>Location:</strong> {{ profile_user.profile.location }}</p>
                <p class="birthdate"><strong>Birth Date:</strong> {{ profile_user.profile.birth_date|date:"Y-m-d" }}</p>
                <a href="{% url 'user_profile' profile_user.username %}" class="btn btn-danger">View Profile</a>
            </div>
            {% endfor %}
        </div>  

        <!-- Quick Notes Section -->
        <div class="quick-notes-section">
            <h3>Quick Notes</h3>
            <form id="quick-notes-form" method="post">
                {% csrf_token %}
                {{ note_form.as_p }}
                <button type="submit">Add Note</button>
            </form>

            <!-- Display the quick notes -->
            <div class="quick-notes-list" id="quick-notes-list">
                {% for note in quick_notes %}
                    <div class="quick-note">
                        <p>{{ note.note }}</p>
                        <span class="timestamp">{{ note.created_at }}</span>
                    </div>
                {% empty %}
                    <p>No notes yet.</p>
                {% endfor %}
            </div>
        </div>


        <div class="continue-reading">
            <h3>Books You Are Currently Reading...</h3>
            <div class="in-progress-books">
                {% for user_book in in_progress_books %}
                    <a href="{% url 'book_detail' user_book.book.id %}" class="in-progress-book-card">
                        {% if user_book.book.cover_image %}
                            <img src="{{ user_book.book.cover_image.url }}" alt="{{ user_book.book.title }}" class="book-cover-reading" />
                        {% endif %}
                        <!-- <h4 class="book-title-reading">{{ user_book.book.title }}</h4> -->
                    </a>
                {% empty %}
                    <p>You have no books in progress.</p>
                {% endfor %}
            </div>
        </div>
        
        

    </div>

    

    
    
</div>
{% endblock %}
library_demo_app/templates/tracker/book_list.html
{% extends 'base.html' %}

{% block content %}
<div class="book-list-container">
  <h2 class="page-heading">My Books</h2>
  <!-- <a href="{% url 'add_book' %}" class="link-style">Add a new book</a> -->
  <a href="{% url 'add_book' %}" class="btn btn-secondary">Add a New Book</a>
  <div class="books">
    {% if user_books %}
      <ul class="book-list">
        {% for user_book in user_books %}
          <li class="book-item">
            <!-- Book Cover Image -->
            {% if user_book.book.cover_image %}
                <img src="{{ user_book.book.cover_image.url }}" alt="Cover image for {{ user_book.book.title }}" class="book-cover" />
            {% endif %}

            <!-- Book Details -->
            <div class="book-details">
              <h3>{{ user_book.book.title }}</h3>
              <p class="author"><strong>Author:</strong> {{ user_book.book.author }}</p>
              <p class="isbn"><strong>ISBN:</strong> {{ user_book.book.isbn }}</p>
              <p class="publication-date"><strong>Publication Date:</strong> {{ user_book.book.publication_date|date:"Y-m-d" }}</p>
              <p class="genres"><strong>Genres:</strong> 
                {% for genre in user_book.book.genres.all %}
                    {{ genre.name }}{% if not forloop.last %}, {% endif %}
                {% endfor %}
              </p>
              <p class="pages"><strong>Pages:</strong> {{ user_book.book.pages }}</p>
              <p class="summary"><strong>Summary:</strong> {{ user_book.book.summary }}</p>
              <p class="read-status"><strong>Read:</strong> {{ user_book.read|yesno:"Yes,No" }}</p>

             
              <!-- Read Status Form -->
              <div class="read-status-form">
                <form id="read-status-form" method="post" action="{% url 'update_read_status' user_book.id %}">
                    {% csrf_token %}
                    <div class="form-group">
                        <label for="read-status-selector_{{ user_book.id }}">Update Read Status:</label>
                        <select id="read-status-selector_{{ user_book.id }}" name="status" class="read-status-selector">
                            <option value="Not Started" {% if not user_book.read and user_book.progress == 0 %} selected {% endif %}>Not Started</option>
                            <option value="In Progress" {% if not user_book.read and user_book.progress > 0 %} selected {% endif %}>In Progress</option>
                            <option value="Completed" {% if user_book.read %} selected {% endif %}>Completed</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="progress_{{ user_book.id }}">Progress (%):</label>
                        <input type="number" class="custom-number-input reading-progress-input" name="progress" id="progress_{{ user_book.id }}" value="{{ user_book.progress }}" min="0" max="100">
                    </div>
                    <div id="progress-bar-container_{{ user_book.id }}" class="progress-bar-container">
                        <div id="progress-bar_{{ user_book.id }}" class="progress-bar" style="width: {{ user_book.progress }}%;"></div>
                    </div>
                    <button type="submit" class="btn btn-secondary">Update</button>
                </form>
            </div>
            



              <!-- Action Links -->
              <div class="book-actions">
                <a href="{% url 'book_detail' user_book.book.id %}" class="btn btn-primary">View details</a>
                <a href="{% url 'book_edit' user_book.id %}" class="btn btn-primary">Edit</a>
                <a href="{% url 'book_delete' user_book.id %}" class="btn btn-danger">Remove</a>

                <a href="javascript:void(0);" 
                  class="btn toggle-visibility {% if user_book.hide_from_profile %}btn-primary{% else %}btn-danger{% endif %}" 
                  data-book-id="{{ user_book.id }}"
                  data-current-state="{{ user_book.hide_from_profile|lower }}">
                    {% if user_book.hide_from_profile %}
                        Display
                    {% else %}
                        Hide
                    {% endif %}
                </a>

              
              </div>
            </div>
          </li>
        {% endfor %}
      </ul>
    {% else %}
      <p class="no-books-message">You have no books in your tracker.</p>
    {% endif %}
  </div>
</div>
{% endblock %}
library_demo_app/templates/tracker/book_detail.html
{% extends 'base.html' %}

{% block content %}
<div class="book-detail-container">
  <h2 class="form-heading">Book Details</h2>

  <div class="book-info">
      {% if book.cover_image %}
          <img src="{{ book.cover_image.url }}" alt="Cover image for {{ book.title }}" class="book-cover" />
      {% endif %}
      <h3 class="book-title">{{ book.title }}</h3>
      <p class="book-author"><strong>Author:</strong> {{ book.author }}</p>
      <p class="book-isbn"><strong>ISBN:</strong> {{ book.isbn }}</p>
      <p class="book-publication-date"><strong>Publication Date:</strong> {{ book.publication_date|date:"Y-m-d" }}</p>
      <p class="book-pages"><strong>Pages:</strong> {{ book.pages }}</p>
      <p class="book-genres"><strong>Genres:</strong> 
        {% for genre in book.genres.all %}
            {{ genre.name }}{% if not forloop.last %}, {% endif %}
        {% endfor %}
      </p>
      <p class="book-summary"><strong>Summary:</strong> {{ book.summary }}</p>
      <p class="book-read-status"><strong>Read:</strong> {{ user_book.read|yesno:"Yes,No" }}</p>

      <div class="action-buttons">
        <a href="{% url 'generate_book_card' book.id %}" class="btn btn-secondary">Generate Shareable Card</a>
    
        {% if user_book %}
            <!-- Show Edit and Remove only if the user has this book in their list -->
            <a href="{% url 'book_edit' user_book.id %}" class="btn btn-primary">Edit</a>
            <a href="{% url 'book_delete' user_book.id %}" class="btn btn-danger">Remove</a>
        {% else %}
            <!-- Add to My List button for AJAX call -->
            <!-- Add to My List link for AJAX call -->
            <a href="javascript:void(0);" class="btn btn-primary add-book-btn" data-book-id="{{ book.id }}">Add to My List</a>


        {% endif %}
    </div>
  </div>

  <h3 class="reviews-heading">Reviews</h3>
  <div class="reviews">
      {% if reviews %}
          {% for review in reviews %}
              <div class="review">
                  <p><strong>{{ review.user.username }}:</strong> {{ review.comment }} (Rating: {{ review.rating }})</p>
                  <p class="review-date">Date: {{ review.created_at|date:"Y-m-d" }}</p>
              </div>
          {% endfor %}
      {% else %}
          <p class="no-reviews">No reviews yet.</p>
      {% endif %}
  </div>

  {% if user_book.read and not has_reviewed %}
      <h3 class="submit-review-heading">Submit a Review</h3>
     
        <form method="post" action="{% url 'submit_review' user_book.id %}" class="review-form" data-user-book-id="{{ user_book.id }}">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit" class="btn btn-primary">Submit Review</button>
        </form>

  {% elif user_book.read %}
      <p class="review-status">You have already submitted a review for this book.</p>
  {% endif %}

  <a href="{% url 'book_list' %}" class="link-style">Back to Book List</a>

</div>
{% endblock %}
library_demo_app/templates/tracker/explore_books.html
{% extends 'base.html' %}

{% block content %}
<div class="explore-books-container">
    <h1>Explore Books</h1>
    <div class="books-grid">
        {% for book in books %}
        <a href="{% url 'book_detail' book.id %}" class="book-card">
            <img src="{{book.cover_image.url}}" alt="{{book.title}}" class="book-cover">
            <h3 class="book-title">{{book.title}}</h3>
            <p class="book-author">{{book.author}}</p>
        </a>
        {% empty %}
        <p>No books to display.</p>
        {% endfor %}
    </div>
</div>
{% endblock %}
Dockerfile
# Use an official lightweight Python image.
FROM python:3.10-slim

# Prevent Python from writing pyc files to disk (optional).
ENV PYTHONDONTWRITEBYTECODE 1

# Prevent Python from buffering stdout and stderr (optional).
ENV PYTHONUNBUFFERED 1

# Set the working directory in the container to /code.
WORKDIR /code

RUN mkdir -p /code/static /code/media

# Install pip requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application's code into the container
COPY . .

# Collect static files
RUN python manage.py collectstatic --no-input && ls /code/static

# Specify the port number the container should expose
EXPOSE 8000

# Command to run the application using Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "library_demo_project.wsgi:application"]
docker-compose.yml
version: '3.8'

services:
  library_demo_web:
    build: .
    command: gunicorn library_demo_project.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - .:/code
      - library_static_volume:/code/staticfiles
      - library_media_volume:/code/media
    environment:
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
    networks:
      - hubstation

  library_demo_nginx:
    image: nginx:1.21-alpine
    ports:
      - "8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - library_static_volume:/usr/share/nginx/html/static
      - library_media_volume:/usr/share/nginx/html/media
    depends_on:
      - library_demo_web
    networks:
      - hubstation

networks:
  hubstation:
    external: true

volumes:
  library_static_volume:
  library_media_volume:
nginx.conf
server {
    listen 80;

    location /static/ {
        alias /usr/share/nginx/html/static/;
        expires 7d;
    }

    location /media/ {
        alias /usr/share/nginx/html/media/;
        expires 7d;
    }

    location / {
        proxy_pass http://library_demo_web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
requirements.txt
# Library Demo App Requirements
gunicorn==20.1.0
asgiref==3.8.1  # Higher version from script requirements
certifi==2024.2.2
charset-normalizer==3.3.2
Django==5.0.3
django-environ==0.11.2
idna==3.6
imgkit==1.2.3
Pillow==10.2.0  #  if using higher version from script
requests==2.31.0
six==1.16.0
sqlparse==0.5.1  # Higher version from script requirements
urllib3==2.2.1

# Additional Script Requirements
Faker==22.0.0
pytest==7.4.4
pytest-django==4.7.0
python-dateutil==2.9.0.post0

Back to Home