diff --git a/GameLibrary/Data/ApplicationDbContext.cs b/GameLibrary/Data/ApplicationDbContext.cs index 50716c8..aca3975 100644 --- a/GameLibrary/Data/ApplicationDbContext.cs +++ b/GameLibrary/Data/ApplicationDbContext.cs @@ -24,4 +24,52 @@ public ApplicationDbContext(DbContextOptions options) : base(options) { } + public DbSet Games => Set(); + public DbSet Reviews => Set(); + public DbSet UserFavorites => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + // Game configurations + modelBuilder.Entity(entity => + { + entity.Property(e => e.Title).IsRequired().HasMaxLength(100); + entity.Property(e => e.Description).IsRequired().HasMaxLength(2000); + entity.Property(e => e.Genre).IsRequired().HasMaxLength(50); + entity.Property(e => e.Developer).IsRequired().HasMaxLength(100); + entity.Property(e => e.Publisher).IsRequired().HasMaxLength(100); + entity.Property(e => e.ImageUrl).HasMaxLength(500); + entity.Property(e => e.Rating).HasPrecision(3, 1); + }); + // Review configurations + modelBuilder.Entity(entity => + { + entity.HasOne(r => r.Game) + .WithMany(g => g.Reviews) + .HasForeignKey(r => r.GameId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(r => r.User) + .WithMany(u => u.Reviews) + .HasForeignKey(r => r.UserId) + .OnDelete(DeleteBehavior.Cascade); + entity.Property(e => e.Comment).IsRequired().HasMaxLength(1000); + entity.Property(e => e.Rating).IsRequired(); + entity.ToTable(t => t.HasCheckConstraint("CK_Review_Rating", "Rating >= 1 AND Rating <= 5")); + }); + // UserFavorite configurations + modelBuilder.Entity(entity => + { + entity.HasOne(uf => uf.User) + .WithMany(u => u.Favorites) + .HasForeignKey(uf => uf.UserId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(uf => uf.Game) + .WithMany(g => g.UserFavorites) + .HasForeignKey(uf => uf.GameId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasIndex(e => new { e.UserId, e.GameId }) + .IsUnique() + .HasDatabaseName("IX_UserFavorites_UserGame"); + }); + } } diff --git a/GameLibrary/Data/DbInitializer.cs b/GameLibrary/Data/DbInitializer.cs new file mode 100644 index 0000000..16d0d3b --- /dev/null +++ b/GameLibrary/Data/DbInitializer.cs @@ -0,0 +1,117 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Data; + +public static class DbInitializer +{ + /// Seeding the database. + public static void Initialize(this IApplicationBuilder app) + { + using IServiceScope scope = app.ApplicationServices.CreateScope(); + var services = scope.ServiceProvider; + using ApplicationDbContext context = services.GetRequiredService(); + var userManager = services.GetRequiredService>(); + var roleManager = services.GetRequiredService>(); + + // Apply any pending migrations + if (context.Database.GetPendingMigrations().Any()) + { + context.Database.Migrate(); + } + + // Check if we already have games + if (context.Games.Any()) + { + return; // DB has been seeded + } + + // Ensure roles are created + var roles = new[] { "Administrator", "User" }; + foreach (var role in roles) + { + if (!roleManager.RoleExistsAsync(role).Result) + { + roleManager.CreateAsync(new Role { Name = role }).Wait(); + } + } + + var user = new User + { + UserName = "admin@example.com", + Email = "admin@example.com", + EmailConfirmed = true + }; + + if (userManager.FindByNameAsync(user.UserName).Result == null) + { + var result = userManager.CreateAsync(user, "Password123!").Result; + if (result.Succeeded) + { + userManager.AddToRoleAsync(user, "Administrator").Wait(); + } + } + + context.SaveChanges(); + + // Add test games + var games = new Game[] + { + new() { + Title = "The Legend of Zelda: Breath of the Wild", + Description = "Step into a world of discovery, exploration, and adventure in The Legend of Zelda: Breath of the Wild. Travel across vast fields, through forests, and to mountain peaks as you discover what has become of the kingdom of Hyrule in this stunning Open-Air Adventure.", + Genre = "Action-Adventure", + ReleaseDate = new DateTime(2017, 3, 3, 0, 0, 0, DateTimeKind.Utc), + Developer = "Nintendo EPD", + Publisher = "Nintendo" + } + }; + + context.Games.AddRange(games); + context.SaveChanges(); + + // Add test reviews with validation + var reviews = new Review[] + { + new() { + GameId = games[0].Id, + UserId = user.Id, // Use the inherited Id property from IdentityUser + Rating = 5, + Comment = "One of the best games I've ever played! The open world is breathtaking and there's so much to discover.", + CreatedAt = DateTime.UtcNow.AddDays(-5) + }, + new() { + GameId = games[0].Id, + UserId = user.Id, // Use the inherited Id property from IdentityUser + Rating = 5, + Comment = "An absolute masterpiece. The attention to detail and storytelling are unmatched.", + CreatedAt = DateTime.UtcNow.AddDays(-3) + }, + new() { + GameId = games[0].Id, + UserId = user.Id, // Use the inherited Id property from IdentityUser + Rating = 4, + Comment = "A visually stunning game with a deep story, though it had some bugs at launch.", + CreatedAt = DateTime.UtcNow.AddDays(-1) + } + }; + + context.Reviews.AddRange(reviews); + context.SaveChanges(); + } +} diff --git a/GameLibrary/Database.db b/GameLibrary/Database.db index 48beeee..9335ca8 100644 Binary files a/GameLibrary/Database.db and b/GameLibrary/Database.db differ diff --git a/GameLibrary/GameLibrary.csproj b/GameLibrary/GameLibrary.csproj index 5fc27bd..2c21f68 100644 --- a/GameLibrary/GameLibrary.csproj +++ b/GameLibrary/GameLibrary.csproj @@ -1,20 +1,18 @@ - - - - net8.0 - enable - enable - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + net8.0 + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/GameLibrary/Migrations/20241111182014_InitialCreate.Designer.cs b/GameLibrary/Migrations/20241114140155_Init.Designer.cs similarity index 63% rename from GameLibrary/Migrations/20241111182014_InitialCreate.Designer.cs rename to GameLibrary/Migrations/20241114140155_Init.Designer.cs index dfa2300..db259db 100644 --- a/GameLibrary/Migrations/20241111182014_InitialCreate.Designer.cs +++ b/GameLibrary/Migrations/20241114140155_Init.Designer.cs @@ -11,8 +11,8 @@ namespace GameLibrary.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20241111182014_InitialCreate")] - partial class InitialCreate + [Migration("20241114140155_Init")] + partial class Init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -20,6 +20,88 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.Entity("GameLibrary.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Developer") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Genre") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Publisher") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasPrecision(3, 1) + .HasColumnType("REAL"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("GameLibrary.Models.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("Reviews", t => + { + t.HasCheckConstraint("CK_Review_Rating", "Rating >= 1 AND Rating <= 5"); + }); + }); + modelBuilder.Entity("GameLibrary.Models.Role", b => { b.Property("Id") @@ -62,10 +144,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Address") .HasColumnType("TEXT"); + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("TEXT"); + b.Property("CreatedAt") + .HasColumnType("TEXT"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("TEXT"); @@ -102,9 +191,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("PhoneNumberConfirmed") .HasColumnType("INTEGER"); - b.Property("ProfilePicture") - .HasColumnType("TEXT"); - b.Property("SecurityStamp") .HasColumnType("TEXT"); @@ -127,6 +213,35 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("GameLibrary.Models.UserFavorite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId", "GameId") + .IsUnique() + .HasDatabaseName("IX_UserFavorites_UserGame"); + + b.HasIndex(new[] { "UserId", "GameId" }, "IX_UserFavorites_UserGame") + .IsUnique(); + + b.ToTable("UserFavorites"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.Property("Id") @@ -230,6 +345,44 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("GameLibrary.Models.Review", b => + { + b.HasOne("GameLibrary.Models.Game", "Game") + .WithMany("Reviews") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameLibrary.Models.User", "User") + .WithMany("Reviews") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GameLibrary.Models.UserFavorite", b => + { + b.HasOne("GameLibrary.Models.Game", "Game") + .WithMany("UserFavorites") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameLibrary.Models.User", "User") + .WithMany("Favorites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("GameLibrary.Models.Role", null) @@ -280,6 +433,20 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("GameLibrary.Models.Game", b => + { + b.Navigation("Reviews"); + + b.Navigation("UserFavorites"); + }); + + modelBuilder.Entity("GameLibrary.Models.User", b => + { + b.Navigation("Favorites"); + + b.Navigation("Reviews"); + }); #pragma warning restore 612, 618 } } diff --git a/GameLibrary/Migrations/20241111182014_InitialCreate.cs b/GameLibrary/Migrations/20241114140155_Init.cs similarity index 65% rename from GameLibrary/Migrations/20241111182014_InitialCreate.cs rename to GameLibrary/Migrations/20241114140155_Init.cs index 7b56f36..32def4e 100644 --- a/GameLibrary/Migrations/20241111182014_InitialCreate.cs +++ b/GameLibrary/Migrations/20241114140155_Init.cs @@ -6,7 +6,7 @@ namespace GameLibrary.Migrations { /// - public partial class InitialCreate : Migration + public partial class Init : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -34,7 +34,8 @@ protected override void Up(MigrationBuilder migrationBuilder) FirstName = table.Column(type: "TEXT", nullable: true), LastName = table.Column(type: "TEXT", nullable: true), Address = table.Column(type: "TEXT", nullable: true), - ProfilePicture = table.Column(type: "TEXT", nullable: true), + AvatarUrl = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), @@ -55,6 +56,26 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_AspNetUsers", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Games", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + Genre = table.Column(type: "TEXT", maxLength: 50, nullable: false), + ReleaseDate = table.Column(type: "TEXT", nullable: false), + Developer = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Publisher = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ImageUrl = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Rating = table.Column(type: "REAL", precision: 3, scale: 1, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Games", x => x.Id); + }); + migrationBuilder.CreateTable( name: "AspNetRoleClaims", columns: table => new @@ -161,6 +182,63 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "Reviews", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + GameId = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Rating = table.Column(type: "INTEGER", nullable: false), + Comment = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Reviews", x => x.Id); + table.CheckConstraint("CK_Review_Rating", "Rating >= 1 AND Rating <= 5"); + table.ForeignKey( + name: "FK_Reviews_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Reviews_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserFavorites", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + GameId = table.Column(type: "INTEGER", nullable: false), + AddedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserFavorites", x => x.Id); + table.ForeignKey( + name: "FK_UserFavorites_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserFavorites_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX_AspNetRoleClaims_RoleId", table: "AspNetRoleClaims", @@ -197,6 +275,27 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "AspNetUsers", column: "NormalizedUserName", unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Reviews_GameId", + table: "Reviews", + column: "GameId"); + + migrationBuilder.CreateIndex( + name: "IX_Reviews_UserId", + table: "Reviews", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserFavorites_GameId", + table: "UserFavorites", + column: "GameId"); + + migrationBuilder.CreateIndex( + name: "IX_UserFavorites_UserGame", + table: "UserFavorites", + columns: new[] { "UserId", "GameId" }, + unique: true); } /// @@ -217,11 +316,20 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "AspNetUserTokens"); + migrationBuilder.DropTable( + name: "Reviews"); + + migrationBuilder.DropTable( + name: "UserFavorites"); + migrationBuilder.DropTable( name: "AspNetRoles"); migrationBuilder.DropTable( name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Games"); } } } diff --git a/GameLibrary/Migrations/ApplicationDbContextModelSnapshot.cs b/GameLibrary/Migrations/ApplicationDbContextModelSnapshot.cs index 4f0fda2..e131c0e 100644 --- a/GameLibrary/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/GameLibrary/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,6 +17,88 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.Entity("GameLibrary.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Developer") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Genre") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Publisher") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasPrecision(3, 1) + .HasColumnType("REAL"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("GameLibrary.Models.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("Reviews", t => + { + t.HasCheckConstraint("CK_Review_Rating", "Rating >= 1 AND Rating <= 5"); + }); + }); + modelBuilder.Entity("GameLibrary.Models.Role", b => { b.Property("Id") @@ -59,10 +141,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Address") .HasColumnType("TEXT"); + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("TEXT"); + b.Property("CreatedAt") + .HasColumnType("TEXT"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("TEXT"); @@ -99,9 +188,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PhoneNumberConfirmed") .HasColumnType("INTEGER"); - b.Property("ProfilePicture") - .HasColumnType("TEXT"); - b.Property("SecurityStamp") .HasColumnType("TEXT"); @@ -124,6 +210,35 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("GameLibrary.Models.UserFavorite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId", "GameId") + .IsUnique() + .HasDatabaseName("IX_UserFavorites_UserGame"); + + b.HasIndex(new[] { "UserId", "GameId" }, "IX_UserFavorites_UserGame") + .IsUnique(); + + b.ToTable("UserFavorites"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.Property("Id") @@ -227,6 +342,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("GameLibrary.Models.Review", b => + { + b.HasOne("GameLibrary.Models.Game", "Game") + .WithMany("Reviews") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameLibrary.Models.User", "User") + .WithMany("Reviews") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GameLibrary.Models.UserFavorite", b => + { + b.HasOne("GameLibrary.Models.Game", "Game") + .WithMany("UserFavorites") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameLibrary.Models.User", "User") + .WithMany("Favorites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("GameLibrary.Models.Role", null) @@ -277,6 +430,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("GameLibrary.Models.Game", b => + { + b.Navigation("Reviews"); + + b.Navigation("UserFavorites"); + }); + + modelBuilder.Entity("GameLibrary.Models.User", b => + { + b.Navigation("Favorites"); + + b.Navigation("Reviews"); + }); #pragma warning restore 612, 618 } } diff --git a/GameLibrary/Models/Game.cs b/GameLibrary/Models/Game.cs new file mode 100644 index 0000000..fc8db10 --- /dev/null +++ b/GameLibrary/Models/Game.cs @@ -0,0 +1,57 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.Models; + +public class Game +{ + public int Id { get; set; } + + [Required] + [StringLength(100)] + public string Title { get; set; } = string.Empty; + + [Required] + [StringLength(2000)] + public string Description { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + public string Genre { get; set; } = string.Empty; + + [Required] + [DataType(DataType.Date)] + public DateTime ReleaseDate { get; set; } + + [Required] + [StringLength(100)] + public string Developer { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string Publisher { get; set; } = string.Empty; + + [StringLength(500)] + [DataType(DataType.ImageUrl)] + public string? ImageUrl { get; set; } + + [Range(0, 5.0)] + public double Rating { get; set; } + + // Navigation properties + public ICollection Reviews { get; set; } = new List(); + public ICollection UserFavorites { get; set; } = new List(); +} diff --git a/GameLibrary/Models/Review.cs b/GameLibrary/Models/Review.cs new file mode 100644 index 0000000..f356771 --- /dev/null +++ b/GameLibrary/Models/Review.cs @@ -0,0 +1,44 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.Models; + +public class Review +{ + public int Id { get; set; } + + [Required] + public int GameId { get; set; } + + [Required] + public Guid UserId { get; set; } + + [Required] + [Range(1, 5, ErrorMessage = "Rating must be between 1 and 5")] + public int Rating { get; set; } + + [Required] + [StringLength(1000, MinimumLength = 10, ErrorMessage = "Comment must be between 10 and 1000 characters")] + public string Comment { get; set; } = string.Empty; + + [Required] + [DataType(DataType.DateTime)] + public DateTime CreatedAt { get; set; } + + // Navigation properties + public Game? Game { get; set; } + public User? User { get; set; } +} diff --git a/GameLibrary/Models/User.cs b/GameLibrary/Models/User.cs index 658f428..5b29ea8 100644 --- a/GameLibrary/Models/User.cs +++ b/GameLibrary/Models/User.cs @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. + using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations; namespace GameLibrary.Models; @@ -21,5 +23,16 @@ public class User : IdentityUser public string? FirstName { get; set; } public string? LastName { get; set; } public string? Address { get; set; } - public string? ProfilePicture { get; set; } + + [StringLength(500)] + [DataType(DataType.ImageUrl)] + public string? AvatarUrl { get; set; } + + [Required] + [DataType(DataType.DateTime)] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + // Navigation properties + public ICollection Reviews { get; set; } = []; + public ICollection Favorites { get; set; } = []; } diff --git a/GameLibrary/Models/UserFavorite.cs b/GameLibrary/Models/UserFavorite.cs new file mode 100644 index 0000000..ca868c0 --- /dev/null +++ b/GameLibrary/Models/UserFavorite.cs @@ -0,0 +1,38 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.Models; + +[Index(nameof(UserId), nameof(GameId), IsUnique = true, Name = "IX_UserFavorites_UserGame")] +public class UserFavorite +{ + public int Id { get; set; } + + [Required] + public Guid UserId { get; set; } + + [Required] + public int GameId { get; set; } + + [Required] + [DataType(DataType.DateTime)] + public DateTime AddedAt { get; set; } + + // Navigation properties + public User? User { get; set; } + public Game? Game { get; set; } +} diff --git a/GameLibrary/Pages/About.cshtml b/GameLibrary/Pages/About.cshtml new file mode 100644 index 0000000..3b343e2 --- /dev/null +++ b/GameLibrary/Pages/About.cshtml @@ -0,0 +1,8 @@ +@page +@model GameLibrary.Pages.AboutModel +@{ + ViewData["Title"] = "About Us"; +} +

@ViewData["Title"]

+ +

Use this area to provide additional information.

diff --git a/GameLibrary/Pages/About.cshtml.cs b/GameLibrary/Pages/About.cshtml.cs new file mode 100644 index 0000000..6fe82e7 --- /dev/null +++ b/GameLibrary/Pages/About.cshtml.cs @@ -0,0 +1,26 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages +{ + public class AboutModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/GameLibrary/Pages/Games/Details.cshtml b/GameLibrary/Pages/Games/Details.cshtml new file mode 100644 index 0000000..13eda56 --- /dev/null +++ b/GameLibrary/Pages/Games/Details.cshtml @@ -0,0 +1,80 @@ +@page "{id:int}" +@model GameLibrary.Pages.Games.DetailsModel +@{ + ViewData["Title"] = Model.Game?.Title ?? "Game Details"; +} + +
+
+ @if (!string.IsNullOrEmpty(Model.Game?.ImageUrl)) + { + @Model.Game.Title + } +
+

@Model.Game?.Title

+
+ @Model.Game?.Genre + @Model.Game?.Developer + @Model.Game?.Publisher + @Model.Game?.ReleaseDate.ToString("MMMM d, yyyy") +
+
+ + @for (var i = 1; i <= 5; i++) + { + if (Model.Game?.Rating >= i) + { + + } + else if (Model.Game?.Rating > i - 1) + { + + } + else + { + + } + } + + @(Model.Game?.Rating.ToString("F1") ?? "N/A") +
+
+
+ +
+

About

+

@Model.Game?.Description

+
+ +
+
Loading reviews...
+
+ + @if (User.Identity?.IsAuthenticated == true) + { +
+

Add Your Review

+
+
+ @for (var i = 1; i <= 5; i++) + { + + + } +
+ + +
+
+ } +
+ +@section Styles { + + +} + +@section Scripts { + +} diff --git a/GameLibrary/Pages/Games/Details.cshtml.cs b/GameLibrary/Pages/Games/Details.cshtml.cs new file mode 100644 index 0000000..08f833b --- /dev/null +++ b/GameLibrary/Pages/Games/Details.cshtml.cs @@ -0,0 +1,64 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Games +{ + public class DetailsModel : PageModel + { + private readonly ApplicationDbContext _context; + + public DetailsModel(ApplicationDbContext context) + { + _context = context; + } + + public Game? Game { get; set; } + + public async Task OnGetAsync(int id) + { + if (_context.Games == null) + { + return NotFound(); + } + + var game = await _context.Games.FirstOrDefaultAsync(m => m.Id == id); + if (game == null) + { + return NotFound(); + } + else + { + Game = game; + } + return Page(); + } + + public async Task OnGetReviewsAsync(int id) + { + var reviews = await _context.Reviews + .Include(r => r.User) + .Where(r => r.GameId == id) + .OrderByDescending(r => r.CreatedAt) + .ToListAsync(); + + return Partial("_ReviewsList", reviews); + } + } +} diff --git a/GameLibrary/Pages/Games/Index.cshtml b/GameLibrary/Pages/Games/Index.cshtml new file mode 100644 index 0000000..c545468 --- /dev/null +++ b/GameLibrary/Pages/Games/Index.cshtml @@ -0,0 +1,28 @@ +@page +@model GameLibrary.Pages.Games.IndexModel +@{ + ViewData["Title"] = "Welcome to GAMELIB"; +} + +

Welcome to GAMELIB

+ +
+

Game List

+
+ @foreach (var game in Model.Games) + { +
+
+ @game.Title +
+
@game.Title
+

@game.Description.Substring(0, Math.Min(100, @game.Description.Length)) + "..." +

+ View Details +
+
+
+ } +
+
diff --git a/GameLibrary/Pages/Games/Index.cshtml.cs b/GameLibrary/Pages/Games/Index.cshtml.cs new file mode 100644 index 0000000..d007ea2 --- /dev/null +++ b/GameLibrary/Pages/Games/Index.cshtml.cs @@ -0,0 +1,41 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Games; + +public class IndexModel : PageModel +{ + private readonly ApplicationDbContext _context; + + public IndexModel(ApplicationDbContext context) + { + _context = context; + } + + public IList Games { get; set; } = new List(); + + public async Task OnGetAsync() + { + Games = await _context.Games + .OrderByDescending(g => g.Rating) + .ThenByDescending(g => g.ReleaseDate) + .ToListAsync(); + } +} diff --git a/GameLibrary/Pages/Games/Reviews.cshtml b/GameLibrary/Pages/Games/Reviews.cshtml new file mode 100644 index 0000000..fcf09e8 --- /dev/null +++ b/GameLibrary/Pages/Games/Reviews.cshtml @@ -0,0 +1,4 @@ +@page +@model GameLibrary.Pages.Games.ReviewsModel + + diff --git a/GameLibrary/Pages/Games/Reviews.cshtml.cs b/GameLibrary/Pages/Games/Reviews.cshtml.cs new file mode 100644 index 0000000..39fa303 --- /dev/null +++ b/GameLibrary/Pages/Games/Reviews.cshtml.cs @@ -0,0 +1,46 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Games +{ + public class ReviewsModel : PageModel + { + private readonly ApplicationDbContext _context; + + public ReviewsModel(ApplicationDbContext context) + { + _context = context; + Reviews = new List(); // Initialize with an empty list + } + + public IEnumerable Reviews { get; set; } + + public async Task OnGetAsync(int id) + { + Reviews = await _context.Reviews + .Include(r => r.User) + .Where(r => r.GameId == id) + .OrderByDescending(r => r.CreatedAt) + .ToListAsync(); + + return Partial("_ReviewsList", Reviews); + } + } +} diff --git a/GameLibrary/Pages/Games/Shared/_ReviewsList.cshtml b/GameLibrary/Pages/Games/Shared/_ReviewsList.cshtml new file mode 100644 index 0000000..df31420 --- /dev/null +++ b/GameLibrary/Pages/Games/Shared/_ReviewsList.cshtml @@ -0,0 +1,51 @@ +@model IEnumerable + +
+

User Reviews

+ @if (!Model.Any()) + { +

No reviews yet. Be the first to review this game!

+ } + else + { + foreach (var review in Model) + { +
+
+
+ @if (!string.IsNullOrEmpty(review.User?.AvatarUrl)) + { + @review.User.UserName + } + else + { +
+ +
+ } + @review.User?.UserName +
+
+ @for (var i = 1; i <= 5; i++) + { + if (review.Rating >= i) + { + + } + else + { + + } + } +
+
+
+

@review.Comment

+
+ +
+ } + } +
diff --git a/GameLibrary/Pages/Shared/_Layout.cshtml b/GameLibrary/Pages/Shared/_Layout.cshtml index 38020a3..74956ff 100644 --- a/GameLibrary/Pages/Shared/_Layout.cshtml +++ b/GameLibrary/Pages/Shared/_Layout.cshtml @@ -1,25 +1,30 @@  + @ViewData["Title"] - GameLibrary - + +