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/[email protected]/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 |
+
|