Upload 28 files
Browse files- core/data/movies.csv +0 -0
- core/data/processed_movies.csv +0 -0
- core/data/processed_movies_with_posters.csv +0 -0
- core/misc/add_images.py +31 -0
- core/misc/data_processing.py +23 -0
- core/model/cosine_sim.pkl +3 -0
- docker/Dockerfile +23 -0
- docker/deploy/Dockerfile +18 -0
- docker/docker-compose.yml +16 -0
- main.py +167 -0
- requirements.txt +32 -0
- static/css/bottom_nav.css +46 -0
- static/css/card.css +54 -0
- static/css/flash_message.css +40 -0
- static/css/form.css +79 -0
- static/css/gototop.css +36 -0
- static/css/navbar.css +104 -0
- static/css/style.css +194 -0
- static/js/base2.js +32 -0
- static/js/main.js +57 -0
- templates/base.html +135 -0
- templates/base2.html +58 -0
- templates/filter.html +49 -0
- templates/login.html +32 -0
- templates/movie_details.html +46 -0
- templates/recommendation.html +41 -0
- templates/register.html +32 -0
- templates/search.html +36 -0
core/data/movies.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
core/data/processed_movies.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
core/data/processed_movies_with_posters.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
core/misc/add_images.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import requests
|
| 3 |
+
|
| 4 |
+
# Load the movies dataset
|
| 5 |
+
movies = pd.read_csv('processed_movies.csv')
|
| 6 |
+
|
| 7 |
+
# TMDb API key and base URL
|
| 8 |
+
api_key = '7283d6e4bfd781f23c42795dcfe9b378'
|
| 9 |
+
base_url = 'https://api.themoviedb.org/3'
|
| 10 |
+
poster_base_url = 'https://image.tmdb.org/t/p/w500'
|
| 11 |
+
|
| 12 |
+
def fetch_poster_url(title):
|
| 13 |
+
search_url = f"{base_url}/search/movie?api_key={api_key}&query={title}"
|
| 14 |
+
response = requests.get(search_url).json()
|
| 15 |
+
results = response.get('results', [])
|
| 16 |
+
if results:
|
| 17 |
+
poster_path = results[0].get('poster_path')
|
| 18 |
+
if poster_path:
|
| 19 |
+
return poster_base_url + poster_path
|
| 20 |
+
return None
|
| 21 |
+
|
| 22 |
+
# Fetch and add poster URLs
|
| 23 |
+
poster_urls = []
|
| 24 |
+
for title in movies['title']:
|
| 25 |
+
poster_url = fetch_poster_url(title)
|
| 26 |
+
poster_urls.append(poster_url)
|
| 27 |
+
|
| 28 |
+
movies['poster_url'] = poster_urls
|
| 29 |
+
|
| 30 |
+
# Save the updated dataset
|
| 31 |
+
movies.to_csv('processed_movies_with_posters.csv', index=False)
|
core/misc/data_processing.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 3 |
+
from sklearn.metrics.pairwise import linear_kernel
|
| 4 |
+
import joblib
|
| 5 |
+
|
| 6 |
+
# Load the dataset
|
| 7 |
+
movies = pd.read_csv('movies.csv')
|
| 8 |
+
|
| 9 |
+
# Convert the titles to lowercase
|
| 10 |
+
movies['title'] = movies['title'].str.lower()
|
| 11 |
+
|
| 12 |
+
# Preprocess the dataset
|
| 13 |
+
movies['overview'] = movies['overview'].fillna('')
|
| 14 |
+
tfidf = TfidfVectorizer(stop_words='english')
|
| 15 |
+
tfidf_matrix = tfidf.fit_transform(movies['overview'])
|
| 16 |
+
|
| 17 |
+
# Compute the cosine similarity matrix
|
| 18 |
+
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
|
| 19 |
+
|
| 20 |
+
# Save the cosine similarity matrix and movies DataFrame
|
| 21 |
+
joblib.dump(cosine_sim, 'cosine_sim.pkl')
|
| 22 |
+
movies.to_csv('processed_movies.csv', index=False)
|
| 23 |
+
|
core/model/cosine_sim.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6486acfe3b59d00a90c5c8f63d94ee0759b42590a9ea4b3be916b41c0a26e13c
|
| 3 |
+
size 800000241
|
docker/Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official Python image from Docker Hub as the base image
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory inside the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy all the contents from your local project directory to the container
|
| 8 |
+
COPY . /app
|
| 9 |
+
|
| 10 |
+
# Install the dependencies listed in requirements.txt
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Expose the port Flask will run on (default: 5000)
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
# Set environment variable to load secrets from .env file
|
| 17 |
+
ENV FLASK_APP=main.py
|
| 18 |
+
ENV FLASK_RUN_HOST=0.0.0.0
|
| 19 |
+
ENV FLASK_RUN_PORT=7860
|
| 20 |
+
ENV FLASK_ENV=development
|
| 21 |
+
|
| 22 |
+
# Run the Flask app
|
| 23 |
+
CMD ["flask", "run"]
|
docker/deploy/Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY . /app
|
| 7 |
+
|
| 8 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
|
| 10 |
+
ENV FLASK_APP=main.py
|
| 11 |
+
ENV FLASK_RUN_HOST=0.0.0.0
|
| 12 |
+
ENV FLASK_RUN_PORT=7860
|
| 13 |
+
|
| 14 |
+
CMD ["gunicorn","-b", "0.0.0.0:7860", "main:app"]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
docker/docker-compose.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
flask-app:
|
| 5 |
+
build: .
|
| 6 |
+
ports:
|
| 7 |
+
- "7860:7860"
|
| 8 |
+
env_file:
|
| 9 |
+
- .env
|
| 10 |
+
volumes:
|
| 11 |
+
- .:/app
|
| 12 |
+
environment:
|
| 13 |
+
- FLASK_APP=main.py
|
| 14 |
+
- FLASK_RUN_HOST=0.0.0.0
|
| 15 |
+
- FLASK_RUN_PORT=7860
|
| 16 |
+
- FLASK_ENV=development
|
main.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, redirect, url_for, session, flash
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import joblib
|
| 4 |
+
from fuzzywuzzy import process
|
| 5 |
+
from flask_bcrypt import Bcrypt
|
| 6 |
+
from functools import wraps
|
| 7 |
+
import os
|
| 8 |
+
from supabase import create_client, Client
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
app = Flask(__name__)
|
| 15 |
+
app.secret_key = os.getenv("SECRET_KEY")
|
| 16 |
+
bcrypt = Bcrypt(app)
|
| 17 |
+
|
| 18 |
+
SUPABASE_URL = os.getenv("URL")
|
| 19 |
+
SUPABASE_KEY = os.getenv("KEY")
|
| 20 |
+
|
| 21 |
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 22 |
+
|
| 23 |
+
movies = pd.read_csv('core/data/processed_movies_with_posters.csv')
|
| 24 |
+
cosine_sim = joblib.load('core/model/cosine_sim.pkl')
|
| 25 |
+
|
| 26 |
+
def login_required(f):
|
| 27 |
+
@wraps(f)
|
| 28 |
+
def decorated_function(*args, **kwargs):
|
| 29 |
+
if 'logged_in' not in session:
|
| 30 |
+
flash('Please log in to access this page.', 'warning')
|
| 31 |
+
return redirect(url_for('login'))
|
| 32 |
+
return f(*args, **kwargs)
|
| 33 |
+
return decorated_function
|
| 34 |
+
|
| 35 |
+
@app.route('/login', methods=['GET', 'POST'])
|
| 36 |
+
def login():
|
| 37 |
+
if request.method == 'POST':
|
| 38 |
+
username = request.form['username']
|
| 39 |
+
password = request.form['password']
|
| 40 |
+
|
| 41 |
+
response = supabase.table('users').select('*').eq('username', username).execute()
|
| 42 |
+
if response.data:
|
| 43 |
+
user = response.data[0]
|
| 44 |
+
if bcrypt.check_password_hash(user['password'], password):
|
| 45 |
+
session['logged_in'] = True
|
| 46 |
+
session['username'] = username
|
| 47 |
+
return redirect(url_for('home'))
|
| 48 |
+
|
| 49 |
+
flash('Invalid username or password.', 'warning')
|
| 50 |
+
|
| 51 |
+
return render_template('login.html')
|
| 52 |
+
|
| 53 |
+
@app.route('/register', methods=['GET', 'POST'])
|
| 54 |
+
def register():
|
| 55 |
+
if request.method == 'POST':
|
| 56 |
+
username = request.form['username']
|
| 57 |
+
password = bcrypt.generate_password_hash(request.form['password']).decode('utf-8')
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
response = supabase.table('users').insert({"username": username, "password": password}).execute()
|
| 61 |
+
if not response.data:
|
| 62 |
+
flash('Username already exists. Please choose a different one.', 'warning')
|
| 63 |
+
else:
|
| 64 |
+
flash('Registration successful! You can now log in.', 'success')
|
| 65 |
+
return redirect(url_for('login'))
|
| 66 |
+
except Exception as e:
|
| 67 |
+
if "duplicate key value violates unique constraint" in str(e):
|
| 68 |
+
flash(f"Username already exits", 'warning')
|
| 69 |
+
else:
|
| 70 |
+
flash(f"An error occurred: {str(e)}", 'warning')
|
| 71 |
+
|
| 72 |
+
return render_template('register.html')
|
| 73 |
+
|
| 74 |
+
@app.route('/logout')
|
| 75 |
+
@login_required
|
| 76 |
+
def logout():
|
| 77 |
+
session.clear()
|
| 78 |
+
flash('You have been logged out.', 'info')
|
| 79 |
+
return redirect(url_for('login'))
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def get_recommendations(title, cosine_sim=cosine_sim):
|
| 83 |
+
title = title.lower()
|
| 84 |
+
if title not in movies['title'].str.lower().values:
|
| 85 |
+
close_matches = process.extract(title, movies['title'].str.lower().values, limit=5)
|
| 86 |
+
return None, [movies[movies['title'].str.lower() == match[0]].iloc[0] for match in close_matches]
|
| 87 |
+
idx = movies[movies['title'].str.lower() == title].index[0]
|
| 88 |
+
sim_scores = list(enumerate(cosine_sim[idx]))
|
| 89 |
+
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
|
| 90 |
+
sim_scores = sim_scores[1:11]
|
| 91 |
+
movie_indices = [i[0] for i in sim_scores]
|
| 92 |
+
return movies.iloc[movie_indices], None
|
| 93 |
+
|
| 94 |
+
def get_recommendations_by_id(movie_id, cosine_sim=cosine_sim):
|
| 95 |
+
idx = movies[movies['id'] == movie_id].index[0]
|
| 96 |
+
sim_scores = list(enumerate(cosine_sim[idx]))
|
| 97 |
+
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
|
| 98 |
+
sim_scores = sim_scores[1:6]
|
| 99 |
+
movie_indices = [i[0] for i in sim_scores]
|
| 100 |
+
return movies.iloc[movie_indices]
|
| 101 |
+
|
| 102 |
+
@app.route('/')
|
| 103 |
+
@login_required
|
| 104 |
+
def home():
|
| 105 |
+
return render_template('recommendation.html', movies=movies.sample(20).to_dict(orient='records'))
|
| 106 |
+
|
| 107 |
+
@app.route('/movie/<int:id>')
|
| 108 |
+
@login_required
|
| 109 |
+
def movie_details(id):
|
| 110 |
+
movie = movies[movies['id'] == id].iloc[0]
|
| 111 |
+
recommendations = get_recommendations_by_id(id).to_dict(orient='records')
|
| 112 |
+
return render_template('movie_details.html', movie=movie, recommendations=recommendations)
|
| 113 |
+
|
| 114 |
+
@app.route('/recommend', methods=['POST'])
|
| 115 |
+
@login_required
|
| 116 |
+
def recommend():
|
| 117 |
+
title = request.form['title']
|
| 118 |
+
recommendations, close_matches = get_recommendations(title)
|
| 119 |
+
if recommendations is None:
|
| 120 |
+
flash("Movie title not found. Did you mean one of these?", 'warning')
|
| 121 |
+
return render_template('recommendation.html', movies=[match.to_dict() for match in close_matches])
|
| 122 |
+
return render_template('recommendation.html', movies=recommendations.to_dict(orient='records'))
|
| 123 |
+
|
| 124 |
+
@app.route('/search', methods=['GET', 'POST'])
|
| 125 |
+
@login_required
|
| 126 |
+
def search():
|
| 127 |
+
if request.method == 'POST':
|
| 128 |
+
query = request.form['query']
|
| 129 |
+
results = movies[movies['title'].str.contains(query, case=False, na=False)]
|
| 130 |
+
return render_template('search.html', movies=results.to_dict(orient='records'))
|
| 131 |
+
return render_template('search.html', movies=None)
|
| 132 |
+
|
| 133 |
+
@app.route('/filter', methods=['GET', 'POST'])
|
| 134 |
+
@login_required
|
| 135 |
+
def filter():
|
| 136 |
+
genres = sorted(movies['genre'].str.split(',', expand=True).stack().dropna().unique())
|
| 137 |
+
languages = sorted(movies['original_language'].dropna().unique())
|
| 138 |
+
|
| 139 |
+
if request.method == 'POST':
|
| 140 |
+
selected_genre = request.form.get('genre')
|
| 141 |
+
selected_language = request.form.get('language')
|
| 142 |
+
|
| 143 |
+
if not selected_genre and not selected_language:
|
| 144 |
+
return render_template('filter.html', movies=None, genres=genres, languages=languages,
|
| 145 |
+
error_message="No movies found. Please adjust your filters.")
|
| 146 |
+
|
| 147 |
+
filtered_movies = movies.copy()
|
| 148 |
+
|
| 149 |
+
if selected_genre:
|
| 150 |
+
filtered_movies = filtered_movies[filtered_movies['genre'].str.contains(selected_genre, na=False)]
|
| 151 |
+
|
| 152 |
+
if selected_language:
|
| 153 |
+
filtered_movies = filtered_movies[filtered_movies['original_language'] == selected_language]
|
| 154 |
+
|
| 155 |
+
filtered_movies['genre'] = filtered_movies['genre'].fillna('').astype(str)
|
| 156 |
+
|
| 157 |
+
sample_size = min(50, len(filtered_movies))
|
| 158 |
+
|
| 159 |
+
filtered_movies = filtered_movies.sample(sample_size).to_dict(orient='records')
|
| 160 |
+
|
| 161 |
+
return render_template('filter.html', movies=filtered_movies, genres=genres, languages=languages,
|
| 162 |
+
error_message=None)
|
| 163 |
+
|
| 164 |
+
return render_template('filter.html', movies=None, genres=genres, languages=languages, error_message=None)
|
| 165 |
+
|
| 166 |
+
if __name__ == '__main__':
|
| 167 |
+
app.run(host='0.0.0.0', port=7860)
|
requirements.txt
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
bcrypt==4.2.0
|
| 2 |
+
blinker==1.8.2
|
| 3 |
+
certifi==2024.7.4
|
| 4 |
+
charset-normalizer==3.3.2
|
| 5 |
+
click==8.1.7
|
| 6 |
+
Flask==3.0.3
|
| 7 |
+
Flask==3.0.3
|
| 8 |
+
Flask-Bcrypt==1.0.1
|
| 9 |
+
gunicorn
|
| 10 |
+
fuzzywuzzy==0.18.0
|
| 11 |
+
idna==3.7
|
| 12 |
+
itsdangerous==2.2.0
|
| 13 |
+
Jinja2==3.1.4
|
| 14 |
+
joblib==1.4.2
|
| 15 |
+
Levenshtein==0.25.1
|
| 16 |
+
MarkupSafe==2.1.5
|
| 17 |
+
numpy==2.0.1
|
| 18 |
+
pandas==2.2.2
|
| 19 |
+
python-dateutil==2.9.0.post0
|
| 20 |
+
python-Levenshtein==0.25.1
|
| 21 |
+
pytz==2024.1
|
| 22 |
+
rapidfuzz==3.9.4
|
| 23 |
+
requests==2.32.3
|
| 24 |
+
scikit-learn==1.5.1
|
| 25 |
+
scipy==1.14.0
|
| 26 |
+
six==1.16.0
|
| 27 |
+
threadpoolctl==3.5.0
|
| 28 |
+
tzdata==2024.1
|
| 29 |
+
urllib3==2.2.2
|
| 30 |
+
Werkzeug==3.0.3
|
| 31 |
+
supabase==2.10.0
|
| 32 |
+
python-dotenv==1.0.1
|
static/css/bottom_nav.css
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Bottom Navigation Bar */
|
| 2 |
+
.bottom-nav {
|
| 3 |
+
position: fixed;
|
| 4 |
+
bottom: 0;
|
| 5 |
+
left: 0;
|
| 6 |
+
width: 100vw;
|
| 7 |
+
background-color: #1e1e1e;
|
| 8 |
+
display: flex;
|
| 9 |
+
justify-content: space-around;
|
| 10 |
+
align-items: center;
|
| 11 |
+
padding: 10px 0;
|
| 12 |
+
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.5);
|
| 13 |
+
z-index: 1000;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.bottom-nav-link {
|
| 17 |
+
display: flex;
|
| 18 |
+
flex-direction: column;
|
| 19 |
+
align-items: center;
|
| 20 |
+
text-decoration: none;
|
| 21 |
+
color: #e50914;
|
| 22 |
+
font-family: 'Bebas Neue', sans-serif;
|
| 23 |
+
transition: color 0.3s;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.bottom-nav-link:hover {
|
| 27 |
+
color: #b20710;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.nav-icon {
|
| 31 |
+
width: 24px;
|
| 32 |
+
height: 24px;
|
| 33 |
+
margin-bottom: 5px;
|
| 34 |
+
fill: currentColor;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.bottom-nav-link span {
|
| 38 |
+
font-size: 12px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@media (min-width: 768px) {
|
| 43 |
+
.bottom-nav {
|
| 44 |
+
display: none;
|
| 45 |
+
}
|
| 46 |
+
}
|
static/css/card.css
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.c-con {
|
| 2 |
+
display: flex;
|
| 3 |
+
justify-content: space-around;
|
| 4 |
+
margin-top: 20px;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
.card-container {
|
| 8 |
+
justify-content: space-around;
|
| 9 |
+
margin-top: 20px;
|
| 10 |
+
display: none;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.card {
|
| 14 |
+
background-color: #1e1e1e;
|
| 15 |
+
border-radius: 5px;
|
| 16 |
+
padding: 20px;
|
| 17 |
+
margin: 10px;
|
| 18 |
+
width: 250px;
|
| 19 |
+
text-align: center;
|
| 20 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 21 |
+
transition: transform 0.2s;
|
| 22 |
+
cursor: pointer;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.card:hover {
|
| 26 |
+
transform: scale(1.05);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.card h2 {
|
| 30 |
+
font-family: 'Bebas Neue', sans-serif;
|
| 31 |
+
color: #e50914;
|
| 32 |
+
font-size: 24px;
|
| 33 |
+
margin-bottom: 10px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.card p {
|
| 37 |
+
font-size: 16px;
|
| 38 |
+
color: #b3b3b3;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
@media (max-width: 768px) {
|
| 42 |
+
.card-container {
|
| 43 |
+
display: flex;
|
| 44 |
+
flex-direction: column;
|
| 45 |
+
align-items: center;
|
| 46 |
+
margin-top: 20px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.card {
|
| 50 |
+
width: 90%;
|
| 51 |
+
max-width: 350px;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
}
|
static/css/flash_message.css
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Style for the flash messages container */
|
| 2 |
+
.flash-messages {
|
| 3 |
+
margin: 20px 0;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
/* General style for flash messages */
|
| 7 |
+
.flash-messages .alert {
|
| 8 |
+
padding: 15px;
|
| 9 |
+
margin-bottom: 10px;
|
| 10 |
+
border: 1px solid transparent;
|
| 11 |
+
border-radius: 4px;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/* Success message */
|
| 15 |
+
.flash-messages .alert-success {
|
| 16 |
+
color: #155724;
|
| 17 |
+
background-color: #d4edda;
|
| 18 |
+
border-color: #c3e6cb;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/* Error message */
|
| 22 |
+
.flash-messages .alert-error {
|
| 23 |
+
color: #721c24;
|
| 24 |
+
background-color: #f8d7da;
|
| 25 |
+
border-color: #f5c6cb;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Warning message */
|
| 29 |
+
.flash-messages .alert-warning {
|
| 30 |
+
color: #856404;
|
| 31 |
+
background-color: #fff3cd;
|
| 32 |
+
border-color: #ffeeba;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Info message */
|
| 36 |
+
.flash-messages .alert-info {
|
| 37 |
+
color: #0c5460;
|
| 38 |
+
background-color: #d1ecf1;
|
| 39 |
+
border-color: #bee5eb;
|
| 40 |
+
}
|
static/css/form.css
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
form {
|
| 2 |
+
display: flex;
|
| 3 |
+
flex-direction:column;
|
| 4 |
+
align-items: center;
|
| 5 |
+
margin-top: 20px;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.form-row {
|
| 9 |
+
display: flex;
|
| 10 |
+
flex-wrap: wrap;
|
| 11 |
+
align-items: center;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.form-group {
|
| 15 |
+
flex: 1;
|
| 16 |
+
min-width: 100px;
|
| 17 |
+
}
|
| 18 |
+
form label {
|
| 19 |
+
margin-bottom: 10px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
form input {
|
| 23 |
+
padding: 10px;
|
| 24 |
+
margin: 10px;
|
| 25 |
+
width: 100%;
|
| 26 |
+
max-width: 300px;
|
| 27 |
+
border: 1px solid #333;
|
| 28 |
+
border-radius: 5px;
|
| 29 |
+
background-color: #333;
|
| 30 |
+
color: #fff;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
form select {
|
| 34 |
+
margin: 10px;
|
| 35 |
+
width: 100%;
|
| 36 |
+
max-width: 100px;
|
| 37 |
+
border: 1px solid #333;
|
| 38 |
+
border-radius: 5px;
|
| 39 |
+
background-color: #333;
|
| 40 |
+
color: #fff;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
form button {
|
| 44 |
+
padding: 10px 20px 10px 20px;
|
| 45 |
+
background-color: #e50914;
|
| 46 |
+
border: none;
|
| 47 |
+
border-radius: 5px;
|
| 48 |
+
color: #fff;
|
| 49 |
+
cursor: pointer;
|
| 50 |
+
transition: background-color 0.2s;
|
| 51 |
+
margin: 10px
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
form button:hover {
|
| 55 |
+
background-color: #b20710;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
.search-container,.recommendation-container {
|
| 61 |
+
display: flex;
|
| 62 |
+
margin-bottom: 20px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.dark-theme-dropdown {
|
| 66 |
+
background-color: #333;
|
| 67 |
+
color: #fff;
|
| 68 |
+
border: 1px solid #444;
|
| 69 |
+
border-radius: 5px;
|
| 70 |
+
padding: 10px;
|
| 71 |
+
width: 100%;
|
| 72 |
+
max-width: 100px;
|
| 73 |
+
appearance: none;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.dark-theme-dropdown option {
|
| 77 |
+
background-color: #333;
|
| 78 |
+
color: #fff;
|
| 79 |
+
}
|
static/css/gototop.css
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.go-to-top {
|
| 2 |
+
position: fixed;
|
| 3 |
+
bottom: 70px;
|
| 4 |
+
right: 20px;
|
| 5 |
+
width: 50px;
|
| 6 |
+
height: 50px;
|
| 7 |
+
border: none;
|
| 8 |
+
background-color: #333;
|
| 9 |
+
color: #fff;
|
| 10 |
+
display: none;
|
| 11 |
+
align-items: center;
|
| 12 |
+
justify-content: center;
|
| 13 |
+
cursor: pointer;
|
| 14 |
+
border-radius: 50%;
|
| 15 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 16 |
+
transition: background-color 0.3s, box-shadow 0.3s;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.go-to-top svg {
|
| 20 |
+
width: 24px;
|
| 21 |
+
height: 24px;
|
| 22 |
+
fill: currentColor;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.go-to-top:hover {
|
| 26 |
+
background-color: #555;
|
| 27 |
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
.go-to-top.show {
|
| 33 |
+
display: flex;
|
| 34 |
+
opacity: 1;
|
| 35 |
+
transform: translateY(0);
|
| 36 |
+
}
|
static/css/navbar.css
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.navbar {
|
| 2 |
+
display: flex;
|
| 3 |
+
justify-content: space-between;
|
| 4 |
+
align-items: center;
|
| 5 |
+
padding: 10px 20px;
|
| 6 |
+
background-color: #333;
|
| 7 |
+
color: #fff;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.navbar-toggle {
|
| 11 |
+
display: none;
|
| 12 |
+
background: none;
|
| 13 |
+
border: none;
|
| 14 |
+
color: #e50914;
|
| 15 |
+
font-size: 24px;
|
| 16 |
+
cursor: pointer;
|
| 17 |
+
margin: 5px 10px;
|
| 18 |
+
position: absolute;
|
| 19 |
+
top: 10px;
|
| 20 |
+
right: 10px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.navbar-nav {
|
| 24 |
+
list-style-type: none;
|
| 25 |
+
padding: 0;
|
| 26 |
+
margin: 0;
|
| 27 |
+
display: flex;
|
| 28 |
+
justify-content: center;
|
| 29 |
+
flex-wrap: wrap;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.navbar-nav li {
|
| 33 |
+
margin: 0 10px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.navbar-nav a {
|
| 37 |
+
color: #e50914;
|
| 38 |
+
text-decoration: none;
|
| 39 |
+
font-family: 'Bebas Neue', sans-serif;
|
| 40 |
+
font-size: 18px;
|
| 41 |
+
transition: color 0.3s;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.navbar-nav a:hover {
|
| 45 |
+
color: #b20710;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
.navbar-logo {
|
| 51 |
+
font-family: 'Bebas Neue', sans-serif;
|
| 52 |
+
font-size: 1.5rem;
|
| 53 |
+
font-weight: bold;
|
| 54 |
+
color: #e50914;
|
| 55 |
+
display: none;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
.logout {
|
| 61 |
+
margin-left: auto;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.logout a {
|
| 65 |
+
color: #fff;
|
| 66 |
+
text-decoration: none;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@media (max-width: 768px) {
|
| 71 |
+
|
| 72 |
+
.navbar-logo {
|
| 73 |
+
display: block;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
.navbar {
|
| 78 |
+
padding:20px;
|
| 79 |
+
display: none;
|
| 80 |
+
}
|
| 81 |
+
.navbar-nav {
|
| 82 |
+
display: none;
|
| 83 |
+
width: 100%;
|
| 84 |
+
text-align: center;
|
| 85 |
+
flex-direction: column;
|
| 86 |
+
padding: 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.navbar-nav.show {
|
| 90 |
+
display: flex;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.navbar-toggle {
|
| 94 |
+
display: block;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.navbar-nav li {
|
| 98 |
+
margin: 5px 0;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.navbar-nav a {
|
| 102 |
+
font-size: 16px;
|
| 103 |
+
}
|
| 104 |
+
}
|
static/css/style.css
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
html, body {
|
| 2 |
+
height: 100%;
|
| 3 |
+
font-family: 'Arial', sans-serif;
|
| 4 |
+
background-color: #121212;
|
| 5 |
+
color: #ffffff;
|
| 6 |
+
margin: 0;
|
| 7 |
+
padding: 0;
|
| 8 |
+
display: flex;
|
| 9 |
+
flex-direction: column;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
::-webkit-scrollbar {
|
| 13 |
+
width: 1px;
|
| 14 |
+
height: 1px;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
::-webkit-scrollbar-track {
|
| 18 |
+
background-color: #121212;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
::-webkit-scrollbar-thumb {
|
| 22 |
+
background-color: #121212;
|
| 23 |
+
border-radius: 10px;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
::-webkit-scrollbar-thumb:hover {
|
| 27 |
+
background-color: #121212;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
.container {
|
| 32 |
+
max-width: 1200px;
|
| 33 |
+
margin: 0 auto;
|
| 34 |
+
padding: 20px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
h1, h2, h3 {
|
| 38 |
+
font-family: 'Bebas Neue', sans-serif;
|
| 39 |
+
color: #e50914;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
h1 {
|
| 43 |
+
text-align: center;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
.movies {
|
| 48 |
+
display: flex;
|
| 49 |
+
flex-wrap: wrap;
|
| 50 |
+
justify-content: space-around;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.movie {
|
| 54 |
+
background-color: #1e1e1e;
|
| 55 |
+
border-radius: 25px;
|
| 56 |
+
padding: 10px;
|
| 57 |
+
margin: 10px;
|
| 58 |
+
width: 200px;
|
| 59 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 60 |
+
transition: transform 0.2s;
|
| 61 |
+
text-align: center;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.movie:hover {
|
| 65 |
+
transform: scale(1.05);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.movie h2, .movie h3 {
|
| 69 |
+
font-size: 18px;
|
| 70 |
+
margin: 10px 0;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.movie p {
|
| 74 |
+
font-size: 14px;
|
| 75 |
+
color: #b3b3b3;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.poster {
|
| 79 |
+
width: 100%;
|
| 80 |
+
height: auto;
|
| 81 |
+
border-radius: 25px;
|
| 82 |
+
margin-bottom: 10px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.poster-container {
|
| 86 |
+
flex: 1;
|
| 87 |
+
margin-right: 20px;
|
| 88 |
+
padding: 10px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.details-container {
|
| 92 |
+
flex: 2;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.movie-detail {
|
| 96 |
+
display: flex;
|
| 97 |
+
align-items: flex-start;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.related-movies {
|
| 101 |
+
margin-top: 40px;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.related-movies h2 {
|
| 105 |
+
text-align: center;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.genre {
|
| 109 |
+
margin-right: 5px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
a {
|
| 113 |
+
color: #e50914;
|
| 114 |
+
text-decoration: none;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
a:hover {
|
| 118 |
+
text-decoration: underline;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
.home-button-container {
|
| 124 |
+
text-align: center;
|
| 125 |
+
margin-top: 40px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.home-button {
|
| 129 |
+
display: inline-block;
|
| 130 |
+
padding: 10px 20px;
|
| 131 |
+
background-color: #e50914;
|
| 132 |
+
color: #fff;
|
| 133 |
+
text-decoration: none;
|
| 134 |
+
border-radius: 5px;
|
| 135 |
+
font-size: 16px;
|
| 136 |
+
transition: background-color 0.2s;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.home-button:hover {
|
| 140 |
+
background-color: #b20710;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.error-message {
|
| 144 |
+
color: #e50914;
|
| 145 |
+
font-size: 10px;
|
| 146 |
+
font-weight: bold;
|
| 147 |
+
text-align: center;
|
| 148 |
+
margin-top: 20px;
|
| 149 |
+
}
|
| 150 |
+
.container h1 {
|
| 151 |
+
margin-top: 20px;
|
| 152 |
+
visibility: visible;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@media (max-width: 768px) {
|
| 158 |
+
.movies {
|
| 159 |
+
display: grid;
|
| 160 |
+
grid-template-columns: repeat(2, 1fr);
|
| 161 |
+
gap: 30px;
|
| 162 |
+
justify-items: center;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.movie {
|
| 166 |
+
width: 100%;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.movie-detail {
|
| 170 |
+
flex-direction: column;
|
| 171 |
+
align-items: center;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.poster-container {
|
| 175 |
+
margin: 50px;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.details-container {
|
| 179 |
+
text-align: center;
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.bottom-gap {
|
| 184 |
+
margin-bottom: 0;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
@media (max-width: 768px) {
|
| 188 |
+
|
| 189 |
+
.bottom-gap {
|
| 190 |
+
margin-bottom: 60px;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
}
|
| 194 |
+
|
static/js/base2.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
// Existing functionality: Redirect to movie details page when clicked
|
| 3 |
+
|
| 4 |
+
const movies = document.querySelectorAll('.movie');
|
| 5 |
+
|
| 6 |
+
movies.forEach(movie => {
|
| 7 |
+
movie.addEventListener('click', () => {
|
| 8 |
+
// Redirect to movie details page when clicked
|
| 9 |
+
const link = movie.querySelector('a');
|
| 10 |
+
if (link) {
|
| 11 |
+
window.location.href = link.href;
|
| 12 |
+
}
|
| 13 |
+
});
|
| 14 |
+
});
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
// Get all elements with the class "option"
|
| 18 |
+
const options = document.querySelectorAll('.option');
|
| 19 |
+
|
| 20 |
+
// Attach a click event listener to each option element
|
| 21 |
+
options.forEach(option => {
|
| 22 |
+
option.addEventListener('click', function() {
|
| 23 |
+
// Remove the "active" class from all option elements
|
| 24 |
+
options.forEach(el => el.classList.remove('active'));
|
| 25 |
+
|
| 26 |
+
// Add the "active" class to the clicked option element
|
| 27 |
+
this.classList.add('active');
|
| 28 |
+
});
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
static/js/main.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
// Existing functionality: Redirect to movie details page when clicked
|
| 3 |
+
|
| 4 |
+
const movies = document.querySelectorAll('.movie');
|
| 5 |
+
|
| 6 |
+
movies.forEach(movie => {
|
| 7 |
+
movie.addEventListener('click', () => {
|
| 8 |
+
// Redirect to movie details page when clicked
|
| 9 |
+
const link = movie.querySelector('a');
|
| 10 |
+
if (link) {
|
| 11 |
+
window.location.href = link.href;
|
| 12 |
+
}
|
| 13 |
+
});
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
// New functionality: Create and handle "Go to Top" button
|
| 18 |
+
const goToTopButton = document.createElement('button');
|
| 19 |
+
goToTopButton.classList.add('go-to-top');
|
| 20 |
+
goToTopButton.innerHTML = `
|
| 21 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 22 |
+
<path d="M12 5.293l-6.293 6.293 1.414 1.414L12 8.121l4.879 4.879 1.414-1.414L12 5.293z"/>
|
| 23 |
+
</svg>
|
| 24 |
+
`;
|
| 25 |
+
document.body.appendChild(goToTopButton);
|
| 26 |
+
|
| 27 |
+
window.addEventListener('scroll', () => {
|
| 28 |
+
if (window.scrollY > 200) {
|
| 29 |
+
goToTopButton.style.display = 'block';
|
| 30 |
+
} else {
|
| 31 |
+
goToTopButton.style.display = 'none';
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
goToTopButton.addEventListener('click', () => {
|
| 36 |
+
window.scrollTo({
|
| 37 |
+
top: 0,
|
| 38 |
+
behavior: 'smooth'
|
| 39 |
+
});
|
| 40 |
+
});
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// Get all elements with the class "option"
|
| 44 |
+
const options = document.querySelectorAll('.option');
|
| 45 |
+
|
| 46 |
+
// Attach a click event listener to each option element
|
| 47 |
+
options.forEach(option => {
|
| 48 |
+
option.addEventListener('click', function() {
|
| 49 |
+
// Remove the "active" class from all option elements
|
| 50 |
+
options.forEach(el => el.classList.remove('active'));
|
| 51 |
+
|
| 52 |
+
// Add the "active" class to the clicked option element
|
| 53 |
+
this.classList.add('active');
|
| 54 |
+
});
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
|
templates/base.html
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="UTF-8">
|
| 5 |
+
<title>{% block title %}{% endblock %}</title>
|
| 6 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/navbar.css') }}">
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/bottom_nav.css') }}">
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/card.css') }}">
|
| 10 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/form.css') }}">
|
| 11 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/gototop.css') }}">
|
| 12 |
+
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
|
| 13 |
+
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<script type="module">
|
| 17 |
+
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed@1.3.14/dist/web.js"
|
| 18 |
+
Chatbot.init({
|
| 19 |
+
chatflowid: "d22a7084-54e8-4caf-8182-7f33b86265b3",
|
| 20 |
+
apiHost: "https://crw-dev-flxoxwxixsxex.hf.space",
|
| 21 |
+
chatflowConfig: {
|
| 22 |
+
// topK: 2
|
| 23 |
+
},
|
| 24 |
+
theme: {
|
| 25 |
+
button: {
|
| 26 |
+
backgroundColor: "#303235",
|
| 27 |
+
right: 20,
|
| 28 |
+
bottom: 10,
|
| 29 |
+
size: 'medium', // small | medium | large | number
|
| 30 |
+
dragAndDrop: true,
|
| 31 |
+
customIconSrc: "https://i.ibb.co/ZN6GSWv/logo.png",
|
| 32 |
+
},
|
| 33 |
+
chatWindow: {
|
| 34 |
+
showTitle: true,
|
| 35 |
+
title: 'CRW AI',
|
| 36 |
+
titleAvatarSrc: 'https://i.ibb.co/ZN6GSWv/logo.png',
|
| 37 |
+
showAgentMessages: true,
|
| 38 |
+
welcomeMessage: 'Hello! Everyone, I am CRW AI. I can help you with your movie related queries.',
|
| 39 |
+
errorMessage: 'sorry, something went wrong. Please try again',
|
| 40 |
+
backgroundColor: "#1e1e1e",
|
| 41 |
+
height: 700,
|
| 42 |
+
width: 400,
|
| 43 |
+
fontSize: 16,
|
| 44 |
+
poweredByTextColor: "#303235",
|
| 45 |
+
botMessage: {
|
| 46 |
+
backgroundColor: "#121212",
|
| 47 |
+
textColor: "#ffffff",
|
| 48 |
+
showAvatar: true,
|
| 49 |
+
avatarSrc: "https://img.icons8.com/papercut/60/bot.png",
|
| 50 |
+
},
|
| 51 |
+
userMessage: {
|
| 52 |
+
backgroundColor: "#e50914",
|
| 53 |
+
textColor: "#ffffff",
|
| 54 |
+
showAvatar: true,
|
| 55 |
+
avatarSrc: "https://img.icons8.com/isometric/50/person-female.png",
|
| 56 |
+
},
|
| 57 |
+
textInput: {
|
| 58 |
+
placeholder: 'Type your question',
|
| 59 |
+
backgroundColor: '#121212',
|
| 60 |
+
textColor: '#ffffff',
|
| 61 |
+
sendButtonColor: '#e50914',
|
| 62 |
+
maxChars: 50,
|
| 63 |
+
maxCharsWarningMessage: 'You exceeded the characters limit. Please input less than 50 characters.',
|
| 64 |
+
autoFocus: true, // If not used, autofocus is disabled on mobile and enabled on desktop. true enables it on both, false disables it on both.
|
| 65 |
+
sendMessageSound: true,
|
| 66 |
+
// sendSoundLocation: "send_message.mp3", // If this is not used, the default sound effect will be played if sendSoundMessage is true.
|
| 67 |
+
receiveMessageSound: true,
|
| 68 |
+
// receiveSoundLocation: "receive_message.mp3", // If this is not used, the default sound effect will be played if receiveSoundMessage is true.
|
| 69 |
+
},
|
| 70 |
+
feedback: {
|
| 71 |
+
color: '#303235',
|
| 72 |
+
},
|
| 73 |
+
footer: {
|
| 74 |
+
textColor: '#e50914',
|
| 75 |
+
text: 'Powered by',
|
| 76 |
+
company: 'CRW',
|
| 77 |
+
companyLink: 'https://crw07.dev',
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
})
|
| 82 |
+
</script>
|
| 83 |
+
<nav class="navbar">
|
| 84 |
+
<button class="go-to-top" aria-label="Go to top">
|
| 85 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 86 |
+
<path d="M12 5.293l-6.293 6.293 1.414 1.414L12 8.121l4.879 4.879 1.414-1.414L12 5.293z"/>
|
| 87 |
+
</svg>
|
| 88 |
+
</button>
|
| 89 |
+
<ul class="navbar-nav">
|
| 90 |
+
<li><a href="{{ url_for('home') }}">Home</a></li>
|
| 91 |
+
<li><a href="{{ url_for('search') }}">Search</a></li>
|
| 92 |
+
<li><a href="{{ url_for('filter') }}">Filter</a></li>
|
| 93 |
+
</ul>
|
| 94 |
+
<div class="logout">
|
| 95 |
+
<ul class="navbar-nav">
|
| 96 |
+
{% if 'logged_in' in session %}
|
| 97 |
+
<li>{{ session['username'] }}</li>
|
| 98 |
+
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
| 99 |
+
{% endif %}
|
| 100 |
+
</ul>
|
| 101 |
+
</div>
|
| 102 |
+
</nav>
|
| 103 |
+
{% block content %}{% endblock %}
|
| 104 |
+
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
| 105 |
+
<!-- Add this at the end of base.html, just before the closing </body> tag -->
|
| 106 |
+
<div class="bottom-nav">
|
| 107 |
+
<a href="{{ url_for('home') }}" class="bottom-nav-link">
|
| 108 |
+
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 109 |
+
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
| 110 |
+
</svg>
|
| 111 |
+
<span>Home</span>
|
| 112 |
+
</a>
|
| 113 |
+
<a href="{{ url_for('search') }}" class="bottom-nav-link">
|
| 114 |
+
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 115 |
+
<path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14z"/>
|
| 116 |
+
</svg>
|
| 117 |
+
<span>Search</span>
|
| 118 |
+
</a>
|
| 119 |
+
|
| 120 |
+
<a href="{{ url_for('filter') }}" class="bottom-nav-link">
|
| 121 |
+
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 122 |
+
<path d="M3 18h6v2H3v-2zm0-5h12v2H3v-2zm0-5h18v2H3V8zm0-5h6v2H3V3zm0 10h12v2H3v-2zm0-5h18v2H3V8z"/>
|
| 123 |
+
</svg>
|
| 124 |
+
<span>Filter</span>
|
| 125 |
+
</a>
|
| 126 |
+
<a class="bottom-nav-link" aria-label="Menu">
|
| 127 |
+
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 128 |
+
<circle cx="12" cy="12" r="10"/>
|
| 129 |
+
</svg>
|
| 130 |
+
</a>
|
| 131 |
+
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
</body>
|
| 135 |
+
</html>
|
templates/base2.html
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="UTF-8">
|
| 5 |
+
<title>{% block title %}{% endblock %}</title>
|
| 6 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/navbar.css') }}">
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/bottom_nav.css') }}">
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/card.css') }}">
|
| 10 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/form.css') }}">
|
| 11 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/gototop.css') }}">
|
| 12 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/flash_message.css') }}">
|
| 13 |
+
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
|
| 14 |
+
<script src="{{ url_for('static', filename='js/base2.js') }}"></script>
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
</head>
|
| 18 |
+
<body>
|
| 19 |
+
<nav class="navbar">
|
| 20 |
+
<ul class="navbar-nav">
|
| 21 |
+
<li><a href="{{ url_for('home') }}">Home</a></li>
|
| 22 |
+
<li><a href="{{ url_for('search') }}">Search</a></li>
|
| 23 |
+
<li><a href="{{ url_for('filter') }}">Filter</a></li>
|
| 24 |
+
</ul>
|
| 25 |
+
<div class="logout">
|
| 26 |
+
<ul class="navbar-nav">
|
| 27 |
+
{% if 'logged_in' in session %}
|
| 28 |
+
<li>{{ session['username'] }}</li>
|
| 29 |
+
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
| 30 |
+
{% endif %}
|
| 31 |
+
</ul>
|
| 32 |
+
</div>
|
| 33 |
+
</nav>
|
| 34 |
+
{% block content %}{% endblock %}
|
| 35 |
+
<div class="bottom-nav">
|
| 36 |
+
<a href="{{ url_for('home') }}" class="bottom-nav-link">
|
| 37 |
+
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 38 |
+
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
| 39 |
+
</svg>
|
| 40 |
+
<span>Home</span>
|
| 41 |
+
</a>
|
| 42 |
+
<a href="{{ url_for('search') }}" class="bottom-nav-link">
|
| 43 |
+
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 44 |
+
<path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14z"/>
|
| 45 |
+
</svg>
|
| 46 |
+
<span>Search</span>
|
| 47 |
+
</a>
|
| 48 |
+
|
| 49 |
+
<a href="{{ url_for('filter') }}" class="bottom-nav-link">
|
| 50 |
+
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 51 |
+
<path d="M3 18h6v2H3v-2zm0-5h12v2H3v-2zm0-5h18v2H3V8zm0-5h6v2H3V3zm0 10h12v2H3v-2zm0-5h18v2H3V8z"/>
|
| 52 |
+
</svg>
|
| 53 |
+
<span>Filter</span>
|
| 54 |
+
</a>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
</body>
|
| 58 |
+
</html>
|
templates/filter.html
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Filter Movies{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="container">
|
| 5 |
+
<form method="POST" action="{{ url_for('filter') }}">
|
| 6 |
+
<div class="form-row">
|
| 7 |
+
<div class="form-group">
|
| 8 |
+
<select name="genre" id="genre" class="dark-theme-dropdown">
|
| 9 |
+
<option value="">All Genres</option>
|
| 10 |
+
{% for genre in genres %}
|
| 11 |
+
<option value="{{ genre }}">{{ genre }}</option>
|
| 12 |
+
{% endfor %}
|
| 13 |
+
</select>
|
| 14 |
+
</div>
|
| 15 |
+
<div class="form-group">
|
| 16 |
+
<select name="language" id="language" class="dark-theme-dropdown">
|
| 17 |
+
<option value="">All Languages</option>
|
| 18 |
+
{% for language in languages %}
|
| 19 |
+
<option value="{{ language }}">{{ language }}</option>
|
| 20 |
+
{% endfor %}
|
| 21 |
+
</select>
|
| 22 |
+
</div>
|
| 23 |
+
<button type="submit">Filter</button>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
</form>
|
| 27 |
+
{% if error_message %}
|
| 28 |
+
<p class="error-message">{{ error_message }}</p>
|
| 29 |
+
{% endif %}
|
| 30 |
+
<div class="movies">
|
| 31 |
+
{% if movies %}
|
| 32 |
+
{% for movie in movies %}
|
| 33 |
+
<div class="movie">
|
| 34 |
+
<img src="{{ movie.poster_url or 'path/to/placeholder.jpg' }}" alt="{{ movie.title }} poster" class="poster">
|
| 35 |
+
<h3><a href="{{ url_for('movie_details', id=movie.id) }}">{{ movie.title }}</a></h3>
|
| 36 |
+
<p>Genres:
|
| 37 |
+
{% for genre in movie.genre.split(',') %}
|
| 38 |
+
<span class="genre">{{ genre }}</span>{% if not loop.last %},{% endif %}
|
| 39 |
+
{% endfor %}
|
| 40 |
+
</p>
|
| 41 |
+
<p>Rating: {{ movie.vote_average }}</p>
|
| 42 |
+
</div>
|
| 43 |
+
{% endfor %}
|
| 44 |
+
{% else %}
|
| 45 |
+
{% endif %}
|
| 46 |
+
</div>
|
| 47 |
+
<div class="bottom-gap"></div>
|
| 48 |
+
</div>
|
| 49 |
+
{% endblock %}
|
templates/login.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base2.html" %}
|
| 2 |
+
{% block title %}Login{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="container">
|
| 5 |
+
<h1>Login</h1>
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
<!-- Login Form -->
|
| 10 |
+
<form method="POST">
|
| 11 |
+
<input type="text" placeholder="Username" id="username" name="username" required>
|
| 12 |
+
<input type="password" placeholder="Password" id="password" name="password" required>
|
| 13 |
+
<button type="submit">Login</button>
|
| 14 |
+
</form>
|
| 15 |
+
|
| 16 |
+
<a href="{{ url_for('register') }}">Don't have an account? Register here</a>
|
| 17 |
+
|
| 18 |
+
<!-- Flash Messages Section -->
|
| 19 |
+
{% with messages = get_flashed_messages(with_categories=True) %}
|
| 20 |
+
{% if messages %}
|
| 21 |
+
<div class="flash-messages">
|
| 22 |
+
{% for category, message in messages %}
|
| 23 |
+
<div class="alert alert-{{ category }}">
|
| 24 |
+
{{ message }}
|
| 25 |
+
</div>
|
| 26 |
+
{% endfor %}
|
| 27 |
+
</div>
|
| 28 |
+
{% endif %}
|
| 29 |
+
{% endwith %}
|
| 30 |
+
|
| 31 |
+
</div>
|
| 32 |
+
{% endblock %}
|
templates/movie_details.html
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}{{ movie.title }}{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="container">
|
| 5 |
+
<div class="movie-detail">
|
| 6 |
+
<div class="poster-container">
|
| 7 |
+
<img src="{{ movie.poster_url or 'path/to/placeholder.jpg' }}" alt="{{ movie.title }} poster" class="poster">
|
| 8 |
+
</div>
|
| 9 |
+
<div class="details-container">
|
| 10 |
+
<h1>{{ movie.title }}</h1>
|
| 11 |
+
<p><strong>Original Language:</strong> {{ movie.original_language }}</p>
|
| 12 |
+
<p><strong>Overview:</strong> {{ movie.overview }}</p>
|
| 13 |
+
<p><strong>Genres:</strong>
|
| 14 |
+
{% for genre in movie.genre.split(',') %}
|
| 15 |
+
<span class="genre">{{ genre }}</span>{% if not loop.last %},{% endif %}
|
| 16 |
+
{% endfor %}
|
| 17 |
+
</p>
|
| 18 |
+
<p><strong>Release Date:</strong> {{ movie.release_date }}</p>
|
| 19 |
+
<p><strong>Rating:</strong> {{ movie.vote_average }}</p>
|
| 20 |
+
<p><strong>Votes:</strong> {{ movie.vote_count }}</p>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
<div class="related-movies">
|
| 24 |
+
<h2>Related Movies</h2>
|
| 25 |
+
<div class="movies">
|
| 26 |
+
{% for related_movie in recommendations %}
|
| 27 |
+
<div class="movie">
|
| 28 |
+
<img src="{{ related_movie.poster_url or 'path/to/placeholder.jpg' }}" alt="{{ related_movie.title }} poster" class="poster">
|
| 29 |
+
<h3><a href="{{ url_for('movie_details', id=related_movie.id) }}">{{ related_movie.title }}</a></h3>
|
| 30 |
+
<p>Genres:
|
| 31 |
+
{% for genre in related_movie.genre.split(',') %}
|
| 32 |
+
<span class="genre">{{ genre }}</span>{% if not loop.last %},{% endif %}
|
| 33 |
+
{% endfor %}
|
| 34 |
+
</p>
|
| 35 |
+
<p>Rating: {{ related_movie.vote_average }}</p>
|
| 36 |
+
</div>
|
| 37 |
+
{% endfor %}
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<!-- Red Button for Home Page -->
|
| 42 |
+
<div class="home-button-container">
|
| 43 |
+
<a href="{{ url_for('home') }}" class="home-button">Back to Home</a>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
{% endblock %}
|
templates/recommendation.html
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Home{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="container">
|
| 5 |
+
<form method="POST" action="{{ url_for('recommend') }}">
|
| 6 |
+
<div class="recommendation-container">
|
| 7 |
+
<input type="text" name="title" placeholder="Enter a movie title" required>
|
| 8 |
+
<button type="submit">Recommend</button>
|
| 9 |
+
</div>
|
| 10 |
+
</form>
|
| 11 |
+
|
| 12 |
+
<!-- Flash Messages Section -->
|
| 13 |
+
{% with messages = get_flashed_messages(with_categories=True) %}
|
| 14 |
+
{% if messages %}
|
| 15 |
+
<div class="flash-messages">
|
| 16 |
+
{% for category, message in messages %}
|
| 17 |
+
<div class="alert alert-{{ category }}">
|
| 18 |
+
{{ message }}
|
| 19 |
+
</div>
|
| 20 |
+
{% endfor %}
|
| 21 |
+
</div>
|
| 22 |
+
{% endif %}
|
| 23 |
+
{% endwith %}
|
| 24 |
+
|
| 25 |
+
<div class="movies">
|
| 26 |
+
{% for movie in movies %}
|
| 27 |
+
<div class="movie">
|
| 28 |
+
<img src="{{ movie.poster_url }}" alt="{{ movie.title }} poster" class="poster">
|
| 29 |
+
<h2><a href="{{ url_for('movie_details', id=movie.id) }}">{{ movie.title }}</a></h2>
|
| 30 |
+
<p>Genres:
|
| 31 |
+
{% for genre in movie.genre.split(',') %}
|
| 32 |
+
<span class="genre">{{ genre }}</span>{% if not loop.last %},{% endif %}
|
| 33 |
+
{% endfor %}
|
| 34 |
+
</p>
|
| 35 |
+
<p>Rating: {{ movie.vote_average }}</p>
|
| 36 |
+
</div>
|
| 37 |
+
{% endfor %}
|
| 38 |
+
</div>
|
| 39 |
+
<div class="bottom-gap"></div>
|
| 40 |
+
</div>
|
| 41 |
+
{% endblock %}
|
templates/register.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base2.html" %}
|
| 2 |
+
{% block title %}Register{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="container">
|
| 5 |
+
<h1>Register</h1>
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
<!-- Registration Form -->
|
| 10 |
+
<form method="POST">
|
| 11 |
+
<input type="text" placeholder="Username" id="username" name="username" required>
|
| 12 |
+
<input type="password" placeholder="Password" id="password" name="password" required>
|
| 13 |
+
<button type="submit">Sign Up</button>
|
| 14 |
+
</form>
|
| 15 |
+
|
| 16 |
+
<a href="{{ url_for('login') }}">Already have an account? Login here</a>
|
| 17 |
+
|
| 18 |
+
<!-- Flash Messages Section -->
|
| 19 |
+
{% with messages = get_flashed_messages(with_categories=True) %}
|
| 20 |
+
{% if messages %}
|
| 21 |
+
<div class="flash-messages">
|
| 22 |
+
{% for category, message in messages %}
|
| 23 |
+
<div class="alert alert-{{ category }}">
|
| 24 |
+
{{ message }}
|
| 25 |
+
</div>
|
| 26 |
+
{% endfor %}
|
| 27 |
+
</div>
|
| 28 |
+
{% endif %}
|
| 29 |
+
{% endwith %}
|
| 30 |
+
|
| 31 |
+
</div>
|
| 32 |
+
{% endblock %}
|
templates/search.html
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Search Movies{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="container">
|
| 5 |
+
<form method="POST" action="{{ url_for('search') }}" class="search-form">
|
| 6 |
+
<div class="search-container">
|
| 7 |
+
<input type="text" name="query" placeholder="Enter movie name" required class="search-input">
|
| 8 |
+
<button type="submit" class="search-button">Search</button>
|
| 9 |
+
</div>
|
| 10 |
+
</form>
|
| 11 |
+
<div class="movies">
|
| 12 |
+
{% if movies %}
|
| 13 |
+
{% for movie in movies %}
|
| 14 |
+
<div class="movie">
|
| 15 |
+
<img src="{{ movie.poster_url }}" alt="{{ movie.title }} poster" class="poster">
|
| 16 |
+
<h2><a href="{{ url_for('movie_details', id=movie.id) }}">{{ movie.title }}</a></h2>
|
| 17 |
+
<p>Genres:
|
| 18 |
+
{% if movie.genre %}
|
| 19 |
+
{% set genres = movie.genre.split(',') if movie.genre is string else [] %}
|
| 20 |
+
{% for genre in genres %}
|
| 21 |
+
<span class="genre">{{ genre }}</span>{% if not loop.last %},{% endif %}
|
| 22 |
+
{% endfor %}
|
| 23 |
+
{% else %}
|
| 24 |
+
<span class="genre">Unknown</span>
|
| 25 |
+
{% endif %}
|
| 26 |
+
</p>
|
| 27 |
+
<p>Rating: {{ movie.vote_average }}</p>
|
| 28 |
+
</div>
|
| 29 |
+
{% endfor %}
|
| 30 |
+
{% else %}
|
| 31 |
+
{% endif %}
|
| 32 |
+
</div>
|
| 33 |
+
<div class="bottom-gap"></div>
|
| 34 |
+
</div>
|
| 35 |
+
{% endblock %}
|
| 36 |
+
|