CrimsonREwind commited on
Commit
86112bd
·
verified ·
1 Parent(s): 7b43d37

Upload 28 files

Browse files
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
+