Merge remote-tracking branch 'origin/dev_msar'

This commit is contained in:
Krzysztof Rudnicki 2024-06-16 20:54:39 +02:00
commit 82d8406271
23 changed files with 1079 additions and 237 deletions

1
backend/.env Normal file
View File

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

4
backend/.gitignore vendored Normal file
View File

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

199
backend/app.py Normal file
View File

@ -0,0 +1,199 @@
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

View File

@ -8,12 +8,17 @@
"name": "movie-recommendation-client", "name": "movie-recommendation-client",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@smastrom/react-rating": "^1.5.0",
"axios": "^1.6.8",
"firebase": "^10.11.1", "firebase": "^10.11.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-firebase-hooks": "^5.1.1", "react-firebase-hooks": "^5.1.1",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.23.1" "react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
@ -30,8 +35,7 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.2.0", "vite": "^5.2.0"
"vite-plugin-mkcert": "^1.17.5"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -46,6 +50,17 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/is-prop-valid": { "node_modules/@emotion/is-prop-valid": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
@ -1214,161 +1229,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
"dev": true,
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz",
"integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==",
"dev": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.3.1",
"@octokit/request-error": "^5.1.0",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/endpoint": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz",
"integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==",
"dev": true,
"dependencies": {
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz",
"integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==",
"dev": true,
"dependencies": {
"@octokit/request": "^8.3.0",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
"dev": true
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "11.3.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz",
"integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==",
"dev": true,
"dependencies": {
"@octokit/types": "^13.5.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz",
"integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==",
"dev": true,
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "13.2.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz",
"integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==",
"dev": true,
"dependencies": {
"@octokit/types": "^13.5.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "^5"
}
},
"node_modules/@octokit/request": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz",
"integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==",
"dev": true,
"dependencies": {
"@octokit/endpoint": "^9.0.1",
"@octokit/request-error": "^5.1.0",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/request-error": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz",
"integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==",
"dev": true,
"dependencies": {
"@octokit/types": "^13.1.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/rest": {
"version": "20.1.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.1.tgz",
"integrity": "sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==",
"dev": true,
"dependencies": {
"@octokit/core": "^5.0.2",
"@octokit/plugin-paginate-rest": "11.3.1",
"@octokit/plugin-request-log": "^4.0.0",
"@octokit/plugin-rest-endpoint-methods": "13.2.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/types": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz",
"integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==",
"dev": true,
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -1649,6 +1509,15 @@
"win32" "win32"
] ]
}, },
"node_modules/@smastrom/react-rating": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@smastrom/react-rating/-/react-rating-1.5.0.tgz",
"integrity": "sha512-7vg3NRgO0tpvEunq8BEWA8qckNSd7x3dVGqaNEfLs3Ow4ibU2EEXJtnd7Yl44xOujWIzXM5Bk2VZm2DVm065Qw==",
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/@swc/core": { "node_modules/@swc/core": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.5.tgz",
@ -1862,6 +1731,60 @@
"@swc/counter": "^0.1.3" "@swc/counter": "^0.1.3"
} }
}, },
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
},
"node_modules/@types/d3-scale": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
"integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
"integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
"integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -2228,8 +2151,7 @@
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"dev": true
}, },
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.19", "version": "10.4.19",
@ -2269,10 +2191,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.2", "version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"dev": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -2285,12 +2206,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"dev": true
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -2454,6 +2369,11 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -2501,6 +2421,14 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2521,7 +2449,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@ -2593,6 +2520,116 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -2610,6 +2647,11 @@
} }
} }
}, },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -2620,17 +2662,10 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -2667,6 +2702,15 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -2940,12 +2984,25 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true "dev": true
}, },
"node_modules/fast-equals": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@ -3124,7 +3181,6 @@
"version": "1.15.6", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -3160,7 +3216,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@ -3396,6 +3451,14 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true "dev": true
}, },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -3587,6 +3650,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.camelcase": { "node_modules/lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -3649,7 +3717,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -3658,7 +3725,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
}, },
@ -3758,7 +3824,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -4104,6 +4169,21 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/protobufjs": { "node_modules/protobufjs": {
"version": "7.3.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz",
@ -4130,8 +4210,7 @@
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
"dev": true
}, },
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
@ -4173,6 +4252,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-circular-progressbar": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz",
"integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==",
"peerDependencies": {
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -4245,6 +4332,35 @@
"react-dom": ">=16.8" "react-dom": ">=16.8"
} }
}, },
"node_modules/react-smooth": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz",
"integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -4266,6 +4382,46 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/recharts": {
"version": "2.12.7",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz",
"integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^16.10.2",
"react-smooth": "^4.0.0",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -4678,6 +4834,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/swr": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
"dependencies": {
"client-only": "^0.0.1",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
@ -4742,6 +4910,11 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -4830,12 +5003,6 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
}, },
"node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"dev": true
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.15", "version": "1.0.15",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz",
@ -4875,12 +5042,41 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.11", "version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
@ -4936,24 +5132,6 @@
} }
} }
}, },
"node_modules/vite-plugin-mkcert": {
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.5.tgz",
"integrity": "sha512-KKGY3iHx/9zb7ow8JJ+nLN2HiNIBuPBwj34fJ+jAJT89/8qfk7msO7G7qipR8VDEm9xMCys0xT11QOJbZcg3/Q==",
"dev": true,
"dependencies": {
"@octokit/rest": "^20.0.2",
"axios": "^1.6.8",
"debug": "^4.3.4",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=v16.7.0"
},
"peerDependencies": {
"vite": ">=3"
}
},
"node_modules/websocket-driver": { "node_modules/websocket-driver": {
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",

View File

@ -10,12 +10,17 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@smastrom/react-rating": "^1.5.0",
"axios": "^1.6.8",
"firebase": "^10.11.1", "firebase": "^10.11.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-firebase-hooks": "^5.1.1", "react-firebase-hooks": "^5.1.1",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.23.1" "react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
@ -32,7 +37,6 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.2.0", "vite": "^5.2.0"
"vite-plugin-mkcert": "^1.17.5"
} }
} }

View File

@ -1,22 +1,15 @@
import { Outlet, RouterProvider, createBrowserRouter } from 'react-router-dom'; import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import Navbar from './components/Navbar'; import Layout from './Layout';
import Home from './pages/Home'; import Home from './pages/Home';
import Recommendations from './pages/Recommendations'; import Recommendations from './pages/Recommendations';
import Rate from './pages/Rate'; import Rate from './pages/Rate';
import Analytics from './pages/Analytics'; import Analytics from './pages/Analytics';
function App() { function App() {
const Layout = (
<>
<Navbar />
<Outlet />
</>
);
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: '/', path: '/',
element: Layout, element: <Layout />,
children: [ children: [
{ path: '/', element: <Home /> }, { path: '/', element: <Home /> },
{ path: '/recommendations', element: <Recommendations /> }, { path: '/recommendations', element: <Recommendations /> },

32
frontend/src/Layout.tsx Normal file
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" 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

@ -1,10 +1,16 @@
import { useAuthState, useSignOut } from 'react-firebase-hooks/auth'; import { useAuthState, useSignOut } from 'react-firebase-hooks/auth';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink, useNavigate } from 'react-router-dom';
import { auth } from '../firebase'; import { auth } from '../firebase';
function Navbar() { function Navbar() {
const [user, userLoading] = useAuthState(auth); const [user, userLoading] = useAuthState(auth);
const [signOut] = useSignOut(auth); const [signOut] = useSignOut(auth);
const navigate = useNavigate();
const handleSignOut = () => {
navigate('/');
signOut();
};
return ( return (
<header className="px-6 py-4 flex justify-between items-center"> <header className="px-6 py-4 flex justify-between items-center">
@ -45,7 +51,7 @@ function Navbar() {
</li> </li>
</ul> </ul>
</nav> </nav>
<button onClick={() => signOut()}>Log out</button> <button onClick={handleSignOut}>Log out</button>
</div> </div>
)} )}
</header> </header>

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

@ -28,19 +28,27 @@ function Welcome() {
return ( return (
<main> <main>
<p className="text-lg mb-8">Hello {user.displayName}!</p> <p className="text-lg mb-12">Hello {user.displayName}!</p>
{ratingsCount === null && <p>Loading your data</p>} {ratingsCount === null && <p>Loading your data</p>}
{Number.isInteger(ratingsCount) && {Number.isInteger(ratingsCount) &&
(ratingsCount === 0 ? ( (ratingsCount === 0 ? (
<p> <p className="leading-loose">
<span>You don't have any ratings yet. Please </span>
<br />
You don't have any ratings yet.
<br />
<span>Please </span>
<Link to="/rate" className="underline"> <Link to="/rate" className="underline">
rate rate
</Link> </Link>
<span> your first movie.</span> <span> your first movie.</span>
</p> </p>
) : ( ) : (
<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> <span>Go to </span>
<Link to="/recommendations" className="underline"> <Link to="/recommendations" className="underline">
recommendations recommendations

View File

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

View File

@ -1,5 +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() { function Analytics() {
return <div>Analytics</div>; 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; export default Analytics;

View File

@ -1,11 +1,10 @@
import { useAuthState, useSignInWithGoogle } from 'react-firebase-hooks/auth'; import { useAuthState, useSignInWithGoogle } from 'react-firebase-hooks/auth';
import { Triangle } from 'react-loader-spinner';
import Welcome from '../components/Welcome'; import Welcome from '../components/Welcome';
import { auth } from '../firebase'; import { auth } from '../firebase';
import googleLogo from '../assets/google.svg'; import googleLogo from '../assets/google.svg';
function Home() { function Home() {
const [user, userLoading] = useAuthState(auth); const [user] = useAuthState(auth);
const [signInWithGoogle] = useSignInWithGoogle(auth); const [signInWithGoogle] = useSignInWithGoogle(auth);
return ( return (
@ -13,8 +12,7 @@ function Home() {
<h2 className="text-2xl font-medium mb-8"> <h2 className="text-2xl font-medium mb-8">
Find the perfect movie to watch tonight. Find the perfect movie to watch tonight.
</h2> </h2>
{userLoading && <Triangle />} {!user && (
{!userLoading && !user && (
<> <>
<p className="mt-4">But first you need to log in:</p> <p className="mt-4">But first you need to log in:</p>
<button <button

View File

@ -1,5 +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() { function Rate() {
return <div>Rate</div>; 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; export default Rate;

View File

@ -1,5 +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() { function Recommendations() {
return <div>Recommendations</div>; 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; 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

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