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 ea794ae..4d35e5c 100644 Binary files a/GameLibrary/Database.db and b/GameLibrary/Database.db differ 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); + } + } +};