diff --git a/Frontend/Pages/Home/AboutUs.razor b/Frontend/Pages/Home/AboutUs.razor index 9c61a795f2d21abd2287fee1c8b12eb8998442e1..d3c2578525db949816296714c51327b2266ab142 100644 --- a/Frontend/Pages/Home/AboutUs.razor +++ b/Frontend/Pages/Home/AboutUs.razor @@ -2,18 +2,30 @@ <MudContainer MaxWidth="MaxWidth.Large"> <MudPaper Class="pa-4"> - <MudText Typo="Typo.h5">About Us</MudText> + <MudText Typo="Typo.h5" Class="d-flex justify-content-center">About Us</MudText> <MudDivider DividerType="DividerType.FullWidth" Class="my-2"/> <MudText Typo="Typo.body1"> - WatchWhiz is a user-friendly TV show recommendation web application focused on giving the best results back - to the user based on the series they look for. With its glamorous algorithm which uses sets of data to filter - the appropriate response, it is guaranteed to return a group of TV programs that users will enjoy. If one is - looking to review and rate a series they can do so, showing the rest of the people using the website what is - considered highly praised by the public, as well as indicating the official ratings of TMDB. Each person gets - their own customizable profile, in which they can change various information about themselves as well as - uploading their own profile picture and banner, with a list of their favourite movies on top of that. With - all these functionalities in mind, our goal is to give people a simple way to discover their new yet unknown - favourite shows. + WatchWhiz is a user-centric TV show recommendation platform designed for those in search of their next + favourite show. Utilising a sophisticated algorithm based on cosine similarity, we meticulously filter and + curate an extensive selection of TV shows that are similar to each other, ensuring a highly accurate and + relevant set of recommendations. + </MudText> + <MudText Typo="Typo.body1"> + You can leave reviews and ratings on any TV show, letting our users share their thoughts on various series, + which we hope will foster a vibrant community of like-minded individuals. We also show two different ratings + across every show, one from TMDB (The Movie Database), and on our end - this provides a well-rounded perspective + on the quality and popularity of each show. + </MudText> + <MudText Typo="Typo.body1"> + We understand that personalisation is key to an engaging user experience, so everyone can create and + customise their own profile, showcasing four of their favourite shows. With the ability to modify personal + information and upload custom profile pictures and banners, users can make their profiles a reflection of + their inner TV series critic. + </MudText> + <MudText Typo="Typo.body1"> + Our platform encourages social interaction, users can explore and appreciate other members’ profiles and + reviews. This feature allows others to discover new shows based on the recommendations of fellow enthusiasts. + We aim to become the ultimate destination for those in search of their next binge-worthy series. </MudText> </MudPaper> </MudContainer> \ No newline at end of file diff --git a/Frontend/Pages/Home/GitLab.razor b/Frontend/Pages/Home/GitLab.razor new file mode 100644 index 0000000000000000000000000000000000000000..3f862bf5c669f104c3c10e9161ed065385699f41 --- /dev/null +++ b/Frontend/Pages/Home/GitLab.razor @@ -0,0 +1,42 @@ +@page "/GitLab" + +<MudContainer MaxWidth="MaxWidth.Large"> + <MudPaper Class="pa-4"> + <MudText Typo="Typo.h5" Class="d-flex justify-content-center">GitLab links</MudText> + <MudDivider DividerType="DividerType.FullWidth" Class="my-2"/> + <MudText Typo="Typo.h6" Class="d-flex justify-content-center">Portal</MudText> + <MudLink Typo="Typo.h6" Class="d-flex justify-content-center" Underline="Underline.None" + Href="https://gitlab.surrey.ac.uk/aa03980/Group17Frontend" Target="_blank"> + https://gitlab.surrey.ac.uk/aa03980/Group17Frontend + </MudLink> + <MudText Typo="Typo.body1" Class="d-flex justify-content-center">The frontend of the website, written in Blazor WASM - .NET 7</MudText> + <MudDivider DividerType="DividerType.FullWidth" Class="my-2"/> + <MudText Typo="Typo.h6" Class="d-flex justify-content-center">Authentication Microservice</MudText> + <MudLink Typo="Typo.h6" Class="d-flex justify-content-center" Underline="Underline.None" + Href="https://gitlab.surrey.ac.uk/aa03980/Group17AuthenticationMicroservice" Target="_blank"> + https://gitlab.surrey.ac.uk/aa03980/Group17AuthenticationMicroservice + </MudLink> + <MudText Typo="Typo.body1" Class="d-flex justify-content-center">The Authentication microservice of the project, written in .NET 8</MudText> + <MudDivider DividerType="DividerType.FullWidth" Class="my-2"/> + <MudText Typo="Typo.h6" Class="d-flex justify-content-center">Profile Microservice</MudText> + <MudLink Typo="Typo.h6" Class="d-flex justify-content-center" Underline="Underline.None" + Href="https://gitlab.surrey.ac.uk/aa03980/Group17ProfileMicroservice" Target="_blank"> + https://gitlab.surrey.ac.uk/aa03980/Group17ProfileMicroservice + </MudLink> + <MudText Typo="Typo.body1" Class="d-flex justify-content-center">The Profile microservice of the project, written in .NET 8</MudText> + <MudDivider DividerType="DividerType.FullWidth" Class="my-2"/> + <MudText Typo="Typo.h6" Class="d-flex justify-content-center">TV Series Recommendation Microservice</MudText> + <MudLink Typo="Typo.h6" Class="d-flex justify-content-center" Underline="Underline.None" + Href="https://gitlab.surrey.ac.uk/aa03980/Group17TVSeriesRecommendationMicroservice" Target="_blank"> + https://gitlab.surrey.ac.uk/aa03980/Group17TVSeriesRecommendationMicroservice + </MudLink> + <MudText Typo="Typo.body1" Class="d-flex justify-content-center">The TV Series Recommendation microservice for the project, written in Flask</MudText> + <MudDivider DividerType="DividerType.FullWidth" Class="my-2"/> + <MudText Typo="Typo.h6" Class="d-flex justify-content-center">Reviews and Ratings Microservice</MudText> + <MudLink Typo="Typo.h6" Class="d-flex justify-content-center" Underline="Underline.None" + Href="https://gitlab.surrey.ac.uk/aa03980/Group17ReviewsandRatingsMicroservice" Target="_blank"> + https://gitlab.surrey.ac.uk/aa03980/Group17ReviewsandRatingsMicroservice + </MudLink> + <MudText Typo="Typo.body1" Class="d-flex justify-content-center">The Reviews and Ratings microservice for the project, written in Flask</MudText> + </MudPaper> +</MudContainer> \ No newline at end of file diff --git a/Frontend/Pages/Home/HomeMain.razor b/Frontend/Pages/Home/HomeMain.razor index 75b94da3b4133ef58e2e8e6745d48c9fb4aabb91..917bc1e65bab01afc76c5a2d31943b81a61994ae 100644 --- a/Frontend/Pages/Home/HomeMain.razor +++ b/Frontend/Pages/Home/HomeMain.razor @@ -59,7 +59,7 @@ _searching = true; try { - NavigationManager.NavigateTo($"/Series?{id}", true); + NavigationManager.NavigateTo($"/Recommendations?{id}", true); StateHasChanged(); } catch (APIException ex) diff --git a/Frontend/Pages/Recommendations/RecommendationsMain.razor b/Frontend/Pages/Recommendations/RecommendationsMain.razor index 4e886e51b733e2edab237960a08d0c8e0e9ba5e9..18fd0ccef90372456dc1df4e9237b6bab4c8ce9b 100644 --- a/Frontend/Pages/Recommendations/RecommendationsMain.razor +++ b/Frontend/Pages/Recommendations/RecommendationsMain.razor @@ -1,15 +1,14 @@ @page "/Recommendations" @inject NavigationManager NavigationManager -@inherits RecommendationsComponentBase -@inject ProfileStateService ProfileStateService -@inject IProfileClient ProfileClient +@inject ITV_SeriesClient SeriesClient @using Group17PortalWasm.Services -@using Group17.Profile @using Group17PortalWasm.Helpers @using System.Globalization -@implements IDisposable +@using Group17.TVSeries +@using APIException = Group17.Profile.APIException +@inherits RecommendationsComponentBase -<MudContainer MaxWidth="MaxWidth.Large"> +<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge"> @if (IsLoading) { <MudGrid> @@ -22,125 +21,38 @@ } else { - <MudGrid> - @if (Profile == null) - { - <MudItem xs="12"> - <MudAlert Severity="Severity.Warning">Profile not set yet. You must update your profile to use Mood Match.</MudAlert> - </MudItem> - } - <MudItem xs="12"> - <MudPaper Class="pa-4"> - <MudGrid Class="position-relative"> - <div class="d-flex justify-content-center profile-avatar-container profile-backdrop"> - @if (!string.IsNullOrWhiteSpace(Profile?.BannerUrl)) - { - <MudImage Src="@(!string.IsNullOrWhiteSpace(_uploadedBannerPictureUri) ? _uploadedBannerPictureUri : Profile?.BannerUrl)" Alt="" Class="profile-backdrop"></MudImage> - } - else - { - <MudImage Src="@(_uploadedBannerPictureUri)" Alt="" Class="profile-backdrop"></MudImage> - } - <MudFileUpload T="IBrowserFile" Class="profile-picture-button" FilesChanged="LoadBannerPictureFile"> - <ButtonTemplate> - <MudButton HtmlTag="label" Color="Color.Transparent" Variant="Variant.Outlined" - Style="width: 100%; height: 100%" for="@context"> - </MudButton> - </ButtonTemplate> - </MudFileUpload> - </div> - <MudItem xs="3"></MudItem> - <MudItem xs="6" Class="d-flex justify-content-center"> - <div class="profile-avatar-container"> - <MudAvatar Color="Color.Primary" Style="width: 200px; height: 200px"> - @if (!string.IsNullOrWhiteSpace(Profile?.ProfilePictureUrl)) - { - <MudImage Src="@(!string.IsNullOrWhiteSpace(_uploadedProfilePictureUri) ? _uploadedProfilePictureUri : Profile?.ProfilePictureUrl)"></MudImage> - } - else - { - if (!string.IsNullOrWhiteSpace(_uploadedProfilePictureUri)) - { - <MudImage Src="@(_uploadedProfilePictureUri)"></MudImage> - } - else - { - @UserInfo?.Username?[0] - } - } - </MudAvatar> - <MudFileUpload T="IBrowserFile" Class="profile-picture-button" FilesChanged="LoadProfilePictureFile"> - <ButtonTemplate> - <MudFab HtmlTag="label" Color="Color.Transparent" Icon="@Icons.Material.Filled.AddCircleOutline" - Style="width: 100%; height: 100%" for="@context"> - </MudFab> - </ButtonTemplate> - </MudFileUpload> - </div> - </MudItem> - <MudItem xs="3" Class="position-relative"> - @if (_profilePictureFile != null) - { - <MudButton Color="Color.Secondary" Variant="Variant.Filled" OnClick="UploadProfilePicture" Disabled="_uploading" Style="position: absolute; bottom: 0; right: 0">Upload Profile Picture</MudButton> - } - @if (_bannerPictureFile != null) - { - <MudButton Color="Color.Secondary" Variant="Variant.Filled" OnClick="UploadBannerPicture" Disabled="_uploading" Style="position: absolute; top: 0; right: 0">Upload Banner</MudButton> - } - </MudItem> - </MudGrid> - </MudPaper> - </MudItem> - <MudItem xs="12" sm="12"> - <MudPaper Class="pa-4"> - @if (Profile == null) + <MudPaper Class="pa-4"> + <MudText Typo="Typo.h5" Class="d-flex justify-content-center"> + Shows similar to @SeriesInfo?.Name + </MudText> + <MudDivider DividerType="DividerType.FullWidth" Class="my-2"></MudDivider> + <MudGrid Style="justify-content: center !important;"> + @if (Recommendations == null || Recommendations.Count == 0) + { + <MudItem xs="12" sm="12"> + <MudPaper Class="pa-4 m-4" Style="text-align: center"> + <MudProgressLinear Color="Color.Primary" Indeterminate="true"/> + </MudPaper> + </MudItem> + } + else + { + @foreach (var series in Recommendations) { - <MudForm @ref="_form" Model="NewDetails"> - <MudTextField T="string" Label="First Name" Disabled="true" Value="UserInfo.Firstname"/> - <MudTextField T="string" Label="Surname" Disabled="true" Value="UserInfo.Surname"/> - <MudTextField T="string" Label="Email Address" Disabled="true" Value="UserInfo.EmailAddress"/> - <MudSelect T="string" Label="Gender" AnchorOrigin="Origin.TopCenter" @bind-Value="NewDetails.Gender"> - <MudSelectItem Value="@("Male")"/> - <MudSelectItem Value="@("Female")"/> - <MudSelectItem Value="@("Other")"/> - </MudSelect> - <MudSelect T="string" Label="Pronoun" AnchorOrigin="Origin.TopCenter" @bind-Value="NewDetails.Pronoun"> - <MudSelectItem Value="@("He/him")"/> - <MudSelectItem Value="@("She/her")"/> - <MudSelectItem Value="@("They/them")"/> - <MudSelectItem Value="@("Other")"/> - </MudSelect> - <MudTextField T="string" Label="Biography" @bind-Value="NewDetails.Biography" Placeholder="Enter your biography here."/> - <MudDatePicker Label="Date Of Birth" @bind-Date="_dob"/> - </MudForm> - } - else - { - <MudForm @ref="_form" Model="Profile"> - <MudTextField T="string" Label="First Name" Disabled="true" Value="UserInfo.Firstname"/> - <MudTextField T="string" Label="Surname" Disabled="true" Value="UserInfo.Surname"/> - <MudTextField T="string" Label="Email Address" Disabled="true" Value="UserInfo.EmailAddress"/> - <MudSelect T="string" Label="Gender" AnchorOrigin="Origin.TopCenter" @bind-Value="Profile.Gender"> - <MudSelectItem Value="@("Male")"/> - <MudSelectItem Value="@("Female")"/> - <MudSelectItem Value="@("Other")"/> - </MudSelect> - <MudSelect T="string" Label="Pronoun" AnchorOrigin="Origin.TopCenter" @bind-Value="Profile.Pronoun"> - <MudSelectItem Value="@("He/him")"/> - <MudSelectItem Value="@("She/her")"/> - <MudSelectItem Value="@("They/them")"/> - <MudSelectItem Value="@("Other")"/> - </MudSelect> - <MudTextField T="string" Label="Biography" @bind-Value="Profile.Biography"/> - <MudDatePicker Label="Date Of Birth" @bind-Date="_dob" Culture="CultureInfo.CurrentCulture" DateFormat="dd/MM/yyyy"/> - </MudForm> + <MudItem xs="2"> + <MudCard Outlined="true" @onclick="@(() => GoToSeries(series.Id))" Class="custom-card" Style="cursor: pointer; height: 500px"> + <MudCardHeader> + <CardHeaderContent> + <MudText Typo="Typo.body1">@series?.Name</MudText> + </CardHeaderContent> + </MudCardHeader> + <MudCardMedia Image="@series?.Poster" Style="height: 100%; object-fit: contain"/> + </MudCard> + </MudItem> } - </MudPaper> - <MudPaper Class="pa-4 mt-4 d-flex justify-content-center"> - <MudButton Disabled="@_uploading" Variant="Variant.Filled" Color="Color.Primary" DisableElevation="true" OnClick="@(CreateOrUpdateProfile)">Update Profile</MudButton> - </MudPaper> - </MudItem> - </MudGrid> + } + </MudGrid> + </MudPaper> } </MudContainer> @@ -149,22 +61,19 @@ protected override async Task OnInitializedAsync() { StartLoading(); - ProfileStateService.OnChange += StateHasChanged; try { - var profileInfo = await ProfileStateService.GetProfileInfo(); - if (profileInfo == null) + if (!string.IsNullOrWhiteSpace(NavigationManager.ToAbsoluteUri(NavigationManager.Uri).Query) && int.TryParse(NavigationManager.ToAbsoluteUri(NavigationManager.Uri).Query[1..], out var parsedInt)) { - var profileAsync = await ProfileClient.GetProfileForUserAsync(); - Profile = profileAsync.Data; - _dob = Profile?.DoB.GetValueOrDefault().Date; - await ProfileStateService.UpdateProfileInfoAsync(new ProfileInfo {ProfileDetails = Profile}); + SeriesId = parsedInt; } else { - Profile = profileInfo.ProfileDetails; - _dob = profileInfo.ProfileDetails?.DoB.GetValueOrDefault().Date; + NavigationManager.NavigateTo("/Profile", true); } + var series = await SeriesClient.Get_series_info_by_idAsync(SeriesId); + SeriesInfo = series.Series_info; + await GetRecommendations(); } catch (APIException ex) { @@ -173,148 +82,20 @@ StopLoading(); } - public void Dispose() - { - ProfileStateService.OnChange -= StateHasChanged; - } - - private async Task CreateOrUpdateProfile() - { - _uploading = true; - try - { - if (Profile == null) - { - NewDetails.DoB = _dob; - var profileAsync = await ProfileClient.CreateOrUpdateProfileAsync(NewDetails); - Profile = profileAsync.Data; - await ProfileStateService.UpdateProfileInfoAsync(new ProfileInfo {ProfileDetails = Profile}); - await DisplaySuccess("Profile successfully updated."); - } - else - { - Profile.DoB = _dob; - var profileAsync = await ProfileClient.CreateOrUpdateProfileAsync(Profile); - _uploading = true; - Profile = profileAsync.Data; - await ProfileStateService.UpdateProfileInfoAsync(new ProfileInfo {ProfileDetails = Profile}); - _uploading = false; - await DisplaySuccess("Profile successfully updated."); - } - StateHasChanged(); - } - catch (APIException ex) - { - await DisplayError(ErrorHelper.UnwrapError(ex).ToString()); - } - _uploading = false; - } - - private async Task UploadProfilePicture() - { - _uploading = true; - try - { - var profile = await ProfileClient.UploadProfilePictureAsync(_profilePictureFile); - if (Profile == null) - { - Profile = profile.Data; - if (profile.Data.DoB == null) - _dob = DateTime.Now; - } - else - { - Profile.ProfilePictureUrl = profile.Data.ProfilePictureUrl; - } - await ProfileStateService.UpdateProfileInfoAsync(new ProfileInfo {ProfileDetails = Profile}); - _profilePictureFile = null; - await DisplaySuccess("Successfully uploaded profile picture."); - } - catch (APIException ex) - { - await DisplayError(ErrorHelper.UnwrapError(ex).ToString()); - } - finally - { - _uploading = false; - } - } - - private async Task UploadBannerPicture() + private async Task GetRecommendations() { - _uploading = true; - try - { - var profile = await ProfileClient.UploadBannerPictureAsync(_bannerPictureFile); - if (Profile == null) - { - Profile = profile.Data; - if (profile.Data.DoB == null) - _dob = DateTime.Now; - } - else - { - Profile.BannerUrl = profile.Data.BannerUrl; - } - await ProfileStateService.UpdateProfileInfoAsync(new ProfileInfo {ProfileDetails = Profile}); - _bannerPictureFile = null; - await DisplaySuccess("Successfully uploaded banner."); - } - catch (APIException ex) - { - await DisplayError(ErrorHelper.UnwrapError(ex).ToString()); - } - finally - { - _uploading = false; - } - } - - private async Task LoadProfilePictureFile(IBrowserFile e) - { - var ms = new MemoryStream(); - await e.OpenReadStream(1000 * 10 * 1024).CopyToAsync(ms); - try - { - ms.Position = 0; - _profilePictureFile = new FileParameter(ms, e.Name, e.ContentType); - _uploadedProfilePictureUri = $"data:image/jpeg;base64, {Convert.ToBase64String(ms.ToArray())}"; - StateHasChanged(); - } - catch (Exception ex) - { - await DisplayError(ErrorHelper.UnwrapError(ex).ToString()); - } + var recommendations = await SeriesClient.Get_recommendations_by_idAsync(SeriesId); + Recommendations = recommendations.Recommendations.ToList(); + StateHasChanged(); } - private async Task LoadBannerPictureFile(IBrowserFile e) + private async Task GoToSeries(int id) { - var ms = new MemoryStream(); - await e.OpenReadStream(1000 * 10 * 1024).CopyToAsync(ms); - try - { - ms.Position = 0; - _bannerPictureFile = new FileParameter(ms, e.Name, e.ContentType); - _uploadedBannerPictureUri = $"data:image/jpeg;base64, {Convert.ToBase64String(ms.ToArray())}"; - StateHasChanged(); - } - catch (Exception ex) - { - await DisplayError(ErrorHelper.UnwrapError(ex).ToString()); - } + NavigationManager.NavigateTo($"/Series?{id}", false); } - [CascadingParameter] - public UserInfo? UserInfo { get; set; } - - private ProfileDTO? Profile { get; set; } = new(); - private ProfileDTO NewDetails { get; } = new(); - private DateTime? _dob; + private SeriesInfo? SeriesInfo { get; set; } + private int SeriesId { get; set; } + private List<SeriesInfo>? Recommendations { get; set; } = new(); - private MudForm _form = new(); - private FileParameter? _profilePictureFile; - private FileParameter? _bannerPictureFile; - private string? _uploadedProfilePictureUri; - private string? _uploadedBannerPictureUri; - private bool _uploading; } \ No newline at end of file diff --git a/TVSeriesRecommendationMicroservice/.gitignore b/TVSeriesRecommendationMicroservice/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1b119837045e7069fb062c5809405111262c4038 --- /dev/null +++ b/TVSeriesRecommendationMicroservice/.gitignore @@ -0,0 +1,2 @@ + +appsettings.json