From 9a6362c42a424c51361bf6c120f5082eca7c1c23 Mon Sep 17 00:00:00 2001 From: Samuel Adjabeng Date: Tue, 12 Nov 2024 13:07:28 +0100 Subject: [PATCH 1/8] Added Details page Details page addition attempt --- GameLibrary/Data/ApplicationDbContext.cs | 70 +++++ GameLibrary/Data/DbInitializer.cs | 167 +++++++++++ .../20241111_DropUsersAndCreateSchema.cs | 143 ++++++++++ .../ApplicationDbContextModelSnapshot.cs | 227 +++++++++++++++ GameLibrary/Data/cleanup.sql | 9 + GameLibrary/Database.db | Bin 8192 -> 12288 bytes GameLibrary/Database.db-shm | Bin 0 -> 32768 bytes GameLibrary/Database.db-wal | 0 GameLibrary/GameLibrary.csproj | 26 +- ...0241112113926_InitialMigration.Designer.cs | 230 +++++++++++++++ .../20241112113926_InitialMigration.cs | 22 ++ GameLibrary/Models/Game.cs | 57 ++++ GameLibrary/Models/Review.cs | 44 +++ GameLibrary/Models/User.cs | 53 ++++ GameLibrary/Models/UserFavorite.cs | 38 +++ GameLibrary/Pages/About.cshtml | 8 + GameLibrary/Pages/About.cshtml.cs | 12 + GameLibrary/Pages/Games/Details.cshtml | 80 ++++++ GameLibrary/Pages/Games/Details.cshtml.cs | 50 ++++ GameLibrary/Pages/Games/Reviews.cshtml | 4 + GameLibrary/Pages/Games/Reviews.cshtml.cs | 32 +++ .../Pages/Games/Shared/_ReviewsList.cshtml | 51 ++++ GameLibrary/Pages/Index.cshtml | 32 ++- GameLibrary/Pages/Index.cshtml.cs | 19 +- GameLibrary/Pages/Shared/_Layout.cshtml | 41 +-- GameLibrary/Program.cs | 22 +- GameLibrary/wwwroot/css/games/details.css | 268 ++++++++++++++++++ GameLibrary/wwwroot/css/site.css | 40 ++- GameLibrary/wwwroot/js/games/details.js | 107 +++++++ 29 files changed, 1799 insertions(+), 53 deletions(-) create mode 100644 GameLibrary/Data/DbInitializer.cs create mode 100644 GameLibrary/Data/Migrations/20241111_DropUsersAndCreateSchema.cs create mode 100644 GameLibrary/Data/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 GameLibrary/Data/cleanup.sql create mode 100644 GameLibrary/Database.db-shm create mode 100644 GameLibrary/Database.db-wal create mode 100644 GameLibrary/Migrations/20241112113926_InitialMigration.Designer.cs create mode 100644 GameLibrary/Migrations/20241112113926_InitialMigration.cs create mode 100644 GameLibrary/Models/Game.cs create mode 100644 GameLibrary/Models/Review.cs create mode 100644 GameLibrary/Models/User.cs create mode 100644 GameLibrary/Models/UserFavorite.cs create mode 100644 GameLibrary/Pages/About.cshtml create mode 100644 GameLibrary/Pages/About.cshtml.cs create mode 100644 GameLibrary/Pages/Games/Details.cshtml create mode 100644 GameLibrary/Pages/Games/Details.cshtml.cs create mode 100644 GameLibrary/Pages/Games/Reviews.cshtml create mode 100644 GameLibrary/Pages/Games/Reviews.cshtml.cs create mode 100644 GameLibrary/Pages/Games/Shared/_ReviewsList.cshtml create mode 100644 GameLibrary/wwwroot/css/games/details.css create mode 100644 GameLibrary/wwwroot/js/games/details.js diff --git a/GameLibrary/Data/ApplicationDbContext.cs b/GameLibrary/Data/ApplicationDbContext.cs index 84ba9a5..e4aa753 100644 --- a/GameLibrary/Data/ApplicationDbContext.cs +++ b/GameLibrary/Data/ApplicationDbContext.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using GameLibrary.Models; using Microsoft.EntityFrameworkCore; namespace GameLibrary.Data; @@ -22,4 +23,73 @@ public ApplicationDbContext(DbContextOptions options) : base(options) { } + + public DbSet Games => Set(); + public DbSet Users => 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); + }); + + // User configurations + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Username).IsUnique(); + entity.HasIndex(e => e.Email).IsUnique(); + entity.Property(e => e.Username).IsRequired().HasMaxLength(50); + entity.Property(e => e.Email).IsRequired().HasMaxLength(100); + entity.Property(e => e.PasswordHash).IsRequired().HasMaxLength(255); + entity.Property(e => e.AvatarUrl).HasMaxLength(500); + }); + + // 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.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..c0ceed6 --- /dev/null +++ b/GameLibrary/Data/DbInitializer.cs @@ -0,0 +1,167 @@ +// 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.EntityFrameworkCore; +using BCrypt.Net; + +namespace GameLibrary.Data; + +public static class DbInitializer +{ + public static void Initialize(ApplicationDbContext context) + { + try + { + // Ensure database is created + context.Database.EnsureCreated(); + + // 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 + } + + // Add test user with proper password hashing + var user = new User + { + Username = "testuser", + Email = "test@example.com", + // Use higher work factor for better security + PasswordHash = BCrypt.Net.BCrypt.HashPassword("password123", workFactor: 12), + IsAdmin = false, + CreatedAt = DateTime.UtcNow + }; + + context.Users.Add(user); + context.SaveChanges(); // Save to get the user ID + + // Add test games + var games = new Game[] + { + new Game + { + 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), + Developer = "Nintendo EPD", + Publisher = "Nintendo", + ImageUrl = "https://assets.nintendo.com/image/upload/c_pad,f_auto,h_613,q_auto,w_1089/ncom/en_US/games/switch/t/the-legend-of-zelda-breath-of-the-wild-switch/hero?v=2021120201", + Rating = 4.9 + }, + new Game + { + Title = "Red Dead Redemption 2", + Description = "America, 1899. The end of the Wild West era has begun. After a robbery goes badly wrong in the western town of Blackwater, Arthur Morgan and the Van der Linde gang are forced to flee. With federal agents and the best bounty hunters in the nation massing on their heels, the gang must rob, steal and fight their way across the rugged heartland of America in order to survive.", + Genre = "Action", + ReleaseDate = new DateTime(2018, 10, 26), + Developer = "Rockstar Games", + Publisher = "Rockstar Games", + ImageUrl = "https://upload.wikimedia.org/wikipedia/en/thumb/4/44/Red_Dead_Redemption_II.jpg/220px-Red_Dead_Redemption_II.jpg", + Rating = 4.8 + }, + new Game + { + Title = "Cyberpunk 2077", + Description = "Cyberpunk 2077 is an open-world, action-adventure story set in Night City, a megalopolis obsessed with power, glamour, and body modification. You play as V, a mercenary outlaw going after a one-of-a-kind implant that is the key to immortality.", + Genre = "RPG", + ReleaseDate = new DateTime(2020, 12, 10), + Developer = "CD Projekt Red", + Publisher = "CD Projekt", + ImageUrl = "https://static.wikia.nocookie.net/cyberpunk/images/9/9f/Cyberpunk2077_box_art.jpg", + Rating = 4.5 + }, + new Game + { + Title = "Super Mario Bros. Wonder", + Description = "Join Mario and his friends on a wondrous adventure in a world where anything can happen! Explore a vibrant and unpredictable land filled with secrets, power-ups, and captivating challenges.", + Genre = "Platformer", + ReleaseDate = new DateTime(2023, 10, 20), + Developer = "Nintendo EPD", + Publisher = "Nintendo", + ImageUrl = "https://upload.wikimedia.org/wikipedia/en/3/34/Super_Mario_Bros._Wonder_cover_art.jpg", + Rating = 4.7 + }, + new Game + { + Title = "God of War", + Description = "Embark on an epic and emotional journey as Kratos, the Spartan warrior, seeks a new life in the Norse realm. Confront powerful gods and mythical creatures while grappling with his past and guiding his son, Atreus.", + Genre = "Action-Adventure", + ReleaseDate = new DateTime(2018, 4, 20), + Developer = "Santa Monica Studio", + Publisher = "Sony Interactive Entertainment", + ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a7/God_of_War_4_cover.jpg", + Rating = 4.8 + }, + new Game + { + Title = "Elden Ring", + Description = "Venture into the Lands Between, a vast and treacherous open world created by Hidetaka Miyazaki and George R. R. Martin. Unravel the mysteries of the Elden Ring, conquer demigods, and become the Elden Lord.", + Genre = "Action RPG", + ReleaseDate = new DateTime(2022, 2, 25), + Developer = "FromSoftware", + Publisher = "Bandai Namco Entertainment", + ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/b9/Elden_Ring_cover_art.jpg", + Rating = 4.6 + } + }; + + context.Games.AddRange(games); + context.SaveChanges(); + + // Add test reviews with validation + var reviews = new Review[] + { + new Review + { + GameId = games[0].Id, + UserId = user.Id, + 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 Review + { + GameId = games[1].Id, + UserId = user.Id, + Rating = 5, + Comment = "An absolute masterpiece. The attention to detail and storytelling are unmatched.", + CreatedAt = DateTime.UtcNow.AddDays(-3) + }, + new Review + { + GameId = games[2].Id, + UserId = user.Id, + 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(); + } + catch (Exception ex) + { + throw new ApplicationException("An error occurred while seeding the database.", ex); + } + } +} diff --git a/GameLibrary/Data/Migrations/20241111_DropUsersAndCreateSchema.cs b/GameLibrary/Data/Migrations/20241111_DropUsersAndCreateSchema.cs new file mode 100644 index 0000000..fa39fce --- /dev/null +++ b/GameLibrary/Data/Migrations/20241111_DropUsersAndCreateSchema.cs @@ -0,0 +1,143 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace GameLibrary.Data.Migrations; + +public partial class DropUsersAndCreateSchema : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + // Drop existing users table + migrationBuilder.Sql("DROP TABLE IF EXISTS users"); + + // Create new tables + 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", nullable: false), + Genre = table.Column(type: "TEXT", nullable: false), + ReleaseDate = table.Column(type: "TEXT", nullable: false), + Developer = table.Column(type: "TEXT", nullable: false), + Publisher = table.Column(type: "TEXT", nullable: false), + ImageUrl = table.Column(type: "TEXT", nullable: true), + Rating = table.Column(type: "REAL", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Games", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Username = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Email = table.Column(type: "TEXT", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: false), + AvatarUrl = table.Column(type: "TEXT", nullable: true), + IsAdmin = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + 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: "INTEGER", nullable: false), + Rating = table.Column(type: "INTEGER", nullable: false), + Comment = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Reviews", x => x.Id); + table.ForeignKey( + name: "FK_Reviews_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Reviews_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + 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: "INTEGER", 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_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserFavorites_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + 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_UserId", + table: "UserFavorites", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "UserFavorites"); + migrationBuilder.DropTable(name: "Reviews"); + migrationBuilder.DropTable(name: "Games"); + migrationBuilder.DropTable(name: "Users"); + + // Recreate original users table + migrationBuilder.Sql(@" + CREATE TABLE users ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + Firstname TEXT NOT NULL, + Lastname TEXT NOT NULL + )"); + } +} diff --git a/GameLibrary/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/GameLibrary/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..cfcee85 --- /dev/null +++ b/GameLibrary/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,227 @@ +// +using System; +using GameLibrary.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameLibrary.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + 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("INTEGER"); + + 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.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + 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("INTEGER"); + + 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("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("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/Data/cleanup.sql b/GameLibrary/Data/cleanup.sql new file mode 100644 index 0000000..45fa63b --- /dev/null +++ b/GameLibrary/Data/cleanup.sql @@ -0,0 +1,9 @@ +-- Delete migration history +DELETE FROM __EFMigrationsHistory; + +-- Drop existing tables if they exist +DROP TABLE IF EXISTS UserFavorites; +DROP TABLE IF EXISTS Reviews; +DROP TABLE IF EXISTS Games; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS Users; diff --git a/GameLibrary/Database.db b/GameLibrary/Database.db index 067335c06a921684e56db7e63c1f9d39de9ca480..709ec1118d19b109db4a6401b27b5a53c948ce73 100644 GIT binary patch literal 12288 zcmeI$%SyvQ6b9g#iFhfrao6pTjS5nlRIKidC1OZ=F-^rTBt)A^pf;FHL6`bIK2CAv z3-|;soht1@B6VB%4|7OPE@zT&H`BdvV-fLTlHP}c*T^g%!>a32B?fB*y_009U<00Izz00bcLZ-M6?-6+@V^u-q8 z%_s^2>#P~yrJ;zEadsYOB1xZeFS3SbnZCt+^VGGtmivsxcMDvGwrFV&H11nhK5w;U z_FR`Y+O4kdnRd(PTE_|g=+p7+rs?&$WA*ocT6WT8Fu4_%QJTq5{nfRwXS4P4b)AxU zJcu5%hml;l2jN5{-*x^hfu1}0Y+b)O^DN4w>Kl;*0Rad=00Izz00bZa0SG_<0uX?} zY6?}L*yY1q^t(y{WyJ}lB7|DegBiDe47@ZxR$7u7jW1@ST{}x8BUZt}B%&!7MWC_n)UP=Epypa2CZKmiI+;2#To0db8wc>n+a diff --git a/GameLibrary/Database.db-shm b/GameLibrary/Database.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10 GIT binary patch literal 32768 zcmeIuAr62r3 - - - net8.0 - enable - enable - - - - - - - + + net8.0 + enable + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/GameLibrary/Migrations/20241112113926_InitialMigration.Designer.cs b/GameLibrary/Migrations/20241112113926_InitialMigration.Designer.cs new file mode 100644 index 0000000..30cba16 --- /dev/null +++ b/GameLibrary/Migrations/20241112113926_InitialMigration.Designer.cs @@ -0,0 +1,230 @@ +// +using System; +using GameLibrary.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameLibrary.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20241112113926_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + 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("INTEGER"); + + 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.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + 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("INTEGER"); + + 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("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("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/20241112113926_InitialMigration.cs b/GameLibrary/Migrations/20241112113926_InitialMigration.cs new file mode 100644 index 0000000..436b1ed --- /dev/null +++ b/GameLibrary/Migrations/20241112113926_InitialMigration.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameLibrary.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} 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..21e96ab --- /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 int 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 new file mode 100644 index 0000000..906df75 --- /dev/null +++ b/GameLibrary/Models/User.cs @@ -0,0 +1,53 @@ +// 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; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Models; + +[Index(nameof(Username), IsUnique = true)] +[Index(nameof(Email), IsUnique = true)] +public class User +{ + public int Id { get; set; } + + [Required] + [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")] + [RegularExpression(@"^[a-zA-Z0-9_-]+$", ErrorMessage = "Username can only contain letters, numbers, underscores, and hyphens")] + public string Username { get; set; } = string.Empty; + + [Required] + [EmailAddress(ErrorMessage = "Invalid email address")] + [StringLength(100)] + public string Email { get; set; } = string.Empty; + + [Required] + [StringLength(255)] // BCrypt hash length is 60 characters + public string PasswordHash { get; set; } = string.Empty; + + [StringLength(500)] + [DataType(DataType.ImageUrl)] + public string? AvatarUrl { get; set; } + + public bool IsAdmin { get; set; } + + [Required] + [DataType(DataType.DateTime)] + public DateTime CreatedAt { get; set; } + + // Navigation properties + public ICollection Reviews { get; set; } = new List(); + public ICollection Favorites { get; set; } = new List(); +} diff --git a/GameLibrary/Models/UserFavorite.cs b/GameLibrary/Models/UserFavorite.cs new file mode 100644 index 0000000..1b499b2 --- /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 System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Models; + +[Index(nameof(UserId), nameof(GameId), IsUnique = true, Name = "IX_UserFavorites_UserGame")] +public class UserFavorite +{ + public int Id { get; set; } + + [Required] + public int 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..3f5331e --- /dev/null +++ b/GameLibrary/Pages/About.cshtml.cs @@ -0,0 +1,12 @@ +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..e758d1d --- /dev/null +++ b/GameLibrary/Pages/Games/Details.cshtml.cs @@ -0,0 +1,50 @@ +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/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..84ef423 --- /dev/null +++ b/GameLibrary/Pages/Games/Reviews.cshtml.cs @@ -0,0 +1,32 @@ +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..c659280 --- /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/Index.cshtml b/GameLibrary/Pages/Index.cshtml index f9ff8d4..5b00352 100644 --- a/GameLibrary/Pages/Index.cshtml +++ b/GameLibrary/Pages/Index.cshtml @@ -1,10 +1,32 @@ @page -@model IndexModel +@model GameLibrary.Pages.IndexModel @{ - ViewData["Title"] = "Home page"; + ViewData["Title"] = "Welcome to GAMELIB"; } -
-

Welcome

-

Learn about building Web apps with ASP.NET Core.

+

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 +
+
+
+ } +
+ +
+ © GAMELIB 2024 • v1.0 • • Powered by Group 17 +
diff --git a/GameLibrary/Pages/Index.cshtml.cs b/GameLibrary/Pages/Index.cshtml.cs index 109b51a..e5f5fb8 100644 --- a/GameLibrary/Pages/Index.cshtml.cs +++ b/GameLibrary/Pages/Index.cshtml.cs @@ -12,22 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.AspNetCore.Mvc; +using GameLibrary.Data; +using GameLibrary.Models; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; namespace GameLibrary.Pages; public class IndexModel : PageModel { - private readonly ILogger _logger; + private readonly ApplicationDbContext _context; - public IndexModel(ILogger logger) + public IndexModel(ApplicationDbContext context) { - _logger = logger; + _context = context; } - public void OnGet() - { + 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/Shared/_Layout.cshtml b/GameLibrary/Pages/Shared/_Layout.cshtml index 6290e2c..bfd1f0a 100644 --- a/GameLibrary/Pages/Shared/_Layout.cshtml +++ b/GameLibrary/Pages/Shared/_Layout.cshtml @@ -1,48 +1,57 @@  - + + @ViewData["Title"] - GameLibrary + @RenderSection("Styles", required: false) +
-
-
+
@RenderBody()
-