From 7662ffdc8eefc008b6f56a69ab0118d667f77bb3 Mon Sep 17 00:00:00 2001 From: Samuel Adjabeng Date: Tue, 17 Dec 2024 21:02:19 +0100 Subject: [PATCH] Implement AJAX functionality for game reviews - Add GameDetailsApiController for handling review operations - Implement client-side JavaScript for dynamic updates - Add proper error handling and user feedback - Update UI with dropdown rating selection --- .../Controllers/GameDetailsApiController.cs | 125 +++++++++++++ GameLibrary/Database.db | Bin 167936 -> 167936 bytes GameLibrary/Pages/Games/Details.cshtml | 112 ++++++------ GameLibrary/Program.cs | 3 + GameLibrary/wwwroot/js/gameDetails.js | 169 ++++++++++++++++++ 5 files changed, 356 insertions(+), 53 deletions(-) create mode 100644 GameLibrary/Controllers/GameDetailsApiController.cs create mode 100644 GameLibrary/wwwroot/js/gameDetails.js diff --git a/GameLibrary/Controllers/GameDetailsApiController.cs b/GameLibrary/Controllers/GameDetailsApiController.cs new file mode 100644 index 0000000..f1094cd --- /dev/null +++ b/GameLibrary/Controllers/GameDetailsApiController.cs @@ -0,0 +1,125 @@ +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + +namespace GameLibrary.Controllers; + +[Route("api/games")] +[ApiController] +public class GameDetailsApiController : ControllerBase +{ + private readonly ApplicationDbContext _context; + + public GameDetailsApiController(ApplicationDbContext context) + { + _context = context; + } + + // GET: api/games/{id} + [HttpGet("{id}")] + public async Task GetGame(Guid id) + { + var game = await _context.Games + .Include(g => g.Reviews) + .FirstOrDefaultAsync(g => g.Id == id); + + if (game == null) + { + return NotFound(); + } + + return Ok(new + { + game.Id, + game.Title, + game.Description, + game.Developer, + game.Publisher, + game.ReleaseDate, + game.ImageUrl, + game.Rating, + ReviewCount = game.Reviews.Count + }); + } + + // GET: api/games/{id}/reviews + [HttpGet("{id}/reviews")] + public async Task GetReviews(Guid id) + { + var reviews = await _context.Reviews + .Include(r => r.User) + .Where(r => r.GameId == id) + .OrderByDescending(r => r.CreatedAt) + .Select(r => new + { + r.Id, + r.Rating, + Content = r.Content, + UserName = r.User.UserName, + CreatedAt = r.CreatedAt + }) + .ToListAsync(); + + return Ok(reviews); + } + + // POST: api/games/{id}/reviews + [HttpPost("{id}/reviews")] + public async Task CreateReview(Guid id, [FromBody] ReviewRequest request) + { + var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (userId == null) + { + return Unauthorized(); + } + + var review = new Review + { + GameId = id, + UserId = Guid.Parse(userId), + Rating = request.Rating, + Content = request.Comment, + CreatedAt = DateTime.UtcNow + }; + + _context.Reviews.Add(review); + await _context.SaveChangesAsync(); + + // Recalculate average rating + var averageRating = await _context.Reviews + .Where(r => r.GameId == id) + .AverageAsync(r => r.Rating); + + var game = await _context.Games.FindAsync(id); + if (game != null) + { + game.Rating = averageRating; + await _context.SaveChangesAsync(); + } + + // Return the new review with username + var reviewResponse = await _context.Reviews + .Include(r => r.User) + .Where(r => r.Id == review.Id) + .Select(r => new + { + r.Id, + r.Rating, + Content = r.Content, + UserName = r.User.UserName, + CreatedAt = r.CreatedAt, + GameRating = averageRating + }) + .FirstAsync(); + + return Ok(reviewResponse); + } +} + +public class ReviewRequest +{ + public int Rating { get; set; } + public string Comment { get; set; } = string.Empty; +} diff --git a/GameLibrary/Database.db b/GameLibrary/Database.db index ea794ae2a95dac0f61721be115bdf9b7f552d6a5..4d35e5c44d19bc8af9493e8ec4887be1d3602aa6 100644 GIT binary patch delta 5536 zcmb_gTWlO>72a#SzU)p)lBP*1*#x(C8qH?@`#+1)WUj8U*VnAw8X4rv+Hq=oU9Vj? z0cpE+iTV)PfsyJeLQo#a1E2=5XkxAp5lBUqP$eGnNb|r8MMa3`mNT<)>bPw(56r{l zGT;8^oAZC?eCMB6-fDW~t)^cj`dSjrZIR4->DOC|&25{H>^<0Aef31*(PA2|(<)qz zHC8A_QmIH|%HTv=mT**LBU7b@W~rvbP1Qt}V z3|K=w#41mNI&k86O>F5jjKqX#3~(!HNuI4NR5-R$+3wky!ra*cVaCG zWi+B(S1u`|%54x=K^bU|7o%Zxl<ZP8;1#O z8xmR#)FyMi{)I!;rBY=nv5NuAQ#~gc=ZMt~rhNua00YS#PEFHL9R`hx5_okTBecaH87aqs~et}YE5EI$9$|wU_3LUAsM3$;aDhM&SE~E&YOK_ZaF@6GAnmX2QGMt_c%Jhztns8Wb^j z3+y6qe*Iw8F3&GzXJ=k0WtYy)RI=syQnpgg&Mi-z%ij6H<$wQZ{SWZRM>p4Z2o@X~ zYk3S}VNklXCXxY7NRmTLCYdEvgHzZj*TkwWOjC6bx3SBpVIg!s%|}0f9`NQ@TB{S~ z@@#e*q?#RHUdnc6&yBqVEX&!2vE@oBJ5^rH&X-rR`QtlN2;2~)kmdx!8fUd*37;DQ zZkj2i3p)taW5XC?2@Cogx4@(+CQ`W2H;wza9sGh>#ryqQYp!0)0l6S=M~~ju-h3Jw z%y~H~uq5Kf=|c2Rv13a5YD5{$Oe@Wq1DQMNtC_9zhnXwN?aVVU4N&D$`W6_{Qo0(L zdK@1O0YWeOSL~^pRnjLdpk*soaKu z{QhSp-GApcK|X#D;A% zlS{-^jKG&Ds9A-7yHLwS7&fIE;h|=Pjdw4{T3dG%G=hRAc}>qDC`EMEv3O z%KHjy4-+`8oR7rQ*CLs}Wlp89rLO_+H_`*O{fP#jdg^gQM~<|(u43#$YcQz`J|Y;j zQOVH&;l~GVeK?!7qTl~8>wKnX%S!s=? zyl3D^gEzX~o(x)^@5s?)fJ|&$KN4%-c>73f=&q$f%;wCU%vRkYrTM_>!#;r2EiFWX0!s@MJFv8EVD&*C zUxNMJMm`7_SXvlhP_zy*G+`m8BE#AE?+)y4RZ%h}h*V>*;?;~wGYuI!beiD7`%$I)ZUq4$t zmP!|gSD!xuPX*H({Df>CS)B0^^(9u1Hl&Lkt8;BWLTAyn~(ri=ZrDZ*ct zk?7|9%C%@Tv3X*d#*`z8bg^r-dK{jeY-5ey*m~=YXD`II_B3ZYBC)2ABTXN-_+MK7 z)1<75CV!ozzmCq3cYf{38{b}weK)!{x!Tg~NA5ogk4}iMc?LZC*X7tqG?|D*qFXn* J)?&Yo{trU-AA0}* delta 380 zcmZvWze@sP7{}lDxi`4Zd+#}E4OBQnh_VQ5Xt2#eq#tlWhDd>~xHRzQ8LxpQYV6U}R%QJjF4}8A0mQiaN9k#0jZxZX7H)Ytaa@ovRK?n2+ z`Z!kT4y9jqD+-keN-Qaq!?_G-uIt_89q)Q_s$7v^5v6j6sAZY!J(QP}ab$P8zintd zMR3uI@sxU4=Rqn<2f#FJkRTd`b1tJc0~Xg@Dl%E>h<1VIr2h07kxZT2q223Hw)Qh> zjQ;m1udx#sFB7_=@QC0mjualC7)|NMKj?Q&_z{SUb_5Ron#9Z`U^d}nR__j(ZOoG- zv5w3OqbIG(JBC-!iYFCL2m!NCthCuz`Fqo@heF&<*Y{$4VPko7OBcrlaDul>$Wc~L L8a&8c!6W?w3R!XM diff --git a/GameLibrary/Pages/Games/Details.cshtml b/GameLibrary/Pages/Games/Details.cshtml index 6e64d94..d0a8750 100644 --- a/GameLibrary/Pages/Games/Details.cshtml +++ b/GameLibrary/Pages/Games/Details.cshtml @@ -8,6 +8,9 @@ } +
+
+
@@ -23,7 +26,7 @@ }
-

@Model.Game.Title

+

@Model.Game.Title

@for (var i = 1; i <= 5; i++) @@ -40,13 +43,13 @@ (@Model.Game.Rating.ToString("F1"))
-

@Model.Game.Description

+

@Model.Game.Description

-

Developer: @Model.Game.Developer

-

Publisher: @Model.Game.Publisher

-

Release Date: @Model.Game.ReleaseDate.ToString("MMMM dd, yyyy")

+

Developer: @Model.Game.Developer

+

Publisher: @Model.Game.Publisher

+

Release Date: @Model.Game.ReleaseDate.ToString("MMMM dd, yyyy")

Genre: @Model.Game.Genre

@@ -108,59 +111,62 @@

Reviews

-
-
-

Average Rating: @Model.AverageRating.ToString("F1")

-
- @for (var i = 1; i <= 5; i++) - { - if (Model.AverageRating >= i) - { - - } - else - { - - } - } -
+
+ @Html.AntiForgeryToken() +
+ +
-
- - @if (!Model.Reviews.Any(r => r.UserId.ToString() == User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value)) - { -
- -
-
- -
- - -
-
- - -
- - +
+ + +
+ + + +
+ @foreach (var review in Model.Reviews) + { +
+
+
@review.User.UserName
+
+ @for (var i = 1; i <= 5; i++) + { + if (review.Rating >= i) + { + + } + else + { + + } + } +
+

@review.Content

+ @review.CreatedAt.ToString("MMMM dd, yyyy")
-
- } - - + } +
+ +@section Scripts { + + +} diff --git a/GameLibrary/Program.cs b/GameLibrary/Program.cs index 823aeb3..ea3bb6c 100644 --- a/GameLibrary/Program.cs +++ b/GameLibrary/Program.cs @@ -74,6 +74,8 @@ public static void Main(string[] args) options.Level = CompressionLevel.SmallestSize; }); + builder.Services.AddControllers(); + var app = builder.Build(); app.UseResponseCompression(); @@ -100,6 +102,7 @@ public static void Main(string[] args) app.UseAuthorization(); app.MapRazorPages(); + app.MapControllers(); app.Run(); } diff --git a/GameLibrary/wwwroot/js/gameDetails.js b/GameLibrary/wwwroot/js/gameDetails.js new file mode 100644 index 0000000..d17cd18 --- /dev/null +++ b/GameLibrary/wwwroot/js/gameDetails.js @@ -0,0 +1,169 @@ +const gameDetails = { + init(gameId) { + this.gameId = gameId; + this.loadGameDetails(); + this.loadReviews(); + this.setupEventListeners(); + }, + + setupEventListeners() { + const form = document.getElementById('reviewForm'); + if (form) { + form.addEventListener('submit', (e) => { + e.preventDefault(); + this.submitReview(form); + }); + } + }, + + async loadGameDetails() { + try { + const response = await fetch(`/api/games/${this.gameId}`); + if (!response.ok) throw new Error('Failed to load game details'); + const data = await response.json(); + this.updateGameDetails(data); + } catch (error) { + console.error('Error loading game details:', error); + this.showError('Failed to load game details. Please try again.'); + } + }, + + async loadReviews() { + try { + const response = await fetch(`/api/games/${this.gameId}/reviews`); + if (!response.ok) throw new Error('Failed to load reviews'); + const reviews = await response.json(); + this.updateReviews(reviews); + } catch (error) { + console.error('Error loading reviews:', error); + this.showError('Failed to load reviews. Please try again.'); + } + }, + + async submitReview(form) { + const submitButton = form.querySelector('button[type="submit"]'); + const originalText = submitButton.textContent; + submitButton.disabled = true; + submitButton.textContent = 'Submitting...'; + + try { + const rating = form.querySelector('select[name="Rating"]').value; + const comment = form.querySelector('textarea[name="Comment"]').value; + + const response = await fetch(`/api/games/${this.gameId}/reviews`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + }, + body: JSON.stringify({ rating: parseInt(rating), comment }) + }); + + if (!response.ok) throw new Error('Failed to submit review'); + + const newReview = await response.json(); + this.addNewReview(newReview); + this.updateGameRating(newReview.gameRating); + form.reset(); + this.showSuccess('Review submitted successfully!'); + } catch (error) { + console.error('Error submitting review:', error); + this.showError('Failed to submit review. Please try again.'); + } finally { + submitButton.disabled = false; + submitButton.textContent = originalText; + } + }, + + updateGameDetails(game) { + document.getElementById('gameTitle').textContent = game.title; + document.getElementById('gameDescription').textContent = game.description; + document.getElementById('gameDeveloper').textContent = game.developer; + document.getElementById('gamePublisher').textContent = game.publisher; + document.getElementById('gameReleaseDate').textContent = new Date(game.releaseDate).toLocaleDateString(); + this.updateGameRating(game.rating); + }, + + updateGameRating(rating) { + const ratingContainer = document.querySelector('.game-rating'); + if (ratingContainer) { + const stars = ratingContainer.querySelectorAll('i'); + stars.forEach((star, index) => { + if (rating >= index + 1) { + star.className = 'fas fa-star text-warning'; + } else { + star.className = 'far fa-star text-warning'; + } + }); + const ratingText = ratingContainer.querySelector('span'); + if (ratingText) { + ratingText.textContent = `(${rating.toFixed(1)})`; + } + } + }, + + updateReviews(reviews) { + const container = document.getElementById('reviewsList'); + if (!container) return; + + container.innerHTML = reviews.map(review => ` +
+
+
${review.userName}
+
+ ${this.getStarRating(review.rating)} +
+
+

${review.content}

+ ${new Date(review.createdAt).toLocaleDateString()} +
+ `).join(''); + }, + + addNewReview(review) { + const container = document.getElementById('reviewsList'); + if (!container) return; + + const reviewElement = document.createElement('div'); + reviewElement.className = 'review mb-3'; + reviewElement.innerHTML = ` +
+
${review.userName}
+
+ ${this.getStarRating(review.rating)} +
+
+

${review.content}

+ ${new Date(review.createdAt).toLocaleDateString()} + `; + container.insertBefore(reviewElement, container.firstChild); + }, + + getStarRating(rating) { + return Array(5).fill(0).map((_, i) => + `` + ).join(''); + }, + + showError(message) { + const errorAlert = document.getElementById('errorAlert'); + const successAlert = document.getElementById('successAlert'); + if (successAlert) successAlert.classList.add('d-none'); + if (errorAlert) { + errorAlert.textContent = message; + errorAlert.classList.remove('d-none'); + setTimeout(() => errorAlert.classList.add('d-none'), 5000); + } + }, + + showSuccess(message) { + const successAlert = document.getElementById('successAlert'); + const errorAlert = document.getElementById('errorAlert'); + if (errorAlert) errorAlert.classList.add('d-none'); + if (successAlert) { + successAlert.textContent = message; + successAlert.classList.remove('d-none'); + setTimeout(() => successAlert.classList.add('d-none'), 5000); + } + } +};