Add 'Programming/ERSMS-project/' from commit 'd060e8285ad6f3dddde51a842f2c8498138afb0b'

git-subtree-dir: Programming/ERSMS-project
git-subtree-mainline: 7861d69ae9
git-subtree-split: d060e8285a
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-02-06 22:14:41 +01:00
commit 838184c85b
86 changed files with 32242 additions and 0 deletions

310
Programming/ERSMS-project/.gitignore vendored Normal file
View File

@ -0,0 +1,310 @@
connector/Lib/*
connector/Scripts/*
pyvenv.cfg
## Core latex/pdflatex auxiliary files:
*.aux
*.lof
*.log
*.lot
*.fls
*.out
*.toc
*.fmt
*.fot
*.cb
*.cb2
.*.lb
## Intermediate documents:
*.dvi
*.xdv
*-converted-to.*
# these rules might exclude image files for figures etc.
# *.ps
# *.eps
# *.pdf
## Generated if empty string is given at "Please type another file name for output:"
.pdf
## Bibliography auxiliary files (bibtex/biblatex/biber):
*.bbl
*.bcf
*.blg
*-blx.aux
*-blx.bib
*.run.xml
## Build tool auxiliary files:
*.fdb_latexmk
*.synctex
*.synctex(busy)
*.synctex.gz
*.synctex.gz(busy)
*.pdfsync
*.rubbercache
rubber.cache
## Build tool directories for auxiliary files
# latexrun
latex.out/
## Auxiliary and intermediate files from other packages:
# algorithms
*.alg
*.loa
# achemso
acs-*.bib
# amsthm
*.thm
# beamer
*.nav
*.pre
*.snm
*.vrb
# changes
*.soc
# comment
*.cut
# cprotect
*.cpt
# elsarticle (documentclass of Elsevier journals)
*.spl
# endnotes
*.ent
# fixme
*.lox
# feynmf/feynmp
*.mf
*.mp
*.t[1-9]
*.t[1-9][0-9]
*.tfm
#(r)(e)ledmac/(r)(e)ledpar
*.end
*.?end
*.[1-9]
*.[1-9][0-9]
*.[1-9][0-9][0-9]
*.[1-9]R
*.[1-9][0-9]R
*.[1-9][0-9][0-9]R
*.eledsec[1-9]
*.eledsec[1-9]R
*.eledsec[1-9][0-9]
*.eledsec[1-9][0-9]R
*.eledsec[1-9][0-9][0-9]
*.eledsec[1-9][0-9][0-9]R
# glossaries
*.acn
*.acr
*.glg
*.glo
*.gls
*.glsdefs
*.lzo
*.lzs
*.slg
*.slo
*.sls
# uncomment this for glossaries-extra (will ignore makeindex's style files!)
# *.ist
# gnuplot
*.gnuplot
*.table
# gnuplottex
*-gnuplottex-*
# gregoriotex
*.gaux
*.glog
*.gtex
# htlatex
*.4ct
*.4tc
*.idv
*.lg
*.trc
*.xref
# hypdoc
*.hd
# hyperref
*.brf
# knitr
*-concordance.tex
# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files
# *.tikz
*-tikzDictionary
# listings
*.lol
# luatexja-ruby
*.ltjruby
# makeidx
*.idx
*.ilg
*.ind
# minitoc
*.maf
*.mlf
*.mlt
*.mtc[0-9]*
*.slf[0-9]*
*.slt[0-9]*
*.stc[0-9]*
# minted
_minted*
*.pyg
# morewrites
*.mw
# newpax
*.newpax
# nomencl
*.nlg
*.nlo
*.nls
# pax
*.pax
# pdfpcnotes
*.pdfpc
# sagetex
*.sagetex.sage
*.sagetex.py
*.sagetex.scmd
# scrwfile
*.wrt
# svg
svg-inkscape/
# sympy
*.sout
*.sympy
sympy-plots-for-*.tex/
# pdfcomment
*.upa
*.upb
# pythontex
*.pytxcode
pythontex-files-*/
# tcolorbox
*.listing
# thmtools
*.loe
# TikZ & PGF
*.dpth
*.md5
*.auxlock
# titletoc
*.ptc
# todonotes
*.tdo
# vhistory
*.hst
*.ver
# easy-todo
*.lod
# xcolor
*.xcp
# xmpincl
*.xmpi
# xindy
*.xdy
# xypic precompiled matrices and outlines
*.xyc
*.xyd
# endfloat
*.ttt
*.fff
# Latexian
TSWLatexianTemp*
## Editors:
# WinEdt
*.bak
*.sav
# Texpad
.texpadtmp
# LyX
*.lyx~
# Kile
*.backup
# gummi
.*.swp
# KBibTeX
*~[0-9]*
# TeXnicCenter
*.tps
# auto folder when using emacs and auctex
./auto/*
*.el
# expex forward references with \gathertags
*-tags.tex
# standalone packages
*.sta
# Makeindex log files
*.lpz
# xwatermark package
*.xwm
# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib
# option is specified. Footnotes are the stored in a file with suffix Notes.bib.
# Uncomment the next line to have this generated file ignored.
#*Notes.bib

View File

@ -0,0 +1,15 @@
FROM node:latest
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# If you also need http-server globally
RUN npm install -g http-server
# Bundle app source
COPY . .
EXPOSE 8080
CMD ["http-server", "-p 8080"]

View File

@ -0,0 +1,39 @@
# Projekt z ERSMS
## Backend
### budowanie i uruchamianie obrazu z bazą danych oraz serwerem:
docker compose up
w przypadku zmian w bazie danych, usunąć kontener oraz dołączone do niego volumy
### stronka administracyjna do bazy danych
Najpierw wpisujemy w przeglądarce http://localhost:8080/
Przy logowaniu wpisujemy:
* login - admin@admin.com
* hasło - admin
Po zalogowaniu się poraz pierwszy od zbudowania obrazu, widzimy stronę główną. Klikamy na niej duży przycisk "Add New Server" i pojawia się okienko z menu dodawania serwera.
W nazwie można napisać cokolwiek, jednak w zakładce "Connection" piszemy następujące rzeczy:
* host name - db
* login - root
* password - root
Reszta pozostaje jak była. Po zapisaniu ustawień, mamy już dostęp do narzędzi administratora dla naszej bazy danych.
### gadanie z endpointami
GET http://localhost:8090/ - testowy domowy
GET http://localhost:8090//api/v3/get/<string:username> - info o userach
GET http://localhost:8090//api/v3/get_movie/<int:movie_ID> - info o filmie
POST http://localhost:8090/api/v3/add/<string:oauth_ID>/<string:username> - dodawanie usera
POST http://localhost:8090/api/v3/add/<string:oauth_ID>/<string:movie_ID>/<int:rating> - dodawanie oceny do filmu przez usera
GET http://localhost:8090/api/v3/ai/<string:oauth_ID> - wyciąganie rekomendacji od AI

View File

@ -0,0 +1,25 @@
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set the working directory to /app2
WORKDIR /app2
# Install necessary OS packages
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy the Python script, requirements, and datasets into the container at /app2
COPY ./analytics.py .
COPY init_scripts/constants.ini ./init_scripts/constants.ini
COPY init_scripts/movies.csv /app/init_scripts/
COPY requirements.txt .
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install requests
# Run frontend_AI_connector.py when the container launches
CMD ["python", "./analytics.py"]

View File

@ -0,0 +1,116 @@
from flask import Flask, request, jsonify
from flask_caching import Cache
import psycopg2
import pandas
import json
from configparser import ConfigParser
from datetime import datetime
app = Flask(__name__)
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
db_connector = None
conn = None
@app.route("/api/get_number_of_ratings", methods=["GET"])
@cache.cached(timeout=500)
def get_number_of_ratings():
cursor = conn.cursor()
cursor.execute("select count(*) as num_of_ratings from ratings")
res = cursor.fetchall()
cursor.close()
return jsonify(res[0]), 200
@app.route("/api/get_movie_ratings/<string:movie_id>", methods=["GET"])
@cache.cached(timeout=50)
def get_movie_ratings(movie_id):
cursor = conn.cursor()
ratings = {}
rating_values = [5, 4, 3, 2, 1]
for rating in rating_values:
cursor.execute("""
SELECT COUNT(*) as count
FROM ratings
WHERE rating = %s AND movie_ID = %s;
""", (rating, movie_id))
result = cursor.fetchone()
ratings[f'{rating}_star'] = result[0]
cursor.close()
return jsonify(ratings), 200
@app.route("/api/get_users_number", methods=["GET"])
@cache.cached(timeout=50)
def get_number_of_users():
cursor = conn.cursor()
cursor.execute("select count(*) as num_of_users from users")
res = cursor.fetchall()
cursor.close()
return jsonify(res[0]), 200
@app.route("/api/get_movie_rating_avg/<string:movie_id>", methods=["GET"])
@cache.cached(timeout=50)
def get_movie_rating_avg(movie_id):
cursor = conn.cursor()
cursor.execute("""
SELECT AVG(rating) as avg_rating
FROM ratings
WHERE movie_ID = %s;
""", (movie_id,))
res = cursor.fetchall()
cursor.close()
return jsonify(res[0]), 200
@app.route("/api/get_user_ratings/<string:user_id>", methods=["GET"])
@cache.cached(timeout=50)
def get_user_ratings(user_id):
cursor = conn.cursor()
cursor.execute("""
SELECT movie_ID, rating
FROM ratings
WHERE oauth_ID = %s;
""", (user_id,))
res = cursor.fetchall()
cursor.close()
return jsonify(res), 200
if __name__ == "__main__":
config = ConfigParser()
config.read("init_scripts/constants.ini")
while True:
try:
conn = psycopg2.connect(
host=config["postgres"]["host"],
database=config["postgres"]["database"],
user=config["postgres"]["user"],
password=config["postgres"]["password"],
port=int(config["postgres"]["port"])
)
except Exception:
print("Trying to connect with database")
continue
else:
break
cache.init_app(app)
app.run(host="localhost", port=8082, debug=True)
conn.close()

View File

@ -0,0 +1,9 @@
[postgres]
host=db
database=test_db
user=root
password=root
port=5432
[movie]
csv_path=../../movie_recommendations/datasets/tmdb_5000_credits.csv

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
flask==3.0.3
psycopg2==2.9.9
pandas==2.2.2
Flask-Caching==2.3.0

View File

@ -0,0 +1 @@
TMDB_BEARER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxYmMxYTM1Y2U5MmIwZjc5MjUzODJhYWQxN2NkMDI5NiIsInN1YiI6IjY2NDFkOTBlNTM3MzE0ZGRmMGZiNGYxZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.PbJkpuXgokjZPd8tL_ZVUyCW4Hf0dW67JInkgld27Ew"

View File

@ -0,0 +1,4 @@
__pycache__
instance
# .env
.json

View File

@ -0,0 +1,22 @@
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set the working directory to /app
WORKDIR /app
# Install necessary OS packages
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy the Python script, requirements, and constants.ini file into the container at /app
COPY ./app.py /app/
COPY requirements.txt .
COPY .env .
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Run frontend_AI_connector.py when the container launches
CMD ["python", "./app.py"]

View File

@ -0,0 +1,201 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
import firebase_admin
from firebase_admin import credentials, auth
from dotenv import load_dotenv
import os
import requests
load_dotenv()
TMDB_BEARER_TOKEN = os.getenv('TMDB_BEARER_TOKEN')
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.sqlite3'
app.config['SQLALCHEY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
CORS(app)
cred = credentials.Certificate('movie-recommendation-firebase-adminsdk.json')
firebase_admin.initialize_app(cred)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
uid = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(80), unique=True, nullable=False)
is_admin = db.Column(db.Boolean, default=False)
class Rating(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.uid'), nullable=False)
movie_id = db.Column(db.Integer, nullable=False)
value = db.Column(db.Integer, nullable=False)
with app.app_context():
db.create_all()
@app.route('/login', methods=['POST'])
def login():
token = request.json.get('token')
try:
decoded_token = auth.verify_id_token(token)
uid = decoded_token['uid']
print(uid)
email = decoded_token.get('email')
user = User.query.filter_by(uid=uid).first()
if user is None:
user = User(uid=uid, email=email)
db.session.add(user)
db.session.commit()
return jsonify({'message': 'Login successful!', 'email': email, 'is_admin': user.is_admin}), 200
except Exception as e:
print(e)
return jsonify({'message': 'Login failed'}), 401
@app.route('/count_user_ratings', methods=['POST'])
def count_user_ratings():
token = request.json.get('token')
try:
decoded_token = auth.verify_id_token(token)
user_id = decoded_token['uid']
user = User.query.filter_by(uid=user_id).first()
if user is None:
return jsonify({'message': 'Error'}), 500
rating_count = Rating.query.filter_by(user_id=user_id).count()
return jsonify({'message': 'Ratings counted!', 'rating_count': rating_count}), 200
except Exception as e:
print(e)
return jsonify({'message': str(e)}), 500
@app.route('/movie/<int:movie_id>', methods=['GET'])
def get_tmdb_data_movie_id(movie_id):
url = f'https://api.themoviedb.org/3/movie/{movie_id}'
headers = {
'Authorization': f'Bearer {TMDB_BEARER_TOKEN}'}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return jsonify(response.json()), 200
except Exception as e:
print(e)
return jsonify({'message': str(e)}), 500
@app.route('/movie', methods=['GET'])
def get_tmdb_data_query():
query = request.args.get('query')
if query:
url = f'https://api.themoviedb.org/3/search/movie?query={query}'
else:
url = 'https://api.themoviedb.org/3/trending/movie/day'
headers = {
'Authorization': f'Bearer {TMDB_BEARER_TOKEN}'}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return jsonify(response.json()), 200
except Exception as e:
print(e)
return jsonify({'message': str(e)}), 500
@app.route('/rating', methods=['POST'])
def add_rating():
token = request.json.get('token')
movie = request.json.get('movie')
value = request.json.get('value')
try:
decoded_token = auth.verify_id_token(token)
user_id = decoded_token['uid']
# user = User.query.filter_by(uid=user_id).first()
# if user is None:
# return jsonify({'message': 'Error'}), 500
rating = Rating.query.filter_by(
user_id=user_id, movie_id=movie).first()
if rating is None:
rating = Rating(user_id=user_id, movie_id=movie, value=value)
db.session.add(rating)
db.session.commit()
return jsonify({'message': 'Rating added successfully!'}), 201
else:
rating.value = value
db.session.commit()
return jsonify({'message': 'Rating updated successfully!'}), 200
except Exception as e:
print(e)
return jsonify({'message': str(e)}), 500
@app.route('/rating', methods=['DELETE'])
def remove_rating():
token = request.json.get('token')
movie = request.json.get('movie')
try:
decoded_token = auth.verify_id_token(token)
user_id = decoded_token['uid']
# user = User.query.filter_by(uid=user_id).first()
# if user is None:
# return jsonify({'message': 'Error'}), 500
rating = Rating.query.filter_by(
user_id=user_id, movie_id=movie).first()
if rating is None:
return jsonify({'message': 'Error'}), 500
db.session.delete(rating)
db.session.commit()
return jsonify({'message': 'Rating removed successfully!'}), 200
except Exception as e:
print(e)
return jsonify({'message': str(e)}), 500
@app.route('/get_rating', methods=['POST'])
def get_rating():
token = request.json.get('token')
movie = request.json.get('movie')
try:
decoded_token = auth.verify_id_token(token)
user_id = decoded_token['uid']
# user = User.query.filter_by(uid=user_id).first()
# if user is None:
# return jsonify({'message': 'Error'}), 500
rating = Rating.query.filter_by(
user_id=user_id, movie_id=movie).first()
if rating is None:
return jsonify({'message': 'Rating not found!'}), 200
else:
return jsonify({'message': 'Rating found!', 'movie': rating.movie_id, 'value': rating.value}), 200
except Exception as e:
print(e)
return jsonify({'message': str(e)}), 500
app.run(host="0.0.0.0", port=8084)

View File

@ -0,0 +1,8 @@
flask==3.0.3
psycopg2==2.9.9
pandas==2.2.2
Flask-Caching==2.3.0
Flask-SQLAlchemy==3.1.1
firebase-admin==6.5.0
python-dotenv==1.0.1
requests==2.3.2

View File

@ -0,0 +1,29 @@
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set the working directory to /app
WORKDIR /app
# Install necessary OS packages
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy the Python script, requirements, and constants.ini file into the container at /app
COPY Include/frontend_AI_connector.py /app/
COPY Include/requirements.txt /app/
COPY Include/init_scripts/constants.ini /app/init_scripts/
COPY Include/init_scripts/movies.csv /app/init_scripts/
# tls
# COPY ./certs/connector.crt /app/
# COPY ./certs/connector.key /app/
# Modify requirements.txt to use psycopg2-binary
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Run frontend_AI_connector.py when the container launches
CMD ["python", "./frontend_AI_connector.py"]

View File

@ -0,0 +1,164 @@
from flask import Flask, request, jsonify
from flask_caching import Cache
import psycopg2
import pandas
import json
from configparser import ConfigParser
from datetime import datetime
import requests
app = Flask(__name__)
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
db_connector = None
conn = None
movie_list = None
def error_decorator(fun):
def inner1(*args, **kwargs):
try:
fun(*args, **kwargs)
except psycopg2.DatabaseError:
return jsonify({"status": "Something... unexpected has occurred :sweat_smile:"}), 500
return inner1
@app.route("/", methods=["GET"])
@cache.cached(timeout=69)
def hello():
return jsonify({"response": "Hello there", "time": datetime.now()}), 200
# endpoint do wyciągania danych o userze
@app.route("/api/v3/get/<string:username>", methods=["GET"])
def access_user(username):
cursor = conn.cursor()
cursor.execute("select * from users where username='{}';".format(username))
res = cursor.fetchall()
cursor.close()
return jsonify(res[0]), 200
# endpoint służący do zapisu danych nowo stworzonego użytkownika, podajemy mu
# id z oautha oraz login
@app.route("/api/v3/add/<string:oauth_ID>/<string:username>", methods=["POST"])
def add_user(oauth_ID, username):
cursor = conn.cursor()
cursor.execute("select * from users where username='{}';".format(username))
res = cursor.fetchall()
if len(res):
cursor.close()
return jsonify({"status": "User already exists"}), 409
cursor.execute("INSERT INTO users (username, oauth_ID) VALUES ('{}','{}');".format(
username, oauth_ID
))
conn.commit()
cursor.close()
return jsonify({"status": "success"}), 200
# roboczy endpoint służący do wyciąganiu rekomendacji
@app.route("/api/v3/ai/<string:oauth_ID>", methods=["GET"])
def get_recommendations(oauth_ID):
cursor = conn.cursor()
cursor.execute("select movie_ID from ratings where oauth_ID='{}'", oauth_ID)
res = cursor.fetchall()
movies = [int(i) for i in res[0]]
url = 'http://localhost:8081/api/v3/AI_recommendations'
response = requests.post(url,
json=movies,
headers={'Content-Type': 'application/json'})
return jsonify(response.json()), 200
@app.route("/api/v3/get_movie/<int:movie_ID>", methods=["GET"])
def get_movie(movie_ID):
movie_info = movie_list.loc[movie_list['movie_id'] == movie_ID]
if movie_info.empty:
return jsonify({"status": "Movie with ID {} doesn't exist".format(movie_ID)}
), 404
cast = json.loads(movie_info["cast"][0].replace('\\"', '"'))
crew = json.loads(movie_info["crew"][0].replace('\\"', '"'))
output_json = {"movie_id": movie_ID,
"title": movie_info["title"][0],
"cast": cast,
"crew": crew}
return jsonify(output_json), 200
@app.route("/api/v3/rate_movie/<string:uID>/<string:movie_ID>/<int:rating>", methods=["POST"])
def rate_movie(uID, movie_ID, rating):
movie_info = movie_list.loc[movie_list['movie_id'] == int(movie_ID)]
if movie_info.empty:
return jsonify({"status": "Movie with ID {} doesn't exist".format(movie_ID)}
), 404
if rating < 1 or rating > 5:
return jsonify({"status": "Incorrect rating"}), 400
cursor = conn.cursor()
cursor.execute("select * from users where oauth_ID='{}';".format(uID))
res = cursor.fetchall()
if not len(res):
cursor.close()
return jsonify({"status": "User doesn't exists"}), 404
cursor.execute("select * from ratings where oauth_ID='{}' AND movie_ID='{}';".format(uID, movie_ID))
res = cursor.fetchall()
if len(res):
sql = """ UPDATE ratings
SET rating = {},
rdate = CURRENT_TIMESTAMP
WHERE oauth_ID = '{}' AND
movie_ID = '{}'
"""
cursor.execute(sql.format(rating, uID, movie_ID))
else:
cursor.execute("INSERT INTO ratings (movie_ID, oauth_ID, rating) VALUES ('{}','{}',{});".format(
movie_ID, uID, rating
))
conn.commit()
cursor.close()
return jsonify({"status": "success"}), 200
if __name__ == "__main__":
config = ConfigParser()
config.read("init_scripts/constants.ini")
while True:
try:
conn = psycopg2.connect(
host=config["postgres"]["host"],
database=config["postgres"]["database"],
user=config["postgres"]["user"],
password=config["postgres"]["password"],
port=int(config["postgres"]["port"])
)
except Exception:
print("Trying to connect with database")
continue
else:
break
movie_list = pandas.read_csv(config["movie"]["csv_path"])
cache.init_app(app)
app.run(host="localhost", port=8090, debug=True)
conn.close()

View File

@ -0,0 +1,9 @@
[postgres]
host=db
database=test_db
user=root
password=root
port=5432
[movie]
csv_path=../../movie_recommendations/datasets/tmdb_5000_credits.csv

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
flask==3.0.3
psycopg2==2.9.9
pandas==2.2.2
Flask-Caching==2.3.0

View File

@ -0,0 +1,20 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
oauth_ID VARCHAR(255) NOT NULL
);
CREATE TABLE ratings (
id SERIAL PRIMARY KEY,
movie_ID VARCHAR(255) NOT NULL,
oauth_ID VARCHAR(255) NOT NULL,
rating INTEGER NOT NULL,
rdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (username, oauth_ID) VALUES
('Mkyong', 40),
('Ali', 28),
('Teoh', 18);

View File

@ -0,0 +1,97 @@
version: '3.8'
services:
app:
build: ./connector
ports:
- "5000:8090" # Adjust if your app uses a different port
depends_on:
- db
networks:
- my-bridge-network
db:
image: postgres:13
environment:
POSTGRES_DB: test_db
POSTGRES_USER: root
POSTGRES_PASSWORD: root
ports:
- 5432:5432
volumes:
- postgres_data:/var/lib/postgresql/data
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- my-bridge-network
pgadmin:
container_name: admin_container
image: dpage/pgadmin4
ports:
- 8080:80
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: admin
networks:
- my-bridge-network
nginx:
image: nginx:latest
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl:/etc/ssl/certs
depends_on:
- app
networks:
- my-bridge-network
frontend:
container_name: frontend
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "8000:80"
depends_on:
- app
networks:
- my-bridge-network
movie_rec:
container_name: movie_rec
build: ./movie_recommendations
ports:
- "8081:8081"
depends_on:
- db
networks:
- my-bridge-network
analitics:
container_name: analytics
build: ./analytics
ports:
- "8082:8082"
depends_on:
- db
networks:
- my-bridge-network
firebase:
container_name: firebase
build: .
ports:
- "8084:8084"
depends_on:
- db
networks:
- my-bridge-network
volumes:
postgres_data:
networks:
my-bridge-network:
driver: bridge

View File

@ -0,0 +1,12 @@
1. Receive data from database
2. Calculate data:
a) User Count
b) Movies Count
c) Rating Count
d) Total data size
e) "Hot" movies -> movies that received most ratings during last
week
f) System logs (keeps tracks of all messages exchanged by all services)
1. timestamp (when was a message send)
2. Message raw data
3. Send requests to frondend upon request

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M256,0C114.62,0,0,114.62,0,256s114.62,256,256,256s256-114.62,256-256S397.38,0,256,0z M172.211,41.609
c-24.934,27.119-44.68,66.125-56.755,111.992H49.749C75.179,102.741,118.869,62.524,172.211,41.609z M25.6,256
c0-26.999,5.077-52.727,13.662-76.8h70.494c-4.608,24.294-7.356,49.963-7.356,76.8s2.748,52.506,7.347,76.8H39.262
C30.677,308.727,25.6,283,25.6,256z M49.749,358.4h65.707c12.083,45.867,31.821,84.872,56.755,111.991
C118.869,449.476,75.179,409.259,49.749,358.4z M243.2,485.188c-43.81-8.252-81.877-58.24-101.359-126.788H243.2V485.188z
M243.2,332.8H135.74c-4.924-24.166-7.74-49.997-7.74-76.8s2.816-52.634,7.74-76.8H243.2V332.8z M243.2,153.6H141.841
C161.323,85.052,199.39,35.063,243.2,26.812V153.6z M462.251,153.6h-65.707c-12.083-45.867-31.821-84.873-56.755-111.992
C393.131,62.524,436.821,102.741,462.251,153.6z M268.8,26.812c43.81,8.252,81.877,58.24,101.359,126.788H268.8V26.812z
M268.8,179.2h107.46c4.924,24.166,7.74,49.997,7.74,76.8s-2.816,52.634-7.74,76.8H268.8V179.2z M268.8,485.188V358.4h101.359
C350.677,426.948,312.61,476.937,268.8,485.188z M339.789,470.391c24.934-27.127,44.672-66.125,56.755-111.991h65.707
C436.821,409.259,393.131,449.476,339.789,470.391z M402.244,332.8c4.608-24.294,7.356-49.963,7.356-76.8
s-2.748-52.506-7.347-76.8h70.494c8.576,24.073,13.653,49.801,13.653,76.8c0,27-5.077,52.727-13.662,76.8H402.244z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
<g id="Bell">
<path d="M46.6674995,8.6586504c-0.5527992,0-1,0.4473-1,1c0,0.5527992,0.4472008,1,1,1
c3.5185013,0,6.3808022,2.8622999,6.3808022,6.3809004c0,0.5527,0.4473,1,1,1c0.5527992,0,1-0.4473,1-1
C55.0483017,12.4184504,51.2886009,8.6586504,46.6674995,8.6586504z"/>
<path d="M8.9517002,17.0395508c0,0.5527,0.4471998,1,1,1c0.5527,0,1-0.4473,1-1
c0-3.5186005,2.8622999-6.3809004,6.3808002-6.3809004c0.5527992,0,1-0.4472008,1-1c0-0.5527-0.4472008-1-1-1
C12.7114,8.6586504,8.9517002,12.4184504,8.9517002,17.0395508z"/>
<path d="M48.1431007,1.27785c-0.5527992,0-1,0.4473-1,0.9999999s0.4472008,1,1,1
c6.7743988,0,12.2860985,5.5107002,12.2860985,12.2851c0,0.5527992,0.4473,1,1,1s1-0.4472008,1-1
C62.4291992,7.6860499,56.0200005,1.27785,48.1431007,1.27785z"/>
<path d="M16.8560009,2.2778499c0-0.5526999-0.4473-0.9999999-1.000001-0.9999999
c-7.8769999,0-14.2852001,6.4081998-14.2852001,14.2851c0,0.5527992,0.4473001,1,1.0000001,1s1-0.4472008,1-1
c0-6.7743998,5.5107002-12.2851,12.2852001-12.2851C16.4087009,3.2778499,16.8560009,2.83055,16.8560009,2.2778499z"/>
<path d="M51.7932014,46.2020493c-0.1280022-0.3828011-0.3692017-0.6965981-0.6621017-0.9463997
c0.0236015-0.6092987,0.0386009-1.2222977,0.0386009-1.8409996c0-13.6198997-5.641201-25.1546001-13.4345016-29.1104012
c0.1161003-0.4706001,0.1844025-0.9601002,0.1844025-1.4666996c0-3.3774004-2.7380028-6.1154003-6.1154022-6.1154003
s-6.1153984,2.7379999-6.1153984,6.1154003c0,0.5065994,0.0682983,0.9960995,0.1843987,1.4666996
C18.0799007,18.2600498,12.4386997,29.7947502,12.4386997,43.41465c0,0.6187019,0.0150003,1.2317009,0.0386,1.8409996
c-0.2929001,0.2498016-0.5340996,0.5635986-0.6620998,0.9463997l-2.8975,8.6665993
c-0.4692001,1.4033012,0.5754004,2.8535004,2.0555,2.8535004h14.1247005c0.8610001,2.8908005,3.535799,5,6.7062988,5
s5.8453026-2.1091995,6.7063026-5h14.1246986c1.4800987,0,2.5247002-1.4501991,2.0555-2.8535004L51.7932014,46.2020493z
M26.7959003,16.1219501l1.4443989-0.7332001l-0.3878994-1.5727005c-0.0841999-0.3413-0.1252003-0.6613998-0.1252003-0.9784994
c0-2.2480001,1.8290005-4.0769005,4.0769997-4.0769005s4.0769997,1.8289003,4.0769997,4.0769005
c0,0.3170996-0.0410004,0.6371994-0.1251984,0.9784994l-0.3879013,1.5727005L36.8125,16.1219501
c7.2531013,3.6816006,12.3186989,14.9046993,12.3186989,27.2926998c0,0.4183006-0.0060997,0.8419991-0.0181007,1.2691002
H14.4953003c-0.0120001-0.4271011-0.0181007-0.8507996-0.0181007-1.2691002
C14.4771996,31.0266495,19.5428009,19.8035507,26.7959003,16.1219501z M31.8041992,60.7221489
c-2.0464001,0-3.8094997-1.2355003-4.5824986-3h9.164999C35.6137009,59.4866486,33.8506012,60.7221489,31.8041992,60.7221489z
M52.7711983,55.6526489c-0.0500984,0.0695-0.1102982,0.0695-0.1359978,0.0695H38.8041992h-14H10.9731998
c-0.0256996,0-0.0859003,0-0.1359997-0.0695c-0.0496998-0.0690994-0.0307999-0.1256981-0.0227003-0.1497993l2.8975-8.6665993
c0.0229006-0.0681992,0.0866003-0.1141014,0.1588001-0.1141014h35.8667984c0.0722008,0,0.1359024,0.0459023,0.158802,0.1140022
l2.8974991,8.6666985C52.8019981,55.5269508,52.8209,55.5835495,52.7711983,55.6526489z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M118.922624,0.37140771 C175.483691,-3.5505123 212.986837,24.1282078 234.430251,57.8515157 C245.182251,74.7603157 255.731584,100.441382 255.780224,126.827729 C255.832277,155.497169 246.544597,180.324476 234.430251,198.541009 C221.979264,217.264422 205.875157,232.728956 185.709184,242.883196 C151.999957,259.857276 104.889984,260.321489 74.033024,243.978022 C39.6684361,225.777276 13.2466761,199.798822 3.41456926,154.746662 C-0.520150741,136.717436 -0.972417408,113.421862 4.50939593,93.4346624 C5.79579605,88.7460224 8.13350272,83.8116224 9.98395605,79.2014891 C18.8765427,57.0472491 34.0125427,37.7823945 52.6834773,24.4580211 C60.185984,19.1042078 69.2876373,13.7794078 77.3179307,10.2248478 C87.3096107,5.80244779 104.132224,1.3966877 118.922624,0.37140771 L118.922624,0.37140771 Z" fill="#FFFFFF">
</path>
<path d="M226.211797,130.015782 C226.211797,183.471996 182.876971,226.803836 129.421611,226.803836 C75.9662507,226.803836 32.6322761,183.471996 32.6322761,130.015782 C32.6322761,76.5608491 75.9662507,33.2255945 129.421611,33.2255945 C182.876544,33.2255945 226.211797,76.5608491 226.211797,130.015782 L226.211797,130.015782 Z" fill="#000000">
</path>
<path d="M118.922624,0.37140771 C175.483691,-3.5505123 212.986837,24.1282078 234.430251,57.8515157 C245.182251,74.7603157 255.731584,100.441382 255.780224,126.827729 C255.832277,155.497169 246.544597,180.324476 234.430251,198.541009 C221.979264,217.264422 205.875157,232.728956 185.709184,242.883196 C151.999957,259.857276 104.889984,260.321489 74.033024,243.978022 C39.6684361,225.777276 13.2466761,199.798822 3.41456926,154.746662 C-0.520150741,136.717436 -0.972417408,113.421862 4.50939593,93.4346624 C5.79579605,88.7460224 8.13350272,83.8116224 9.98395605,79.2014891 C18.8765427,57.0472491 34.0125427,37.7823945 52.6834773,24.4580211 C60.185984,19.1042078 69.2876373,13.7794078 77.3179307,10.2248478 C87.3096107,5.80244779 104.132224,1.3966877 118.922624,0.37140771 L118.922624,0.37140771 Z M99.762304,9.67786112 C78.753664,15.1246878 63.3497173,24.8829811 49.9464107,35.4071411 C30.6188361,50.5828224 18.2975561,71.7604224 11.0787827,97.2665557 C3.04763593,125.643302 8.20646272,159.982289 19.2904094,181.570769 C30.7843827,203.958822 46.217344,221.337382 68.0114773,233.576742 C89.2146773,245.484156 119.036971,253.130022 150.126464,247.262502 C177.748864,242.049489 198.727637,230.016209 215.818197,212.226769 C238.684117,188.425169 257.061931,144.585596 244.832384,98.3613824 C241.563264,86.0072491 237.289344,73.1313024 230.598784,62.2308224 C226.984064,56.3419691 221.679744,50.5486891 216.365611,44.7131691 C196.309717,22.6882078 163.894571,3.70879437 122.207104,6.39295445 C114.273664,6.90367445 107.301504,7.72287445 99.762304,9.67786112 L99.762304,9.67786112 Z" fill="#000000">
</path>
<g transform="translate(7.680000, 9.386667)" fill="#FFFFFF">
<g transform="translate(0.000000, 2.986667)">
<path d="M127.896362,234.025436 L239.741909,122.182449 L239.138518,121.579044 L127.292971,233.422031 L127.896362,234.025436 L127.896362,234.025436 Z">
</path>
<path d="M118.118869,225.167835 L230.452096,112.836742 L229.848704,112.233338 L117.515477,224.564432 L118.118869,225.167835 L118.118869,225.167835 Z">
</path>
<path d="M108.34095,216.311515 L221.16271,103.491461 L220.559317,102.888059 L107.737557,215.708112 L108.34095,216.311515 L108.34095,216.311515 Z">
</path>
<path d="M98.5630294,207.453915 L211.872896,94.1461817 L211.269504,93.5427783 L97.9596373,206.850512 L98.5630294,207.453915 L98.5630294,207.453915 Z">
</path>
<path d="M88.7855366,198.596741 L202.58351,84.8004745 L201.980117,84.1970722 L88.1821434,197.993339 L88.7855366,198.596741 L88.7855366,198.596741 Z">
</path>
<path d="M79.00762,189.741698 L193.293273,75.4551911 L192.689873,74.8517956 L78.40422,189.138302 L79.00762,189.741698 L79.00762,189.741698 Z">
</path>
<path d="M69.2296967,180.882394 L184.003883,66.1099145 L183.40049,65.5065122 L68.6263033,180.278992 L69.2296967,180.882394 L69.2296967,180.882394 Z">
</path>
<path d="M59.4517778,172.02522 L174.713644,56.7642067 L174.110249,56.1608067 L58.8483822,171.42182 L59.4517778,172.02522 L59.4517778,172.02522 Z">
</path>
<path d="M49.6738606,163.168044 L165.421701,47.4189239 L164.818299,46.8155294 L49.0704594,162.564649 L49.6738606,163.168044 L49.6738606,163.168044 Z">
</path>
<path d="M39.8963639,154.310447 L156.134444,38.0736472 L155.531049,37.4702461 L39.2929694,153.707046 L39.8963639,154.310447 L39.8963639,154.310447 Z">
</path>
<path d="M30.1184445,145.4537 L146.844631,28.7283667 L146.241236,28.1249667 L29.5150489,144.8503 L30.1184445,145.4537 L30.1184445,145.4537 Z">
</path>
<path d="M20.3405245,136.596527 L137.555244,19.38266 L136.951849,18.77926 L19.7371289,135.993127 L20.3405245,136.596527 L20.3405245,136.596527 Z">
</path>
<path d="M10.5626039,127.738927 L128.265431,10.0373805 L127.662036,9.43397947 L9.95920941,127.135526 L10.5626039,127.738927 L10.5626039,127.738927 Z">
</path>
<path d="M0.784683926,118.881754 L118.975617,0.692100527 L118.372223,0.088699473 L0.181289407,118.278353 L0.784683926,118.881754 L0.784683926,118.881754 Z">
</path>
</g>
<g>
<path d="M0.14330767,122.999079 L114.223734,237.084625 L114.827146,236.481241 L0.746718997,122.395695 L0.14330767,122.999079 L0.14330767,122.999079 Z">
</path>
<path d="M9.39472045,113.615398 L123.697014,227.923238 L124.300426,227.319855 L9.99813288,113.012015 L9.39472045,113.615398 L9.39472045,113.615398 Z">
</path>
<path d="M18.6461344,104.231719 L133.170294,218.760999 L133.773706,218.157615 L19.2495456,103.628335 L18.6461344,104.231719 L18.6461344,104.231719 Z">
</path>
<path d="M27.8975477,94.8480388 L142.644001,209.599612 L143.247412,208.996228 L28.500959,94.2446545 L27.8975477,94.8480388 L27.8975477,94.8480388 Z">
</path>
<path d="M37.1485349,85.4643594 L152.116855,200.437373 L152.720265,199.833987 L37.7519451,84.8609739 L37.1485349,85.4643594 L37.1485349,85.4643594 Z">
</path>
<path d="M46.3999494,76.0806805 L161.590989,191.275561 L162.194397,190.672173 L47.0033573,75.4772928 L46.3999494,76.0806805 L46.3999494,76.0806805 Z">
</path>
<path d="M55.6513628,66.6970005 L171.064269,182.113747 L171.667677,181.510359 L56.2547706,66.0936128 L55.6513628,66.6970005 L55.6513628,66.6970005 Z">
</path>
<path d="M64.9023505,57.3133217 L180.537551,172.951508 L181.140956,172.348118 L65.5057561,56.7099317 L64.9023505,57.3133217 L64.9023505,57.3133217 Z">
</path>
<path d="M74.1537644,47.9296422 L190.011258,163.789696 L190.614662,163.186304 L74.7571689,47.3262511 L74.1537644,47.9296422 L74.1537644,47.9296422 Z">
</path>
<path d="M83.4051783,38.5459628 L199.484112,154.627029 L200.087515,154.023637 L84.0085817,37.9425705 L83.4051783,38.5459628 L83.4051783,38.5459628 Z">
</path>
<path d="M92.6565911,29.1622822 L208.957818,145.466069 L209.561222,144.862678 L93.2599955,28.5588911 L92.6565911,29.1622822 L92.6565911,29.1622822 Z">
</path>
<path d="M101.907579,19.7786034 L218.431099,136.30383 L219.034501,135.700437 L102.510981,19.17521 L101.907579,19.7786034 L101.907579,19.7786034 Z">
</path>
<path d="M111.158992,10.39535 L227.904379,127.142443 L228.507781,126.53905 L111.762394,9.79195665 L111.158992,10.39535 L111.158992,10.39535 Z">
</path>
<path d="M120.410833,1.01167058 L237.378086,117.980204 L237.981487,117.376809 L121.014234,0.408276091 L120.410833,1.01167058 L120.410833,1.01167058 Z">
</path>
</g>
</g>
<path d="M209.796224,45.2605824 C199.729877,35.1942345 187.353984,26.1233011 172.570837,20.6261278 C157.247531,14.9279945 139.913771,10.1250078 119.470037,11.8670878 C86.9042773,14.6429811 64.0392107,29.2541811 46.661504,46.3554091 C34.4895561,58.3332224 25.3337161,73.0109824 19.2899827,90.1497557 C10.4161694,115.314982 10.9546227,145.018236 20.9322227,170.622076 C29.4429427,192.461862 43.3727573,210.077222 63.0843307,223.722876 C81.6148907,236.551462 108.210731,246.500902 138.082091,243.978022 C174.464811,240.905169 201.085397,222.239356 218.554411,200.731089 C223.024171,195.228796 227.468331,189.009276 230.050091,181.571196 C232.579797,176.211836 233.817984,172.568529 234.977237,170.622502 C239.829291,159.750182 242.399957,148.278822 243.188437,135.587196 C245.614037,96.5506091 230.594517,66.0593024 209.796224,45.2605824 L209.796224,45.2605824 Z M199.073664,164.193062 C199.004117,164.341542 198.941824,164.479782 198.869291,164.632956 C197.064491,169.797756 193.955371,174.116476 190.830464,177.937702 C178.613291,192.872742 159.998251,205.834022 134.555264,207.968209 C113.665237,209.719249 95.0664107,202.811089 82.1081173,193.903142 C70.8475307,186.162129 62.287744,176.580049 56.2853973,165.064316 C55.9193173,164.371409 52.4334507,156.340262 52.4232107,155.986982 C47.6680107,141.237542 47.255424,117.520849 52.136064,103.288529 C55.3932373,93.7896491 60.8217173,83.5121024 68.0114773,75.3695957 C77.938304,64.1265024 87.872384,56.9222357 103.594624,51.8299691 C110.773291,49.5046357 117.199744,47.1379157 126.038997,46.9032491 C147.594197,46.3293824 169.544064,56.0074624 181.329451,66.6105557 C192.159957,76.3543424 204.503424,95.3346091 207.606571,113.141969 C210.800171,131.470289 207.391957,149.365116 199.073664,164.193062 L199.073664,164.193062 Z" fill="#000000">
</path>
<g transform="translate(83.626667, 76.373333)">
<path d="M77.9810133,105.400747 C72.67328,105.400747 68.01536,102.039467 66.38976,97.0363733 L60.4778667,79.02336 L30.9034667,79.02336 L25.41056,96.8721067 C23.7525333,101.97248 19.0592,105.386667 13.72416,105.386667 C12.4433067,105.386667 11.1709867,105.184 9.94261333,104.785493 C3.56565333,102.87104 -0.0546133333,96.0072533 1.89909333,89.49376 L26.8616533,10.4226133 C28.48256,5.39264 33.2544,1.88416 38.4669867,1.88416 L51.7405867,1.88416 C56.9826133,1.88416 61.7540267,5.33290667 63.34336,10.2711467 L89.5957333,89.3128533 C91.6949333,95.7751467 88.21888,102.71232 81.8513067,104.785067 C80.5922133,105.193813 79.2904533,105.400747 77.9810133,105.400747 L77.9810133,105.400747 L77.9810133,105.400747 Z" fill="#FFFFFF">
</path>
<path d="M77.9810133,103.69408 C73.2261841,103.69408 69.0608465,100.688308 67.6071126,96.2141657 L61.6940408,78.1975435 L61.4049446,77.3166933 L60.4778667,77.3166933 L30.9034667,77.3166933 L29.958141,77.3166933 L29.6800885,78.2202018 L24.1871818,96.0689485 C22.7072691,100.620899 18.5067226,103.68 13.72416,103.68 C12.5787502,103.68 11.4396612,103.498823 10.3376028,103.141295 C4.61174908,101.421988 1.37421306,95.2722447 3.12512866,89.4348397 L28.0822714,10.3812927 C29.5308212,5.88624847 33.8113885,2.73749333 38.4669867,2.73749333 L51.7405867,2.73749333 C56.4288544,2.73749333 60.7065138,5.82950865 62.1249103,10.2366284 L88.3809821,89.2896459 C90.2599306,95.0739946 87.1485772,101.287946 81.4551086,103.141261 C80.3246516,103.508247 79.1562581,103.69408 77.9810133,103.69408 L77.9810133,103.69408 Z M77.9810133,106.25408 C79.4253475,106.25408 80.8604464,106.025828 82.2465357,105.575854 C89.2922648,103.282357 93.1289211,95.6198406 90.8131148,88.4907346 L64.5581113,9.4410208 C62.8016101,3.98319054 57.536605,0.177493333 51.7405867,0.177493333 L38.4669867,0.177493333 C32.6951659,0.177493333 27.4336011,4.04786331 25.6433485,9.60334848 L0.678475274,88.6817473 C-1.48096359,95.8806734 2.51145487,103.464326 9.57456724,105.584772 C10.9041173,106.016429 12.3097398,106.24 13.72416,106.24 C19.6168848,106.24 24.7994128,102.465759 26.6278552,96.8411577 L32.1268448,78.9731849 L30.9034667,79.8766933 L60.4778667,79.8766933 L59.2616926,78.9958432 L65.1735859,97.0088565 C66.9698962,102.537361 72.1204498,106.25408 77.9810133,106.25408 L77.9810133,106.25408 Z" fill="#000000">
</path>
</g>
<g transform="translate(61.440000, 19.200000)" fill="#FFFFFF">
<path d="M2.13376,33.8577067 L2.10261333,33.8154667 C-1.01034667,29.5492267 -0.0968533333,23.5810133 4.48768,20.2350933 C9.07221333,16.8896 14.9614933,17.8286933 18.0744533,22.0945067 L18.1056,22.1367467 C21.21856,26.4029867 20.3050667,32.3716267 15.7205333,35.71712 C11.136,39.0626133 5.24672,38.1239467 2.13376,33.8577067 L2.13376,33.8577067 Z M13.93408,25.2462933 L13.9029333,25.2040533 C12.3387733,23.06048 9.42634667,22.3232 7.15562667,23.9803733 C4.90581333,25.6221867 4.73088,28.5469867 6.29504,30.6909867 L6.32618667,30.7332267 C7.89077333,32.8768 10.8027733,33.61408 13.0525867,31.9722667 C15.3237333,30.3150933 15.4986667,27.3902933 13.93408,25.2462933 L13.93408,25.2462933 Z">
</path>
<path d="M32.1467733,5.89525333 L36.8251733,4.39424 L49.94048,19.6407467 L44.7364267,21.3102933 L42.4571733,18.5924267 L35.70176,20.7598933 L35.4542933,24.2888533 L30.3505067,25.9264 L32.1467733,5.89525333 L32.1467733,5.89525333 Z M39.84384,15.264 L36.2948267,10.9111467 L35.91552,16.5243733 L39.84384,15.264 L39.84384,15.264 Z">
</path>
<path d="M58.3658667,10.48192 L58.4068267,0.155306667 L63.5831467,0.175786667 L63.5426133,10.39744 C63.5319467,13.0513067 64.8669867,14.3176533 66.9166933,14.3261867 C68.9664,14.3342933 70.3112533,13.1310933 70.3210667,10.55616 L70.3624533,0.20352 L75.5387733,0.224 L75.49824,10.4192 C75.4747733,16.3575467 72.0750933,18.9457067 66.8458667,18.9248 C61.6170667,18.9034667 58.3432533,16.2363733 58.3658667,10.48192 L58.3658667,10.48192 Z">
</path>
<path d="M94.2331733,8.67754667 L88.95616,7.06688 L90.2600533,2.79466667 L105.6896,7.50378667 L104.385707,11.776 L99.1086933,10.16576 L95.04384,23.4845867 L90.1687467,21.9968 L94.2331733,8.67754667 L94.2331733,8.67754667 Z">
</path>
<path d="M119.471787,13.4651733 L123.715413,16.28928 L119.901867,22.0202667 L125.348693,25.6452267 L129.162667,19.9138133 L133.406293,22.73792 L123.216213,38.05056 L118.97216,35.2264533 L122.844587,29.4075733 L117.397333,25.7826133 L113.525333,31.6014933 L109.281707,28.7773867 L119.471787,13.4651733 L119.471787,13.4651733 Z">
</path>
</g>
<g transform="translate(65.280000, 196.266667)" fill="#FFFFFF">
<path d="M130.622293,3.79008 L130.65472,3.83146667 C133.92896,7.97568 133.243733,13.9754667 128.790187,17.49376 C124.33664,21.0120533 118.41536,20.2986667 115.14112,16.1544533 L115.108693,16.1130667 C111.834453,11.9684267 112.520107,5.96906667 116.973227,2.45077333 C121.426773,-1.06709333 127.348053,-0.354133333 130.622293,3.79008 L130.622293,3.79008 Z M119.158187,12.8469333 L119.190613,12.88832 C120.83584,14.97088 123.774293,15.5963733 125.980587,13.85344 C128.165973,12.12672 128.229547,9.19722667 126.583893,7.11466667 L126.551467,7.07328 C124.906667,4.99072 121.967787,4.36522667 119.7824,6.09194667 C117.576107,7.83488 117.51296,10.7643733 119.158187,12.8469333 L119.158187,12.8469333 Z">
</path>
<path d="M101.84832,32.9309867 L97.24032,34.6363733 L83.4666667,19.98208 L88.5922133,18.0846933 L90.9892267,20.70016 L97.6426667,18.23744 L97.7344,14.7012267 L102.761387,12.8405333 L101.84832,32.9309867 L101.84832,32.9309867 Z M93.7463467,23.9104 L97.48352,28.1024 L97.61536,22.47808 L93.7463467,23.9104 L93.7463467,23.9104 Z">
</path>
<path d="M75.4722133,29.4336 L75.9274667,39.7499733 L70.7562667,39.97824 L70.30528,29.7668267 C70.1883733,27.11552 68.7940267,25.91488 66.7464533,26.0053333 C64.69888,26.0957867 63.4133333,27.36256 63.5268267,29.9349333 L63.98336,40.2773333 L58.81216,40.5056 L58.3624533,30.32064 C58.10048,24.3882667 61.37216,21.63968 66.59584,21.40928 C71.8199467,21.1784533 75.2183467,23.6846933 75.4722133,29.4336 L75.4722133,29.4336 Z">
</path>
<path d="M39.5396267,32.8068267 L44.8674133,34.24512 L43.70304,38.5578667 L28.1250133,34.3530667 L29.2893867,30.0398933 L34.6171733,31.4781867 L38.2468267,18.03136 L43.1688533,19.36 L39.5396267,32.8068267 L39.5396267,32.8068267 Z">
</path>
<path d="M14.3573333,29.0594133 L9.98442667,26.4388267 L13.5236267,20.5333333 L7.91168,17.1694933 L4.37248,23.0749867 L0,20.4544 L9.45664,4.67669333 L13.82912,7.29770667 L10.2357333,13.2932267 L15.8481067,16.6570667 L19.4414933,10.6615467 L23.8139733,13.28256 L14.3573333,29.0594133 L14.3573333,29.0594133 Z">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 407.392 407.392" xml:space="preserve">
<g>
<path d="M402.016,322.382l-88.896-88.902c12.424-17.657,19.143-38.649,19.143-60.69c0-28.254-10.998-54.816-30.978-74.795
c-19.979-19.979-46.545-30.982-74.795-30.982c-2.074,0-4.137,0.066-6.19,0.184v30.658c2.05-0.166,4.111-0.27,6.19-0.27
c20.09,0,38.971,7.823,53.176,22.028c14.207,14.206,22.029,33.091,22.029,53.177c0,20.09-7.822,38.976-22.029,53.177
c-14.205,14.206-33.086,22.029-53.176,22.029c-20.092,0-38.973-7.823-53.178-22.029c-4.686-4.684-8.66-9.885-11.905-15.463
c-3.989,2.305-8.61,3.635-13.54,3.635c-9.436,0-17.762-4.836-22.632-12.16c-0.104,0.157-0.22,0.307-0.327,0.462
c4.904,16.888,14.006,32.365,26.787,45.145c19.979,19.98,46.543,30.982,74.795,30.982c22.035,0,43.033-6.723,60.684-19.147
l88.906,88.903c3.58,3.583,8.273,5.373,12.973,5.373c4.693,0,9.385-1.79,12.965-5.373
C409.184,341.159,409.184,329.544,402.016,322.382z"/>
<path d="M102.602,135.172c-6.662,0-12.07,5.404-12.07,12.071v39.736c0,6.666,5.408,12.071,12.07,12.071
c6.67,0,12.07-5.405,12.07-12.071v-39.736C114.672,140.577,109.271,135.172,102.602,135.172z"/>
<path d="M12.07,135.172c-6.66,0-12.07,5.404-12.07,12.071v39.736c0,6.666,5.41,12.071,12.07,12.071
c6.67,0,12.07-5.405,12.07-12.071v-39.736C24.141,140.577,18.74,135.172,12.07,135.172z"/>
<path d="M57.336,85.381c-6.662,0-12.072,5.405-12.072,12.071v89.527c0,6.666,5.41,12.071,12.072,12.071
c6.67,0,12.07-5.405,12.07-12.071V97.452C69.406,90.786,64.006,85.381,57.336,85.381z"/>
<path d="M193.139,199.05c6.662,0,12.07-5.405,12.07-12.071V65.766c0-6.666-5.408-12.07-12.07-12.07
c-6.67,0-12.078,5.404-12.078,12.07v121.213C181.061,193.645,186.469,199.05,193.139,199.05z"/>
<path d="M147.865,199.05c6.67,0,12.072-5.405,12.072-12.071V97.452c0-6.666-5.402-12.071-12.072-12.071
c-6.66,0-12.07,5.405-12.07,12.071v89.527C135.795,193.645,141.205,199.05,147.865,199.05z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 419.931 419.931"
xml:space="preserve">
<g>
<g>
<g>
<path d="M282.895,352.367c-2.176-1.324-4.072-3.099-5.579-5.25c-0.696-0.992-1.284-2.041-1.771-3.125H28.282V100.276h335.624
v159.138c7.165,0.647,13.177,5.353,15.701,11.797c2.235-1.225,4.726-1.982,7.344-2.213c1.771-0.154,3.53-0.044,5.236,0.293
V39.561c0-12.996-10.571-23.569-23.566-23.569H23.568C10.573,15.992,0,26.565,0,39.561v309.146
c0,12.996,10.573,23.568,23.568,23.568h257.179c-2.007-4.064-2.483-8.652-1.302-13.066
C280.126,356.67,281.304,354.354,282.895,352.367z M338.025,55.569c0-4.806,3.896-8.703,8.702-8.703h8.702
c4.807,0,8.702,3.896,8.702,8.703v9.863c0,4.806-3.896,8.702-8.702,8.702h-8.702c-4.807,0-8.702-3.896-8.702-8.702V55.569z
M297.56,55.569c0-4.806,3.896-8.703,8.702-8.703h8.703c4.807,0,8.702,3.896,8.702,8.703v9.863c0,4.806-3.896,8.702-8.702,8.702
h-8.703c-4.806,0-8.702-3.896-8.702-8.702V55.569z M257.094,55.569c0-4.806,3.897-8.703,8.702-8.703h8.702
c4.807,0,8.703,3.896,8.703,8.703v9.863c0,4.806-3.896,8.702-8.703,8.702h-8.702c-4.805,0-8.702-3.896-8.702-8.702V55.569z"/>
<path d="M419.875,335.77l-2.615-14.83c-0.353-1.997-2.256-3.331-4.255-2.979l-13.188,2.324c-1.583-3.715-3.605-7.195-6.005-10.38
l8.614-10.268c0.626-0.744,0.931-1.709,0.847-2.68c-0.086-0.971-0.554-1.867-1.3-2.494l-11.534-9.68
c-0.746-0.626-1.713-0.93-2.683-0.845c-0.971,0.085-1.867,0.552-2.493,1.298l-8.606,10.26c-3.533-1.8-7.312-3.188-11.271-4.104
v-13.392c0-2.028-1.645-3.674-3.673-3.674h-15.06c-2.027,0-3.673,1.646-3.673,3.674v13.392
c-3.961,0.915-7.736,2.304-11.271,4.104l-8.608-10.259c-1.304-1.554-3.62-1.756-5.175-0.453l-11.535,9.679
c-0.746,0.627-1.213,1.523-1.299,2.494c-0.084,0.971,0.22,1.937,0.846,2.683l8.615,10.266c-2.396,3.184-4.422,6.666-6.005,10.38
l-13.188-2.325c-1.994-0.351-3.901,0.982-4.255,2.979l-2.614,14.83c-0.169,0.959,0.05,1.945,0.607,2.744
c0.561,0.799,1.41,1.342,2.37,1.511l13.198,2.326c0.215,4.089,0.927,8.045,2.073,11.812l-11.6,6.695
c-0.844,0.485-1.459,1.289-1.712,2.229c-0.252,0.941-0.119,1.943,0.367,2.787l7.529,13.041c0.485,0.844,1.289,1.459,2.229,1.711
c0.313,0.084,0.632,0.125,0.951,0.125c0.639,0,1.272-0.166,1.836-0.492l11.609-6.703c2.73,2.925,5.812,5.517,9.18,7.709
l-4.584,12.593c-0.332,0.916-0.289,1.926,0.123,2.809s1.157,1.566,2.072,1.898l14.148,5.149c0.406,0.148,0.832,0.224,1.257,0.224
c0.53,0,1.063-0.115,1.554-0.345c0.883-0.411,1.564-1.157,1.897-2.073l4.583-12.593c1.965,0.238,3.965,0.361,5.994,0.361
s4.029-0.125,5.994-0.361l4.584,12.593c0.332,0.916,1.016,1.662,1.897,2.073c0.49,0.229,1.021,0.345,1.554,0.345
c0.424,0,0.85-0.074,1.256-0.224l14.15-5.149c0.913-0.332,1.659-1.017,2.07-1.898c0.412-0.883,0.456-1.893,0.123-2.809
l-4.584-12.591c3.365-2.192,6.447-4.786,9.18-7.709l11.609,6.703c0.563,0.324,1.197,0.492,1.836,0.492
c0.318,0,0.64-0.043,0.951-0.125c0.941-0.252,1.743-0.869,2.229-1.711l7.529-13.043c0.486-0.842,0.619-1.846,0.367-2.787
c-0.253-0.938-0.868-1.742-1.712-2.229l-11.598-6.693c1.146-3.768,1.856-7.724,2.071-11.812l13.198-2.327
c0.96-0.169,1.812-0.712,2.37-1.511C419.825,337.715,420.044,336.729,419.875,335.77z M354.184,359.336
c-11.155,0-20.2-9.045-20.2-20.201s9.046-20.2,20.2-20.2c11.156,0,20.201,9.044,20.201,20.2S365.34,359.336,354.184,359.336z"/>
<g>
<path d="M164.695,235.373c0-4.752-2.785-9.117-7.096-11.119l-39.455-18.332l39.456-18.334c4.31-2.004,7.095-6.368,7.095-11.118
v-0.319c0-4.21-2.119-8.075-5.665-10.334c-1.962-1.253-4.247-1.916-6.606-1.916c-1.778,0-3.563,0.391-5.16,1.133l-63.078,29.333
c-4.309,2.004-7.092,6.368-7.092,11.117v0.877c0,4.743,2.782,9.104,7.093,11.118l63.084,29.336
c1.631,0.755,3.368,1.138,5.162,1.138c2.338,0,4.616-0.664,6.597-1.924c3.548-2.268,5.666-6.13,5.666-10.335L164.695,235.373
L164.695,235.373z"/>
<path d="M226.932,134.012c-2.301-3.15-6.002-5.03-9.901-5.03h-0.314c-5.354,0-10.048,3.425-11.679,8.516L163.478,266.27
c-1.183,3.718-0.517,7.813,1.781,10.962c2.301,3.148,6.002,5.029,9.901,5.029h0.315c5.352,0,10.043-3.426,11.672-8.516
l41.555-128.762C229.896,141.268,229.234,137.167,226.932,134.012z"/>
<path d="M308.001,194.366l-63.079-29.333c-1.592-0.74-3.374-1.131-5.152-1.131c-2.358,0-4.644,0.661-6.605,1.912
c-3.552,2.263-5.671,6.127-5.671,10.337v0.319c0,4.746,2.783,9.111,7.097,11.123l39.454,18.33l-39.455,18.331
c-4.311,2.002-7.096,6.367-7.096,11.119v0.321c0,4.205,2.119,8.066,5.669,10.336c1.974,1.258,4.254,1.923,6.595,1.923
c1.792,0,3.527-0.383,5.169-1.141l63.082-29.336c4.307-2.009,7.088-6.371,7.088-11.114v-0.877
C315.094,200.735,312.311,196.371,308.001,194.366z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,79 @@
\documentclass{article}[11pt]
\usepackage{graphicx}
\usepackage{float}
\title{ERSMS, Group F, Project Documentation}
\author{Hubert Dwornik, Michał Łezka, Jakub Mazur \\ Michał Sar, Krzysztof Rudnicki}
\begin{document}
\maketitle
\section{System Architecture}
We have designed and implemented 4 microservices,
all of microservices are written in Python using Flask framework
\paragraph{AI recommendations}
Based on a list of movies ids, calculates and returns list of ids recommended for user who likes given movies
\paragraph{Analytics}
Holds information about number of
ratings, number of users,
average movie ratings, ratings of
given user which are later used in
webinterface to show data for admins
concerning website usage
\paragraph{Notifications}
Notifies user whenever there is a new movie recommend by AI recommender for them
\paragraph{Backend}
Updates database and mantains all ongoing and incoming communication between all microservices, both between microservies and from microservices to the webinterface
\paragraph{Caching}
We implemented two caches
\begin{enumerate}
\item Backend is cached inside Analytics Service \\
Analytics service holds tata about users and movies,
in order to not pull all the data from backend every time we update analytics
(for example every day), we keep the cache of backend date in our analytics
service
\item AI recommendations are cached inside Notification Service \\
AI recommendations can change whenever new movie appears, notification
service keeps the cache of ai recommendation service in order to not
pull all the data from the ai recommendation every time it wants to notify
the users about new movies to recommend
\end{enumerate}
\paragraph{Database}
We use postgresql database to contain data about users, movies and user ratings
\begin{figure}[H]
\caption{System architecture representation, webinterface although not part of microservices included to show relation with backend}
\centering
\includegraphics[width=\textwidth]{images/systemArchitecture.drawio.pdf}
\end{figure}
\section{Automated Infrastructure Management solution}
We use \textbf{Dockerfiles}
for each microservice, webinterface and database which
later get combined in docker compose file, after each commit on
\textbf{GitHub} main repository docker compose gets automatically run
on \textbf{Google Cloud} platform and deployed
\section{Federated authorization and authentication management in the project}
We use industry standard \textbf{OAuth} protocol in our webinterface,
user creates their account and logs in, we use user token to
authorize their access on backend to their ratings and
recommendations. We use \textbf{firebase} services to manage OAuth protocol.
\section{Threat model with mitigations}
Our single most important asset are user likes for specific movies \\
We expect either bots or human agents trying to access those likes for a specific users or to modify user ratings to improve or decrease certain movies ratings \\
To mitigate that we use:
\begin{enumerate}
\item Certificates on our frontend, which encrypt data transmitted between website and user
\item OAuth which is used to authenticate user and lower amount of bots accessing our Infrastructure
\item TLS encryption between our microservices so that even our inside communication is encrypted
\item Google cloud default security policies allowing us to monitor odd and potentially harmfull behaviours
\end{enumerate}
\begin{figure}[H]
\caption{Threat model}
\centering
\includegraphics[width=\textwidth]{images/threat_model.png}
\end{figure}
\end{document}

View File

@ -0,0 +1,25 @@
One script upon being ran should deploy the entire solution to some
cloud service
It should:
1. Build everything
2. Connect to cloud service (Azure?)
3. Send the data
After it will be run website should be accessible under some address
(cloud service should provide this address?)
Decide:
What cloud service? (Azure?) Requirements:
a. Free (https://github.com/cloudcommunity/Cloud-Free-Tier-Comparison)
b. Popular
AWS:
+ Most popular
+ "Always" free
+ AWS CDK available
Azure
-Microsoft
Google Cloud
+We have google accounts anyway
What technology for script (Ansible?)

View File

@ -0,0 +1,12 @@
Monitoring service
Monitoring service should keep track of all communication
send between all services
Store logs with:
1. timestamp (when was a message send)
2. Message raw data
To decide:
What to use for Monitoring service?
Maybe cloud service will provide us with functioning one?
Maybe deployment tool already has one?

View File

@ -0,0 +1,19 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
};

View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@ -0,0 +1,19 @@
FROM node:18-alpine AS build-stage
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 8000
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Movie Recommendation</title>
</head>
<body>
<div id="root" class="flex min-h-dvh flex-col bg-gray-800 text-white"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "movie-recommendation-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@smastrom/react-rating": "^1.5.0",
"axios": "^1.6.8",
"firebase": "^10.11.1",
"react": "^18.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
"react-firebase-hooks": "^5.1.1",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-mkcert": "^1.17.5"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,26 @@
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import Layout from './Layout';
import Home from './pages/Home';
import Recommendations from './pages/Recommendations';
import Rate from './pages/Rate';
import Analytics from './pages/Analytics';
function App() {
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: '/', element: <Home /> },
{ path: '/recommendations', element: <Recommendations /> },
{ path: '/rate', element: <Rate /> },
{ path: '/analytics', element: <Analytics /> },
{ path: '*', element: <code>404</code> },
],
},
]);
return <RouterProvider router={router} />;
}
export default App;

View File

@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { useAuthState } from 'react-firebase-hooks/auth';
import { Triangle } from 'react-loader-spinner';
import { Outlet, useNavigate } from 'react-router-dom';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import { auth } from './firebase';
function Layout() {
const [user, userLoading] = useAuthState(auth);
const navigate = useNavigate();
useEffect(() => {
if (!userLoading && !user) {
navigate('/');
}
}, [navigate, user, userLoading]);
return (
<>
<Navbar />
{userLoading ? (
<Triangle wrapperClass="m-auto" height={120} width={120} />
) : (
<Outlet />
)}
<Footer />
</>
);
}
export default Layout;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#fff" d="M44.59 4.21a63.28 63.28 0 004.33 120.9 67.6 67.6 0 0032.36.35 57.13 57.13 0 0025.9-13.46 57.44 57.44 0 0016-26.26 74.33 74.33 0 001.61-33.58H65.27v24.69h34.47a29.72 29.72 0 01-12.66 19.52 36.16 36.16 0 01-13.93 5.5 41.29 41.29 0 01-15.1 0A37.16 37.16 0 0144 95.74a39.3 39.3 0 01-14.5-19.42 38.31 38.31 0 010-24.63 39.25 39.25 0 019.18-14.91A37.17 37.17 0 0176.13 27a34.28 34.28 0 0113.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.22 61.22 0 0087.2 4.59a64 64 0 00-42.61-.38z"/><path fill="#e33629" d="M44.59 4.21a64 64 0 0142.61.37 61.22 61.22 0 0120.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.28 34.28 0 00-13.64-8 37.17 37.17 0 00-37.46 9.74 39.25 39.25 0 00-9.18 14.91L8.76 35.6A63.53 63.53 0 0144.59 4.21z"/><path fill="#f8bd00" d="M3.26 51.5a62.93 62.93 0 015.5-15.9l20.73 16.09a38.31 38.31 0 000 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 01-5.5-40.9z"/><path fill="#587dbd" d="M65.27 52.15h59.52a74.33 74.33 0 01-1.61 33.58 57.44 57.44 0 01-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0012.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68z"/><path fill="#319f43" d="M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0044 95.74a37.16 37.16 0 0014.08 6.08 41.29 41.29 0 0015.1 0 36.16 36.16 0 0013.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 01-25.9 13.47 67.6 67.6 0 01-32.36-.35 63 63 0 01-23-11.59A63.73 63.73 0 018.75 92.4z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="790" height="512.20805" viewBox="0 0 790 512.20805" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M925.56335,704.58909,903,636.49819s24.81818,24.81818,24.81818,45.18181l-4.45454-47.09091s12.72727,17.18182,11.45454,43.27273S925.56335,704.58909,925.56335,704.58909Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><path d="M441.02093,642.58909,419,576.13509s24.22155,24.22155,24.22155,44.09565l-4.34745-45.95885s12.42131,16.76877,11.17917,42.23245S441.02093,642.58909,441.02093,642.58909Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><path d="M784.72555,673.25478c.03773,43.71478-86.66489,30.26818-192.8092,30.35979s-191.53562,13.68671-191.57335-30.028,86.63317-53.29714,192.77748-53.38876S784.68782,629.54,784.72555,673.25478Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><rect y="509.69312" width="790" height="2" fill="#3f3d56"/><polygon points="505.336 420.322 491.459 420.322 484.855 366.797 505.336 366.797 505.336 420.322" fill="#a0616a"/><path d="M480.00587,416.35743H508.3101a0,0,0,0,1,0,0V433.208a0,0,0,0,1,0,0H464.69674a0,0,0,0,1,0,0v-1.54149A15.30912,15.30912,0,0,1,480.00587,416.35743Z" fill="#2f2e41"/><polygon points="607.336 499.322 593.459 499.322 586.855 445.797 607.336 445.797 607.336 499.322" fill="#a0616a"/><path d="M582.00587,495.35743H610.3101a0,0,0,0,1,0,0V512.208a0,0,0,0,1,0,0H566.69674a0,0,0,0,1,0,0v-1.54149A15.30912,15.30912,0,0,1,582.00587,495.35743Z" fill="#2f2e41"/><path d="M876.34486,534.205A10.31591,10.31591,0,0,0,873.449,518.654l-32.23009-131.2928L820.6113,396.2276l38.33533,126.949a10.37185,10.37185,0,0,0,17.39823,11.0284Z" transform="translate(-205 -193.89598)" fill="#a0616a"/><path d="M851.20767,268.85955a11.38227,11.38227,0,0,0-17.41522,1.15247l-49.88538,5.72709,7.58861,19.24141,45.36779-8.49083a11.44393,11.44393,0,0,0,14.3442-17.63014Z" transform="translate(-205 -193.89598)" fill="#a0616a"/><path d="M769,520.58909l21.76811,163.37417,27.09338-5.578s-3.98437-118.98157,9.56238-133.32513S810,505.58909,810,505.58909Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><path d="M778,475.58909l-10,15s-77-31.99929-77,19-4.40631,85.60944-6,88,18.43762,8.59375,28,7c0,0,11.79687-82.21884,11-87,0,0,75.53355,37.03335,89.87712,33.84591S831.60944,536.964,834,530.58909s-1-57-1-57l-47.81-14.59036Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><path d="M779.34915,385.52862l-2.85032-3.42039s-31.92361-71.82815-19.3822-91.21035,67.26762-22.23252,68.97783-21.0924-4.08488,15.9428-.09446,22.78361c0,0-42.394,9.19121-45.24435,10.33134s21.96615,43.2737,21.96615,43.2737l-2.85031,25.6529Z" transform="translate(-205 -193.89598)" fill="#ccc"/><path d="M835.21549,350.18459S805.57217,353.605,804.432,353.605s-1.71017-7.41084-1.71017-7.41084l-26.223,35.91406S763.57961,486.29929,767,484.58909s66.50531,8.11165,67.07539,3.55114-.57008-27.3631,1.14014-28.50324,29.64328-71.82811,29.64328-71.82811-2.85032-14.82168-12.54142-19.95227S835.21549,350.18459,835.21549,350.18459Z" transform="translate(-205 -193.89598)" fill="#ccc"/><path d="M855.73783,378.11779l9.121,9.69109S878.41081,499.1687,871,502.58909s-22,3-22,3l-14.35458-52.79286Z" transform="translate(-205 -193.89598)" fill="#ccc"/><circle cx="601.72966" cy="122.9976" r="26.2388" fill="#a0616a"/><path d="M800.57267,320.98789c-.35442-5.44445-7.22306-5.631-12.67878-5.68255s-11.97836.14321-15.0654-4.35543c-2.0401-2.973-1.65042-7.10032.035-10.28779s4.45772-5.639,7.18508-7.99742c7.04139-6.08884,14.29842-12.12936,22.7522-16.02662s18.36045-5.472,27.12788-2.3435c10.77008,3.84307,25.32927,23.62588,26.5865,34.99176s-3.28507,22.95252-10.9419,31.44586-25.18188,5.0665-36.21069,8.088c6.7049-9.48964,2.28541-26.73258-8.45572-31.164Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><circle cx="361.7217" cy="403.5046" r="62.98931" fill="#f9c326"/><path d="M524.65625,529.9355a45.15919,45.15919,0,0,1-41.25537-26.78614L383.44873,278.05757a59.83039,59.83039,0,1,1,111.87012-41.86426l72.37744,235.41211a45.07978,45.07978,0,0,1-43.04,58.33008Z" transform="translate(-205 -193.89598)" fill="#f9c326"/></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 423.04 35.4"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="17.7" x2="423.04" y2="17.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M227.5,0h8.9l8.75,23.2h.1L254.15,0h8.35L247.9,35.4h-6.25Zm46.6,0h7.8V35.4h-7.8Zm22.2,0h24.05V7.2H304.1v6.6h15.35V21H304.1v7.2h17.15v7.2H296.3Zm55,0H363a33.54,33.54,0,0,1,8.07,1A18.55,18.55,0,0,1,377.75,4a15.1,15.1,0,0,1,4.52,5.53A18.5,18.5,0,0,1,384,17.8a16.91,16.91,0,0,1-1.63,7.58,16.37,16.37,0,0,1-4.37,5.5,19.52,19.52,0,0,1-6.35,3.37A24.59,24.59,0,0,1,364,35.4H351.29Zm7.81,28.2h4a21.57,21.57,0,0,0,5-.55,10.87,10.87,0,0,0,4-1.83,8.69,8.69,0,0,0,2.67-3.34,11.92,11.92,0,0,0,1-5.08,9.87,9.87,0,0,0-1-4.52,9,9,0,0,0-2.62-3.18,11.68,11.68,0,0,0-3.88-1.88,17.43,17.43,0,0,0-4.67-.62h-4.6ZM395.24,0h13.2a34.42,34.42,0,0,1,4.63.32,12.9,12.9,0,0,1,4.17,1.3,7.88,7.88,0,0,1,3,2.73A8.34,8.34,0,0,1,421.39,9a7.42,7.42,0,0,1-1.67,5,9.28,9.28,0,0,1-4.43,2.82v.1a10,10,0,0,1,3.18,1,8.38,8.38,0,0,1,2.45,1.85,7.79,7.79,0,0,1,1.57,2.62,9.16,9.16,0,0,1,.55,3.2,8.52,8.52,0,0,1-1.2,4.68,9.42,9.42,0,0,1-3.1,3,13.38,13.38,0,0,1-4.27,1.65,23.11,23.11,0,0,1-4.73.5h-14.5ZM403,14.15h5.65a8.16,8.16,0,0,0,1.78-.2A4.78,4.78,0,0,0,412,13.3a3.34,3.34,0,0,0,1.13-1.2,3.63,3.63,0,0,0,.42-1.8,3.22,3.22,0,0,0-.47-1.82,3.33,3.33,0,0,0-1.23-1.13,5.77,5.77,0,0,0-1.7-.58,10.79,10.79,0,0,0-1.85-.17H403Zm0,14.65h7a8.91,8.91,0,0,0,1.83-.2,4.78,4.78,0,0,0,1.67-.7,4,4,0,0,0,1.23-1.3,3.71,3.71,0,0,0,.47-2,3.13,3.13,0,0,0-.62-2A4,4,0,0,0,413,21.45,7.83,7.83,0,0,0,411,20.9a15.12,15.12,0,0,0-2.05-.15H403Zm-199,6.53H205a17.66,17.66,0,0,0,17.66-17.66h0A17.67,17.67,0,0,0,205,0h-.91A17.67,17.67,0,0,0,186.4,17.67h0A17.66,17.66,0,0,0,204.06,35.33ZM10.1,6.9H0V0H28V6.9H17.9V35.4H10.1ZM39,0h7.8V13.2H61.9V0h7.8V35.4H61.9V20.1H46.75V35.4H39ZM80.2,0h24V7.2H88v6.6h15.35V21H88v7.2h17.15v7.2h-25Zm55,0H147l8.15,23.1h.1L163.45,0H175.2V35.4h-7.8V8.25h-.1L158,35.4h-5.95l-9-27.15H143V35.4h-7.8Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,30 @@
import tmdb from '../assets/tmdb.svg';
function Footer() {
return (
<div className="mt-auto mx-auto">
<footer className="py-6 px-4 mt-16 flex flex-col items-center gap-4">
<a
href="https://www.themoviedb.org"
target="_blank"
className="underline"
>
<img src={tmdb} alt="The Movie Database" className="w-[180px]" />
</a>
<small className="text-sm text-[#a0d7d8] text-center leading-relaxed">
<span>This product uses the </span>
<a
href="https://www.themoviedb.org"
target="_blank"
className="underline"
>
TMDB
</a>
<span> API but is not endorsed or certified by TMDB.</span>
</small>
</footer>
</div>
);
}
export default Footer;

View File

@ -0,0 +1,61 @@
import { useAuthState, useSignOut } from 'react-firebase-hooks/auth';
import { Link, NavLink, useNavigate } from 'react-router-dom';
import { auth } from '../firebase';
function Navbar() {
const [user, userLoading] = useAuthState(auth);
const [signOut] = useSignOut(auth);
const navigate = useNavigate();
const handleSignOut = () => {
navigate('/');
signOut();
};
return (
<header className="px-6 py-4 flex justify-between items-center">
<Link to="/">
<h1 className="text-xl sm:text-2xl">Movie Recommendation</h1>
</Link>
{userLoading && <p>Loading user info</p>}
{!userLoading && !user && (
<p className="text-gray-300 text-lg">Logged out</p>
)}
{!!user && (
<div className="flex gap-8">
<nav>
<ul className="flex flex-col text-center gap-4 md:flex-row">
<li>
<NavLink
to="/recommendations"
className={({ isActive }) => (isActive ? 'font-medium' : '')}
>
Recommendations
</NavLink>
</li>
<li>
<NavLink
to="/rate"
className={({ isActive }) => (isActive ? 'font-medium' : '')}
>
Rate
</NavLink>
</li>
<li>
<NavLink
to="/analytics"
className={({ isActive }) => (isActive ? 'font-medium' : '')}
>
Analytics
</NavLink>
</li>
</ul>
</nav>
<button onClick={handleSignOut}>Log out</button>
</div>
)}
</header>
);
}
export default Navbar;

View File

@ -0,0 +1,98 @@
import { useEffect, useState } from 'react';
import { useAuthState } from 'react-firebase-hooks/auth';
import { useCollectionData } from 'react-firebase-hooks/firestore';
import {
addDoc,
collection,
deleteDoc,
query,
setDoc,
where,
} from 'firebase/firestore';
import { Rating, RatingChange } from '@smastrom/react-rating';
import { auth, db } from '../firebase';
import TMDBMovie from '../types/TMDBMovie';
import posterPlaceholder from '../assets/poster_placeholder.svg';
interface RateMovieProps {
movie: TMDBMovie;
}
const ratingsRef = collection(db, 'ratings');
function RateMovie({ movie }: RateMovieProps) {
const [user] = useAuthState(auth);
if (!user) throw new Error('No user');
const [rating, setRating] = useState<number | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const q = query(
ratingsRef,
where('userId', '==', user.uid),
where('movieId', '==', movie.id),
);
const [values, isLoading, , snapshot] = useCollectionData<{
userId?: string;
movieId?: number;
rating?: number;
}>(q);
const dbRating = values?.[0]?.rating;
const docRef = snapshot?.docs[0]?.ref;
useEffect(() => {
setRating(dbRating || null);
}, [dbRating]);
const handleChange: RatingChange = async (value: number) => {
setIsProcessing(true);
const data = {
userId: user.uid,
movieId: movie.id,
rating: value,
};
try {
if (docRef) {
if (value !== 0) {
await setDoc(docRef, data);
} else {
await deleteDoc(docRef);
}
} else {
await addDoc(ratingsRef, data);
}
} catch (error) {
console.log(error);
} finally {
setIsProcessing(false);
}
};
const posterUrl = `https://image.tmdb.org/t/p/w154${movie.poster_path}`;
return (
<article className="flex flex-col items-center w-[250px]">
<img
src={movie.poster_path ? posterUrl : posterPlaceholder}
alt={movie.title}
className="w-[154px] h-[231px] object-contain mb-2"
/>
<h3 className="my-auto text-lg text-center text-gray-200 font-medium">
{movie.title}
</h3>
<Rating
value={rating || 0}
onChange={handleChange}
isDisabled={isLoading || isProcessing}
className="max-w-[150px] mt-2"
/>
</article>
);
}
export default RateMovie;

View File

@ -0,0 +1,63 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar';
import IRecommendation from '../types/Recommendation';
import TMDBMovie from '../types/TMDBMovie';
import posterPlaceholder from '../assets/poster_placeholder.svg';
interface RecommendationProps {
recommendation: IRecommendation;
}
function Recommendation({ recommendation }: RecommendationProps) {
const [movie, setMovie] = useState<TMDBMovie | null>(null);
useEffect(() => {
(async () => {
const url = `https://api.themoviedb.org/3/movie/${recommendation.movieId}`;
const res = await axios.get(url, {
headers: {
Authorization: `Bearer ${import.meta.env.VITE_TMDB_BEARER_TOKEN}`,
},
});
const movieDetails = res.data as TMDBMovie;
setMovie(movieDetails);
})();
}, [recommendation.movieId]);
if (!movie) return null;
const posterUrl = `https://image.tmdb.org/t/p/w154${movie.poster_path}`;
return (
<article className="flex flex-col items-center w-[250px]">
<img
src={movie.poster_path ? posterUrl : posterPlaceholder}
alt={movie.title}
className="w-[154px] h-[231px] object-contain mb-2"
/>
<h3 className="my-auto text-lg text-center text-gray-200 font-medium">
{movie.title}
</h3>
<CircularProgressbar
value={recommendation.match}
text={`${recommendation.match}%`}
className="size-[75px] mt-4"
strokeWidth={11}
circleRatio={0.75}
styles={buildStyles({
rotation: 1 / 2 + 1 / 8,
strokeLinecap: 'butt',
trailColor: '#eee',
textSize: '24px',
textColor: '#fff',
})}
/>
</article>
);
}
export default Recommendation;

View File

@ -0,0 +1,27 @@
import { RotatingLines } from 'react-loader-spinner';
interface RefreshProps {
mutate: () => void;
isValidating: boolean;
}
function Refresh({ mutate, isValidating }: RefreshProps) {
return (
<div className="flex relative">
<button
onClick={() => mutate()}
disabled={isValidating}
className={`mt-10 mb-10 flex items-center gap-2 border rounded-full py-2 px-4 border-gray-400 hover:bg-gray-700 duration-100 ${isValidating ? 'cursor-wait' : ''}`}
>
{isValidating ? 'Refreshing...' : 'Refresh'}
</button>
{isValidating && (
<div className="absolute -right-2 translate-x-full top-1/2 -translate-y-1/2">
<RotatingLines width="24" strokeColor="#fff" />
</div>
)}
</div>
);
}
export default Refresh;

View File

@ -0,0 +1,63 @@
import {
collection,
getCountFromServer,
query,
where,
} from 'firebase/firestore';
import { useEffect, useState } from 'react';
import { useAuthState } from 'react-firebase-hooks/auth';
import { Link } from 'react-router-dom';
import { auth, db } from '../firebase';
const ratingsRef = collection(db, 'ratings');
function Welcome() {
const [user] = useAuthState(auth);
const [ratingsCount, setRatingsCount] = useState<number | null>(null);
if (!user) throw new Error('No user');
useEffect(() => {
(async () => {
const ratingsQuery = query(ratingsRef, where('userId', '==', user.uid));
const snapshot = await getCountFromServer(ratingsQuery);
setRatingsCount(snapshot.data().count);
})();
}, [user.uid]);
return (
<main>
<p className="text-lg mb-12">Hello {user.displayName}!</p>
{ratingsCount === null && <p>Loading your data</p>}
{Number.isInteger(ratingsCount) &&
(ratingsCount === 0 ? (
<p className="leading-loose">
<br />
You don't have any ratings yet.
<br />
<span>Please </span>
<Link to="/rate" className="underline">
rate
</Link>
<span> your first movie.</span>
</p>
) : (
<p className="leading-loose">
<span>You have </span>
<span className="text-yellow-400 font-bold">{ratingsCount}</span>
<span> movie rating{ratingsCount! > 1 ? 's' : ''}!</span>
<br />
<span>Go to </span>
<Link to="/recommendations" className="underline">
recommendations
</Link>
<span> to get inspired.</span>
</p>
))}
</main>
);
}
export default Welcome;

View File

@ -0,0 +1,18 @@
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: 'AIzaSyAF34ovW0qaDlV68sbpCjylz4TQ0he_XEM',
authDomain: 'movie-recommendation-2024.firebaseapp.com',
projectId: 'movie-recommendation-2024',
storageBucket: 'movie-recommendation-2024.appspot.com',
messagingSenderId: '777010585410',
appId: '1:777010585410:web:7db1436b72879d512032db',
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
export { auth, db };

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import '@smastrom/react-rating/style.css';
import 'react-circular-progressbar/dist/styles.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@ -0,0 +1,73 @@
import { ThreeDots } from 'react-loader-spinner';
import {
Area,
AreaChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import useSWR, { Fetcher } from 'swr';
import Refresh from '../components/Refresh';
interface VisitorsData {
date: string;
visitors: number;
}
const fetcher: Fetcher<VisitorsData[], string> = async () => {
const res = {
data: {
visitors: [
{ date: '2024-05-10', visitors: Math.floor(Math.random() * 10) },
{ date: '2024-05-11', visitors: Math.floor(Math.random() * 10) },
{ date: '2024-05-12', visitors: Math.floor(Math.random() * 10) },
{ date: '2024-05-13', visitors: Math.floor(Math.random() * 10) },
{ date: '2024-05-14', visitors: Math.floor(Math.random() * 10) },
{ date: '2024-05-15', visitors: Math.floor(Math.random() * 10) },
{ date: '2024-05-16', visitors: Math.floor(Math.random() * 10) },
],
},
};
await new Promise((res) => setTimeout(res, 1500));
return res.data.visitors as VisitorsData[];
};
function Analytics() {
const { data, isLoading, isValidating, mutate } = useSWR('visitors', fetcher);
return (
<main className="flex flex-col items-center px-6 flex-1">
<h2 className="text-2xl font-medium">Analytics</h2>
{isLoading && (
<div className="my-auto flex flex-col items-center gap-2">
<ThreeDots />
<p className="italic">Loading analytics...</p>
</div>
)}
{!isLoading && <Refresh mutate={mutate} isValidating={isValidating} />}
{!isLoading && !data?.length && <p>Try again later 😬</p>}
{!isLoading && !!data?.length && (
<div className="w-full max-w-6xl text-[#2a5e87] my-auto">
<ResponsiveContainer width="100%" height={400}>
<AreaChart
data={data}
margin={{ top: 5, right: 30, bottom: 5, left: 0 }}
>
<Legend verticalAlign="top" height={30} />
<XAxis dataKey="date" />
<YAxis />
<Area type="monotone" dataKey="visitors" />
<Tooltip />
</AreaChart>
</ResponsiveContainer>
</div>
)}
</main>
);
}
export default Analytics;

View File

@ -0,0 +1,33 @@
import { useAuthState, useSignInWithGoogle } from 'react-firebase-hooks/auth';
import Welcome from '../components/Welcome';
import { auth } from '../firebase';
import googleLogo from '../assets/google.svg';
function Home() {
const [user] = useAuthState(auth);
const [signInWithGoogle] = useSignInWithGoogle(auth);
return (
<main className="flex flex-col items-center mt-16 mx-4 text-center">
<h2 className="text-2xl font-medium mb-8">
Find the perfect movie to watch tonight.
</h2>
{!user && (
<>
<p className="mt-4">But first you need to log in:</p>
<button
type="button"
onClick={() => signInWithGoogle()}
className="mt-10 flex items-center gap-2 border rounded-full py-2 px-4 border-gray-400 hover:bg-gray-700 duration-100"
>
<img src={googleLogo} alt="Google" className="size-8" />
<span>Log in with Google</span>
</button>
</>
)}
{!!user && <Welcome />}
</main>
);
}
export default Home;

View File

@ -0,0 +1,52 @@
import axios from 'axios';
import { useState } from 'react';
import { ThreeDots } from 'react-loader-spinner';
import useSWR, { Fetcher } from 'swr';
import RateMovie from '../components/RateMovie';
import TMDBMovie from '../types/TMDBMovie';
const fetcher: Fetcher<TMDBMovie[], string> = async (url) => {
const res = await axios.get(url, {
headers: {
Authorization: `Bearer ${import.meta.env.VITE_TMDB_BEARER_TOKEN}`,
},
});
return res.data.results as TMDBMovie[];
};
function Rate() {
const [value, setValue] = useState('');
const tmdbQuery = value
? `https://api.themoviedb.org/3/search/movie?query=${value}`
: `https://api.themoviedb.org/3/trending/movie/day`;
const { data, isLoading } = useSWR(tmdbQuery, fetcher);
return (
<main className="flex flex-col items-center px-6">
<h2 className="text-2xl font-medium">Rate a movie</h2>
<div className="max-w-full w-[600px]">
<input
type="text"
placeholder="Find a movie..."
value={value}
onChange={(e) => setValue(e.target.value)}
className="mt-12 bg-gray-700 rounded-full py-2 px-4 mb-12 w-full"
/>
</div>
{isLoading && <ThreeDots />}
{!isLoading && !data?.length && <p>Could not find a movie 😬</p>}
{!isLoading && !!data?.length && (
<section className="flex flex-wrap gap-x-6 gap-y-9 justify-center">
{data.slice(0, 12).map((movie) => (
<RateMovie movie={movie} key={movie.id} />
))}
</section>
)}
</main>
);
}
export default Rate;

View File

@ -0,0 +1,62 @@
import { useAuthState } from 'react-firebase-hooks/auth';
import { ThreeDots } from 'react-loader-spinner';
import useSWR, { Fetcher } from 'swr';
import Recommendation from '../components/Recommendation';
import Refresh from '../components/Refresh';
import { auth } from '../firebase';
import IRecommendation from '../types/Recommendation';
const fetcher: Fetcher<IRecommendation[], string> = async (userId) => {
const res = {
data: {
userId,
recommendations: [
{ movieId: 174, match: 78 },
{ movieId: 70, match: 74 },
{ movieId: 421, match: 70 },
{ movieId: 5846, match: 54 },
{ movieId: 926, match: 48 },
],
},
};
await new Promise((res) => setTimeout(res, 2500));
return res.data.recommendations as IRecommendation[];
};
function Recommendations() {
const [user] = useAuthState(auth);
if (!user) throw new Error('User not found');
const { data, isLoading, isValidating, mutate } = useSWR(user.uid, fetcher);
return (
<main className="flex flex-col items-center px-6 flex-1">
<h2 className="text-2xl font-medium">Recommendations</h2>
{isLoading && (
<div className="my-auto flex flex-col items-center gap-2">
<ThreeDots />
<p className="italic">Loading recommendations...</p>
</div>
)}
{!isLoading && <Refresh mutate={mutate} isValidating={isValidating} />}
{!isLoading && !data?.length && (
<p>Could not find any recommendations for you 😬</p>
)}
{!isLoading && !!data?.length && (
<section className="flex flex-wrap gap-x-6 gap-y-9 justify-center">
{data.map((recommendation) => (
<Recommendation
recommendation={recommendation}
key={recommendation.movieId}
/>
))}
</section>
)}
</main>
);
}
export default Recommendations;

View File

@ -0,0 +1,6 @@
interface Recommendation {
movieId: number;
match: number;
}
export default Recommendation;

View File

@ -0,0 +1,18 @@
interface TMDBMovie {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
original_language: string;
original_title: string;
overview: string;
popularity: number;
poster_path: string | null;
release_date: string;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export default TMDBMovie;

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,9 @@
import type { Config } from 'tailwindcss';
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
} satisfies Config;

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import mkcert from 'vite-plugin-mkcert'
export default defineConfig({
server: { https: true }, // Not needed for Vite 5+
plugins: [ react(), mkcert() ]
})

View File

@ -0,0 +1,29 @@
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set the working directory to /app2
WORKDIR /app
# Install necessary OS packages
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy the Python script, requirements, and datasets into the container at /app2
COPY movie_recommender.py .
COPY datasets/tmdb_5000_credits.csv ./movie_recommendations/datasets/
COPY datasets/tmdb_5000_movies.csv ./movie_recommendations/datasets/
COPY init_scripts/constants.ini ./init_scripts/constants.ini
COPY init_scripts/movies.csv /app/init_scripts/
# Modify requirements.txt to use psycopg2-binary
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
# Install any needed packages specified in requirements.txt
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install requests
# Run frontend_AI_connector.py when the container launches
CMD ["python", "./movie_recommender.py"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
[postgres]
host=db
database=test_db
user=root
password=root
port=5432
[movie]
csv_path=../../movie_recommendations/datasets/tmdb_5000_credits.csv

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,170 @@
import pandas as pd
import numpy as np
from ast import literal_eval
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import hashlib
import json
from configparser import ConfigParser
import psycopg2
from flask import Flask, request, jsonify
from flask_caching import Cache
app = Flask(__name__)
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
db_connector = None
conn = None
def get_director(x):
for i in x:
if i['job'] == 'Director':
return i['name']
return np.nan
def get_list(x):
if isinstance(x, list):
names = [i['name'] for i in x]
if len(names) > 3:
names = names[:3]
return names
return []
def clean_data(x):
if isinstance(x, list):
return [str.lower(i.replace(" ", "")) for i in x]
else:
if isinstance(x, str):
return str.lower(x.replace(" ", ""))
else:
return ''
def create_soup(x):
return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director'] + ' ' + ' '.join(x['genres'])
class MovieRecommender:
def __init__(self):
self.df = None
self.cosine_sim = None
def fit(self, credits_file, movies_file):
"""
Fittuje AI do przekazanych danych
:param credits_file: csv z creditsami
:param movies_file: csv z filmami
:return: Nic
"""
df1 = pd.read_csv(credits_file)
df2 = pd.read_csv(movies_file)
df1.columns = ['id', 'tittle', 'cast', 'crew']
df2 = df2.merge(df1, on='id')
df2['overview'] = df2['overview'].fillna('')
self.df = df2
features = ['cast', 'crew', 'keywords', 'genres']
for feature in features:
df2[feature] = df2[feature].apply(literal_eval)
df2['director'] = df2['crew'].apply(get_director)
features = ['cast', 'keywords', 'genres']
for feature in features:
df2[feature] = df2[feature].apply(get_list)
features = ['cast', 'keywords', 'director', 'genres']
for feature in features:
df2[feature] = df2[feature].apply(clean_data)
df2['soup'] = df2.apply(create_soup, axis=1)
count = CountVectorizer(stop_words='english')
count_matrix = count.fit_transform(df2['soup'])
self.cosine_sim = cosine_similarity(count_matrix, count_matrix)
self.df = df2.reset_index()
def _get_recommendations_one_input(self, movie_id):
"""
Tworzy rekomendacje, bazując na jednym filmie
:param movie_id: id filmu, dla którego ma zrobić rekomendację
:return: Zwraca listę [movie_ids, similarity_scores] gdzie oba argumenty np.array
"""
indices = pd.Series(self.df.index, index=self.df['id']).drop_duplicates()
idx = indices[movie_id]
sim_scores = list(enumerate(self.cosine_sim[idx]))
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
sim_scores = sim_scores[1:101]
movie_indices = [i[0] for i in sim_scores]
sim_scores = np.array([t[1] for t in sim_scores])
return [self.df['id'].iloc[movie_indices].values, sim_scores]
def get_recommendations(self, movie_ids: list) -> {}:
"""
Tworzy listę rekomendacji bazującą na id podanych filmów
:param movie_ids: id filmów, na podstawie których ma wybrać rekomendowane filmy
:return: Zwraca dicta {movie_id: similarity_scores}
"""
recommended_movies = {}
for movie_id in movie_ids:
recommended_ids, sim_scores = self._get_recommendations_one_input(movie_id)
for recommended_id, sim_score in zip(recommended_ids, sim_scores):
if recommended_id in movie_ids:
continue
if recommended_movies.get(int(recommended_id)) is None:
recommended_movies[int(recommended_id)] = float(round((sim_score / len(movie_ids)), 4))
else:
recommended_movies[int(recommended_id)] += float(round((sim_score / len(movie_ids)), 4))
return recommended_movies
recommender = MovieRecommender()
recommender.fit('datasets/tmdb_5000_credits.csv',
'datasets/tmdb_5000_movies.csv')
def make_cache_key():
data = request.get_json()
if isinstance(data, list):
data = sorted(data)
key = hashlib.md5(json.dumps(data).encode('utf-8')).hexdigest()
return key
@app.route("/api/v3/AI_recommendations", methods=["POST"])
@cache.cached(timeout=300, key_prefix=make_cache_key)
def AI_recommendations():
ids = request.get_json()
recommendations = recommender.get_recommendations(ids)
return jsonify(recommendations)
if __name__ == "__main__":
config = ConfigParser()
config.read("init_scripts/constants.ini")
while True:
try:
conn = psycopg2.connect(
host=config["postgres"]["host"],
database=config["postgres"]["database"],
user=config["postgres"]["user"],
password=config["postgres"]["password"],
port=int(config["postgres"]["port"])
)
except Exception:
print("Trying to connect with database")
continue
else:
break
cache.init_app(app)
app.run(host="localhost", port=8081, debug=True)
conn.close()

View File

@ -0,0 +1,4 @@
flask==3.0.3
pandas==2.2.2
Flask-Caching==2.3.0
scikit-learn==1.5.0

View File

@ -0,0 +1,24 @@
server {
listen 80;
server_name localhost;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/ssl/certs/nginx.crt;
ssl_certificate_key /etc/ssl/certs/nginx.key;
location / {
try_files $uri @app_proxy;
}
location @app_proxy {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_pass http://app:5000; # Use the service name 'app' and the internal port '80'
}
}

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUaPIEdr82RMes6XSaYOB55pQbYDswDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDYxNjIxNTYxNloXDTI1MDYx
NjIxNTYxNlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAlgWNGOpLB7FonzAvxoMmcDKu7SJDAnBd8HnrswRb4noI
HCy2b3JOYi5RU66AGfDB9N4YFkhSNYkjSGLQdmoPqFoR5IyFAtvdGQUsAhRl4YP9
/ust4NlPAVfxYPGhHyrpPozJK/RYIFcwPDKgMEEI636jHWw6mJaOsVgiMpQgs9oM
V4b5b9LfB6wD72pi6Ag6xMi5PUvj91UfvkR59Gy844zriyV094saxwBRCL6naSMA
vToVFIcaGCIXHfO2DiTVq4dDkCkHYRiva64UcV08x0I2GyxNLCUsjgDrQy/pRfdn
QE1QO55LoDk4SI3U0PSGCyBMg0Kr7eC+6MwhjI/2ewIDAQABo1MwUTAdBgNVHQ4E
FgQUZQxFwZhHP7+N2huK4T8S4+dCMoIwHwYDVR0jBBgwFoAUZQxFwZhHP7+N2huK
4T8S4+dCMoIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALTJ4
1bql/QkcM7t3DSCm7H7NK0fW9+3G/FYtCQUxvs3+rFvxymHbwHERbR1AReJy2Brx
ayNMQNUA48sUa2VsRxEv1f/Qb5WVue3rIonugZ792bsI+6yMVxYVt97ETAnalSH4
L3kWqGWJnkyfMm3RB7sGEFVnO2/s+vEhbzCRpCt2qda1pXPIBagPSCFm3InU9+Tq
2bDsASuUEEz7RWTPa/R1qkCU5Sl3+CZUpTIpRDxB2MCaGk+FXQ9u2CpZBiZDOL/p
e0Af5yhYlNBHp+Ut+N1kQSKKyOqlrqkN92nx7NLqOYBkjfkns9lTciyKiKefq08G
qesECi3RgXXuG6xv5Q==
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWBY0Y6ksHsWif
MC/GgyZwMq7tIkMCcF3weeuzBFvieggcLLZvck5iLlFTroAZ8MH03hgWSFI1iSNI
YtB2ag+oWhHkjIUC290ZBSwCFGXhg/3+6y3g2U8BV/Fg8aEfKuk+jMkr9FggVzA8
MqAwQQjrfqMdbDqYlo6xWCIylCCz2gxXhvlv0t8HrAPvamLoCDrEyLk9S+P3VR++
RHn0bLzjjOuLJXT3ixrHAFEIvqdpIwC9OhUUhxoYIhcd87YOJNWrh0OQKQdhGK9r
rhRxXTzHQjYbLE0sJSyOAOtDL+lF92dATVA7nkugOThIjdTQ9IYLIEyDQqvt4L7o
zCGMj/Z7AgMBAAECggEAEYz2s8p9GppLpgvqGuwu7ANR6ZYPhtKEeuSYiWniIf2q
wzkL4r/ZoazPgN2ySNacqjvtT3YIgBDaGtbMkn3X9RcMbtNtoCb+l7W+L0QZoydg
6Ji01lA16O9T4saB1facMOAhjM3aHXZ1wyUdDmgdVgeLp56IwF8ktGSeI5KmvMOA
tHH20ZaG0Iy9kHjqgnRTKsF1giUl4XtNR0tbzVBdzvXZz8BPzAtNkC0QwcA3r6Mr
PRzpelCQ9cFdbbpssy73aZ4cu30nVYdIuHFCmB25QdOt6Uzo4yoxmUK3PSKjf0py
Ow5fHN5UqB1vZ0C/zNAapeg9elZvsYMU+LSUkGknaQKBgQDNfBb/FasjhPLstiHR
U8LXTLinsU5qtKDVQIEpxQOub3lwRFs8bK0T2PfdbNJqUv55B301ORpbyqXLWS9N
BNEUWkQ0Tpw9CsnItENvQnObUh6iIPU0uKLSp8Vnj4v3zOLfAMBZMitqbituqooE
rrXmv9Oy3Sa/G1gX6HC7Hrdv/wKBgQC65vfMjmYXXRL47puTkfkBVWIZPV5WNq2F
VnEarBUcO4ZqwweKKzO38mLXFWVcxz9QGzznoR/ndi1KGJSmBPbYPJwIKEcUCgZv
25QUsVstV7mB2O53YEYfr4eGx0+lacofSYOcNV//Mrf1VcEQn8stme+hRp3xsvRa
FAni8K05hQKBgQCWc8IeuU3aLvDzMpPmQa8Klwko92CULncITTeFTtRYNxSyh8pJ
nsTHIHizrocOBICAO2SIwKu1A9aK4l0IxnsTrNf9eIVbCHggNSSe5QfidKkrSnhf
RsUo+mBGaEsyf9ipMVKkvGAfiFTSrZlqvkU/k7q8XsKM8Md6kd1glKf5HwKBgGp0
Q4/vS5bjHHtM7LAQ5JMt3sIhgin42rynj6Rxf1SlUtpcW18HXF3ZYRLAzQsbMaSe
3wHPdCyR0xnxBjnJeg+P9g9vYYy4aRItLxraKeSZor+in7C+1TIW+Ep8G5FLwCQx
6xR+Ej940+6Y+W5OlZtTonlpj2yrUSI9Z6QrEX9BAoGADH9nrJ+Gl6GaTEtlTHUQ
XbQLmlfffQPIvc+v6z6TAvuULqQrK/Tya8s5dqDmM+RfoU12vij4yCblChDo8Pef
IgInN2S7hmuMHVB5QdVHoiHzVnryIsr29fR9jDp3QP7AnnFJbZ5sOQxId+0KpYtG
ZTcpATmbdu3hUJSNSpK1+ig=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,32 @@
###
POST http://127.0.0.1:8081/api/v3/AI_recommendations
Content-Type: application/json
[
49026,
155,
312113
]
###
POST http://127.0.0.1:8090/api/v3/add/1234/blep
###
GET http://127.0.0.1:8090/api/v3/get/blep
###
POST http://127.0.0.1:8090/api/v3/add/6666/blop
###
POST http://127.0.0.1:8090/api/v3/rate_movie/1234/155/4
###
POST http://127.0.0.1:8090/api/v3/rate_movie/6666/155/5
###
POST http://127.0.0.1:8090/api/v3/rate_movie/1234/19995/5
###
GET http://localhost:8082/api/get_movie_ratings/155
###
GET http://localhost:8082/api/get_number_of_ratings
###
GET http://localhost:8082/api/get_users_number
###
GET http://localhost:8082/api/get_movie_rating_avg/155
###
GET http://localhost:8082/api/get_user_ratings/1234
###