Changeset fa0fbaf
- Timestamp:
- 09/01/26 06:11:34 (4 weeks ago)
- Branches:
- main
- Children:
- d2eccb3
- Parents:
- 08aefc6
- Files:
-
- 13 added
- 5 deleted
- 48 edited
-
KernelRecordsMVC.Application/Services/AccountService.cs (deleted)
-
KernelRecordsMVC.Application/Services/OrderService.cs (deleted)
-
KernelRecordsMVC.Application/Services/ProductService.cs (deleted)
-
KernelRecordsMVC.Application/Services/ReleaseService.cs (deleted)
-
KernelRecordsMVC.Application/ViewModels/CreateReleaseViewModel.cs (added)
-
KernelRecordsMVC.Application/ViewModels/CreateTrackViewModel.cs (added)
-
KernelRecordsMVC.Application/ViewModels/ProfileViewModel.cs (added)
-
KernelRecordsMVC.Application/ViewModels/SaleItemViewModel.cs (added)
-
KernelRecordsMVC.Web/Controllers/AccountController.cs (modified) (10 diffs)
-
KernelRecordsMVC.Web/Controllers/AdminController.cs (modified) (10 diffs)
-
KernelRecordsMVC.Web/Controllers/HomeController.cs (modified) (1 diff)
-
KernelRecordsMVC.Web/Controllers/OrderController.cs (modified) (5 diffs)
-
KernelRecordsMVC.Web/Controllers/ReleaseController.cs (modified) (4 diffs)
-
KernelRecordsMVC.Web/Controllers/SaleController.cs (added)
-
KernelRecordsMVC.Web/Controllers/TopSellersController.cs (added)
-
KernelRecordsMVC.Web/Controllers/WishlistController.cs (added)
-
KernelRecordsMVC.Web/KernelRecordsMVC.Web.csproj (modified) (2 diffs)
-
KernelRecordsMVC.Web/Program.cs (modified) (3 diffs)
-
KernelRecordsMVC.Web/Views/Account/Profile.cshtml (added)
-
KernelRecordsMVC.Web/Views/Admin/CreateProduct.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/Admin/CreateRelease.cshtml (added)
-
KernelRecordsMVC.Web/Views/Admin/EditProduct.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/Admin/Index.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/Admin/Products.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/Home/Index.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/Order/Cart.cshtml (modified) (2 diffs)
-
KernelRecordsMVC.Web/Views/Release/Details.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/Release/Index.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/Sale/Index.cshtml (added)
-
KernelRecordsMVC.Web/Views/Shared/_Layout.cshtml (modified) (1 diff)
-
KernelRecordsMVC.Web/Views/TopSellers/Index.cshtml (added)
-
KernelRecordsMVC.Web/Views/Wishlist/Index.cshtml (added)
-
KernelRecordsMVC.Web/appsettings.json (modified) (2 diffs)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Application.dll (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Application.pdb (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Domain.dll (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Domain.pdb (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Infrastructure.dll (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Infrastructure.pdb (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Web.deps.json (modified) (1 diff)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Web.dll (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Web.exe (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Web.pdb (modified) ( previous)
-
KernelRecordsMVC.Web/bin/Debug/net8.0/appsettings.json (modified) (2 diffs)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.AssemblyInfo.cs (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.AssemblyInfoInputs.cache (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.GeneratedMSBuildEditorConfig.editorconfig (modified) (3 diffs)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.assets.cache (modified) ( previous)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.csproj.AssemblyReference.cache (modified) ( previous)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.csproj.CoreCompileInputs.cache (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.csproj.FileListAbsolute.txt (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.dll (modified) ( previous)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.pdb (modified) ( previous)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.sourcelink.json (added)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/apphost.exe (modified) ( previous)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/ref/KernelRecordsMVC.Web.dll (modified) ( previous)
-
KernelRecordsMVC.Web/obj/Debug/net8.0/refint/KernelRecordsMVC.Web.dll (modified) ( previous)
-
KernelRecordsMVC.Web/obj/KernelRecordsMVC.Web.csproj.nuget.dgspec.json (modified) (5 diffs)
-
KernelRecordsMVC.Web/obj/KernelRecordsMVC.Web.csproj.nuget.g.props (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/project.assets.json (modified) (3 diffs)
-
KernelRecordsMVC.Web/obj/project.nuget.cache (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/project.packagespec.json (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/rider.project.model.nuget.info (modified) (1 diff)
-
KernelRecordsMVC.Web/obj/rider.project.restore.info (modified) (1 diff)
-
KernelRecordsMVC.Web/wwwroot/css/site.css (modified) (1 diff)
-
KernelRecordsMVC/KernelRecordsMVC.csproj (deleted)
Legend:
- Unmodified
- Added
- Removed
-
KernelRecordsMVC.Web/Controllers/AccountController.cs
r08aefc6 rfa0fbaf 1 using Microsoft.AspNetCore.Mvc; 2 using KernelRecordsMVC.Data; 1 using System.Security.Claims; 2 using KernelRecordsMVC.Application.ViewModels; 3 using KernelRecordsMVC.Infrastructure.Data; 3 4 using KernelRecordsMVC.Models; 4 using KernelRecordsMVC.Application.ViewModels; 5 using Microsoft.AspNetCore.Http; 6 7 namespace KernelRecordsMVC.Controllers; 5 using Microsoft.AspNetCore.Authentication; 6 using Microsoft.AspNetCore.Mvc; 7 using Microsoft.EntityFrameworkCore; 8 9 namespace KernelRecordsMVC.Web.Controllers; 8 10 9 11 public class AccountController : Controller … … 16 18 } 17 19 18 // ============================== 20 21 // ========================================================= 19 22 // REGISTER - GET 20 // ============================== 23 // ========================================================= 21 24 22 25 [HttpGet] … … 27 30 28 31 29 // ============================== 32 // ========================================================= 30 33 // REGISTER - POST 31 // ============================== 34 // ========================================================= 32 35 33 36 [HttpPost] … … 61 64 Username = model.Username, 62 65 63 // We'll replace this with proper password hashing64 // in the security cleanup section.66 // TODO: 67 // Replace with password hashing later. 65 68 Password = model.Password, 66 69 … … 74 77 _context.SaveChanges(); 75 78 79 76 80 // Every normal registered user is a Consumer. 77 81 var consumer = new Consumer … … 89 93 90 94 91 // ============================== 95 // ========================================================= 92 96 // LOGIN - GET 93 // ============================== 97 // ========================================================= 94 98 95 99 [HttpGet] … … 100 104 101 105 102 // ============================== 106 // ========================================================= 103 107 // LOGIN - POST 104 // ============================== 108 // ========================================================= 105 109 106 110 [HttpPost] 107 111 [ValidateAntiForgeryToken] 108 public IActionResult Login(LoginViewModel model) 112 public async Task<IActionResult> Login( 113 LoginViewModel model) 109 114 { 110 115 if (!ModelState.IsValid) … … 125 130 } 126 131 132 133 // ===================================================== 134 // DETERMINE ACCOUNT TYPE 135 // ===================================================== 136 137 var admin = _context.Admins 138 .FirstOrDefault(x => x.UserId == user.UserId); 139 140 string role; 141 142 if (admin != null) 143 { 144 role = "Admin"; 145 } 146 else 147 { 148 role = "Consumer"; 149 } 150 151 152 // ===================================================== 153 // CREATE CLAIMS 154 // ===================================================== 155 156 var claims = new List<Claim> 157 { 158 new Claim( 159 ClaimTypes.NameIdentifier, 160 user.UserId.ToString()), 161 162 new Claim( 163 ClaimTypes.Name, 164 user.Username), 165 166 new Claim( 167 ClaimTypes.Email, 168 user.Email), 169 170 new Claim( 171 ClaimTypes.Role, 172 role) 173 }; 174 175 176 if (admin != null) 177 { 178 claims.Add( 179 new Claim( 180 "AdminType", 181 admin.Type.ToString())); 182 } 183 184 185 var identity = new ClaimsIdentity( 186 claims, 187 "Cookies"); 188 189 var principal = 190 new ClaimsPrincipal(identity); 191 192 193 // ===================================================== 194 // SIGN IN 195 // ===================================================== 196 197 await HttpContext.SignInAsync( 198 "Cookies", 199 principal); 200 201 202 // ===================================================== 203 // KEEP SESSION FOR EXISTING CODE 204 // ===================================================== 205 127 206 HttpContext.Session.SetInt32( 128 207 "UserId", … … 133 212 user.Username); 134 213 135 // Determine account type from the actual 136 // ADMINS / CONSUMERS tables. 137 var admin = _context.Admins 138 .FirstOrDefault(x => x.UserId == user.UserId); 214 HttpContext.Session.SetString( 215 "Role", 216 role); 139 217 140 218 if (admin != null) 141 219 { 142 HttpContext.Session.SetString(143 "Role",144 "Admin");145 146 220 HttpContext.Session.SetString( 147 221 "AdminType", 148 222 admin.Type.ToString()); 149 223 } 150 else 151 { 152 HttpContext.Session.SetString( 153 "Role", 154 "Consumer"); 155 } 224 156 225 157 226 return RedirectToAction( … … 161 230 162 231 163 // ============================== 232 // ========================================================= 233 // PROFILE - GET 234 // ========================================================= 235 236 [HttpGet] 237 public IActionResult Profile() 238 { 239 // Get logged-in user's ID from authentication claims. 240 var userIdClaim = User.FindFirst( 241 ClaimTypes.NameIdentifier); 242 243 if (userIdClaim == null) 244 return RedirectToAction(nameof(Login)); 245 246 if (!long.TryParse( 247 userIdClaim.Value, 248 out var userId)) 249 { 250 return RedirectToAction(nameof(Login)); 251 } 252 253 254 // Load user + consumer information. 255 var user = _context.Users 256 .Include(x => x.Consumer) 257 .FirstOrDefault(x => 258 x.UserId == userId); 259 260 if (user == null) 261 return NotFound(); 262 263 264 var model = new ProfileViewModel 265 { 266 UserId = user.UserId, 267 268 Username = user.Username, 269 270 Email = user.Email, 271 272 TelephoneNumber = 273 user.TelephoneNumber, 274 275 ShippingAddress = 276 user.ShippingAddress, 277 278 DateCreated = 279 user.DateCreated, 280 281 PointsCollected = 282 user.Consumer?.PointsCollected ?? 0 283 }; 284 285 286 return View(model); 287 } 288 289 290 // ========================================================= 291 // PROFILE - POST 292 // ========================================================= 293 294 [HttpPost] 295 [ValidateAntiForgeryToken] 296 public async Task<IActionResult> Profile( 297 ProfileViewModel model) 298 { 299 // Get the currently authenticated user. 300 var userIdClaim = User.FindFirst( 301 ClaimTypes.NameIdentifier); 302 303 if (userIdClaim == null) 304 return RedirectToAction(nameof(Login)); 305 306 if (!long.TryParse( 307 userIdClaim.Value, 308 out var userId)) 309 { 310 return RedirectToAction(nameof(Login)); 311 } 312 313 314 if (!ModelState.IsValid) 315 return View(model); 316 317 318 var user = _context.Users 319 .FirstOrDefault(x => 320 x.UserId == userId); 321 322 if (user == null) 323 return NotFound(); 324 325 326 // ===================================================== 327 // CHECK EMAIL 328 // ===================================================== 329 330 var emailExists = _context.Users.Any(x => 331 x.Email == model.Email && 332 x.UserId != userId); 333 334 if (emailExists) 335 { 336 ModelState.AddModelError( 337 nameof(model.Email), 338 "This email is already registered."); 339 340 return View(model); 341 } 342 343 344 // ===================================================== 345 // UPDATE USER 346 // ===================================================== 347 348 user.Email = model.Email; 349 350 user.TelephoneNumber = 351 model.TelephoneNumber; 352 353 user.ShippingAddress = 354 model.ShippingAddress; 355 356 357 _context.SaveChanges(); 358 359 360 TempData["Success"] = 361 "Your profile has been updated successfully."; 362 363 364 // ===================================================== 365 // REFRESH EMAIL CLAIM 366 // ===================================================== 367 368 var claims = new List<Claim> 369 { 370 new Claim( 371 ClaimTypes.NameIdentifier, 372 user.UserId.ToString()), 373 374 new Claim( 375 ClaimTypes.Name, 376 user.Username), 377 378 new Claim( 379 ClaimTypes.Email, 380 user.Email), 381 382 new Claim( 383 ClaimTypes.Role, 384 User.IsInRole("Admin") 385 ? "Admin" 386 : "Consumer") 387 }; 388 389 390 var identity = new ClaimsIdentity( 391 claims, 392 "Cookies"); 393 394 var principal = 395 new ClaimsPrincipal(identity); 396 397 398 await HttpContext.SignInAsync( 399 "Cookies", 400 principal); 401 402 403 // Keep session synchronized. 404 HttpContext.Session.SetString( 405 "Username", 406 user.Username); 407 408 409 return RedirectToAction(nameof(Profile)); 410 } 411 412 413 // ========================================================= 164 414 // LOGOUT 165 // ============================== 166 167 public IActionResult Logout() 168 { 415 // ========================================================= 416 417 [HttpPost] 418 [ValidateAntiForgeryToken] 419 public async Task<IActionResult> Logout() 420 { 421 await HttpContext.SignOutAsync("Cookies"); 422 169 423 HttpContext.Session.Clear(); 170 424 -
KernelRecordsMVC.Web/Controllers/AdminController.cs
r08aefc6 rfa0fbaf 1 using Microsoft.AspNetCore.Mvc; 1 using KernelRecordsMVC.Application.ViewModels; 2 using KernelRecordsMVC.Domain.Enums; 3 using KernelRecordsMVC.Infrastructure.Data; 4 using KernelRecordsMVC.Models; 5 using Microsoft.AspNetCore.Mvc; 2 6 using Microsoft.EntityFrameworkCore; 3 using KernelRecordsMVC.Data; 4 using KernelRecordsMVC.Models; 5 using KernelRecordsMVC.Domain.Enums; 6 using KernelRecordsMVC.Application.ViewModels; 7 using Microsoft.AspNetCore.Http; 8 9 namespace KernelRecordsMVC.Controllers; 7 8 namespace KernelRecordsMVC.Web.Controllers; 10 9 11 10 public class AdminController : Controller … … 19 18 20 19 21 // ========================================== 20 // ========================================================= 22 21 // ADMIN DASHBOARD 23 // ========================================== 22 // ========================================================= 24 23 25 24 [HttpGet] … … 29 28 return Forbid(); 30 29 30 31 ViewBag.ProductCount = 32 _context.Products.Count(); 33 34 ViewBag.ReleaseCount = 35 _context.Releases.Count(); 36 37 ViewBag.OutOfStockCount = 38 _context.Products.Count(x => 39 x.Stock <= 0); 40 41 ViewBag.LowStockCount = 42 _context.Products.Count(x => 43 x.Stock > 0 && 44 x.Stock <= 5); 45 46 31 47 return View(); 32 48 } 33 49 34 50 35 // ========================================== 36 // UC008 37 // NEW PRODUCT - GET 38 // ========================================== 51 // ========================================================= 52 // PRODUCTS 53 // ========================================================= 54 55 [HttpGet] 56 public IActionResult Products() 57 { 58 if (!IsProductManager()) 59 return Forbid(); 60 61 62 var products = _context.Products 63 .Include(p => p.Release) 64 .OrderBy(p => p.Release.Title) 65 .ThenBy(p => p.Format) 66 .ToList(); 67 68 69 return View(products); 70 } 71 72 73 // ========================================================= 74 // CREATE PRODUCT - GET 75 // ========================================================= 39 76 40 77 [HttpGet] … … 44 81 return Forbid(); 45 82 46 ViewBag.Releases = _context.Releases 47 .OrderBy(x => x.Title)48 .ToList(); 49 50 return View( new CreateProductViewModel());51 }52 53 54 // ========================================== 55 // UC00856 // NEWPRODUCT - POST57 // ========================================== 83 84 LoadReleases(); 85 86 87 return View( 88 new CreateProductViewModel()); 89 } 90 91 92 // ========================================================= 93 // CREATE PRODUCT - POST 94 // ========================================================= 58 95 59 96 [HttpPost] … … 65 102 return Forbid(); 66 103 104 67 105 if (!ModelState.IsValid) 68 106 { 69 107 LoadReleases(); 108 70 109 return View(model); 71 110 } 72 111 73 112 74 // Make sure the release actually exists.75 113 var release = _context.Releases 76 114 .FirstOrDefault(x => 77 115 x.ReleaseId == model.ReleaseId); 78 116 117 79 118 if (release == null) 80 119 { 81 120 ModelState.AddModelError( 82 "ReleaseId",121 nameof(model.ReleaseId), 83 122 "The selected release does not exist."); 84 123 124 85 125 LoadReleases(); 126 127 86 128 return View(model); 87 129 } 88 130 89 131 90 // Don't allow duplicate format for91 // the same release.92 var alreadyExists = _context.Products.Any(p =>93 p.ReleaseId == model.ReleaseId &&94 p.Format == model.Format); 132 var alreadyExists = 133 _context.Products.Any(p => 134 p.ReleaseId == model.ReleaseId && 135 p.Format == model.Format); 136 95 137 96 138 if (alreadyExists) 97 139 { 98 140 ModelState.AddModelError( 99 "Format",141 nameof(model.Format), 100 142 "This release already has a product in this format."); 101 143 144 102 145 LoadReleases(); 146 147 103 148 return View(model); 104 149 } 105 150 106 151 107 var product = new Product 108 { 109 ProductId = GetNextProductId(), 110 111 ReleaseId = model.ReleaseId, 112 113 Format = model.Format, 114 115 Price = model.Price, 116 117 ProductDescription = 118 model.ProductDescription, 119 120 Stock = model.Stock 121 }; 122 123 124 _context.Products.Add(product); 125 126 _context.SaveChanges(); 127 128 129 // Record the modification. 130 CreateModification( 131 ModificationType.CREATE, 132 product.ProductId); 133 134 135 TempData["Success"] = 136 "Product created successfully."; 137 138 139 return RedirectToAction( 140 nameof(Index)); 141 } 142 143 144 // ========================================== 145 // HELPERS 146 // ========================================== 152 using var transaction = 153 _context.Database.BeginTransaction(); 154 155 156 try 157 { 158 var product = new Product 159 { 160 ProductId = 161 GetNextProductId(), 162 163 ReleaseId = 164 model.ReleaseId, 165 166 Format = 167 model.Format, 168 169 Price = 170 model.Price, 171 172 ProductDescription = 173 model.ProductDescription, 174 175 Stock = 176 model.Stock 177 }; 178 179 180 _context.Products.Add(product); 181 182 _context.SaveChanges(); 183 184 185 CreateModification( 186 ModificationType.CREATE, 187 product.ProductId); 188 189 190 transaction.Commit(); 191 192 193 TempData["Success"] = 194 $"{release.Title} ({product.Format}) was created successfully."; 195 196 197 return RedirectToAction( 198 nameof(Products)); 199 } 200 catch 201 { 202 transaction.Rollback(); 203 204 throw; 205 } 206 } 207 208 209 // ========================================================= 210 // EDIT PRODUCT - GET 211 // ========================================================= 212 213 [HttpGet] 214 public IActionResult EditProduct(long id) 215 { 216 if (!IsProductManager()) 217 return Forbid(); 218 219 220 var product = _context.Products 221 .Include(p => p.Release) 222 .FirstOrDefault(p => 223 p.ProductId == id); 224 225 226 if (product == null) 227 return NotFound(); 228 229 230 var model = 231 new EditProductViewModel 232 { 233 ProductId = 234 product.ProductId, 235 236 ReleaseId = 237 product.ReleaseId, 238 239 ReleaseTitle = 240 product.Release.Title, 241 242 Format = 243 product.Format, 244 245 Price = 246 product.Price, 247 248 ProductDescription = 249 product.ProductDescription, 250 251 Stock = 252 product.Stock, 253 254 ModificationType = 255 ModificationType.UPDATE 256 }; 257 258 259 return View(model); 260 } 261 262 263 // ========================================================= 264 // EDIT PRODUCT - POST 265 // ========================================================= 266 267 [HttpPost] 268 [ValidateAntiForgeryToken] 269 public IActionResult EditProduct( 270 EditProductViewModel model) 271 { 272 if (!IsProductManager()) 273 return Forbid(); 274 275 276 var product = _context.Products 277 .Include(p => p.Release) 278 .FirstOrDefault(p => 279 p.ProductId == model.ProductId); 280 281 282 if (product == null) 283 return NotFound(); 284 285 286 // These values come from the database. 287 model.ReleaseId = 288 product.ReleaseId; 289 290 model.ReleaseTitle = 291 product.Release.Title; 292 293 model.Format = 294 product.Format; 295 296 297 // Display-only fields should not block 298 // validation of the submitted form. 299 ModelState.Remove( 300 nameof(model.ReleaseId)); 301 302 ModelState.Remove( 303 nameof(model.ReleaseTitle)); 304 305 ModelState.Remove( 306 nameof(model.Format)); 307 308 309 // ===================================================== 310 // DISCOUNT VALIDATION 311 // ===================================================== 312 313 if (model.ModificationType == 314 ModificationType.DISCOUNT) 315 { 316 if (!model.Discount.HasValue) 317 { 318 ModelState.AddModelError( 319 nameof(model.Discount), 320 "Please enter a discount percentage."); 321 } 322 else if ( 323 model.Discount.Value <= 0 || 324 model.Discount.Value >= 100) 325 { 326 ModelState.AddModelError( 327 nameof(model.Discount), 328 "Discount must be greater than 0 and less than 100."); 329 } 330 } 331 332 333 if (!ModelState.IsValid) 334 return View(model); 335 336 337 using var transaction = 338 _context.Database.BeginTransaction(); 339 340 341 try 342 { 343 // ================================================= 344 // APPLY DISCOUNT 345 // ================================================= 346 347 if (model.ModificationType == 348 ModificationType.DISCOUNT) 349 { 350 var discountPercentage = 351 model.Discount!.Value; 352 353 354 var oldPrice = 355 product.Price; 356 357 358 var discountAmount = 359 oldPrice * 360 (discountPercentage / 100m); 361 362 363 var newPrice = 364 oldPrice - discountAmount; 365 366 367 newPrice = 368 Math.Round( 369 newPrice, 370 2, 371 MidpointRounding.AwayFromZero); 372 373 374 // Product.Price becomes the 375 // actual current sale price. 376 product.Price = 377 newPrice; 378 379 380 _context.Products.Update( 381 product); 382 383 _context.SaveChanges(); 384 385 386 CreateModification( 387 ModificationType.DISCOUNT, 388 product.ProductId, 389 discountPercentage); 390 391 392 transaction.Commit(); 393 394 395 TempData["Success"] = 396 $"{discountPercentage:0.##}% discount applied. " + 397 $"Price changed from {oldPrice:C} to {newPrice:C}."; 398 399 400 return RedirectToAction( 401 nameof(Products)); 402 } 403 404 405 // ================================================= 406 // NORMAL UPDATE 407 // ================================================= 408 409 product.Price = 410 model.Price; 411 412 product.ProductDescription = 413 model.ProductDescription; 414 415 product.Stock = 416 model.Stock; 417 418 419 _context.Products.Update( 420 product); 421 422 _context.SaveChanges(); 423 424 425 CreateModification( 426 ModificationType.UPDATE, 427 product.ProductId); 428 429 430 transaction.Commit(); 431 432 433 TempData["Success"] = 434 $"{product.Release.Title} was updated successfully."; 435 436 437 return RedirectToAction( 438 nameof(Products)); 439 } 440 catch 441 { 442 transaction.Rollback(); 443 444 throw; 445 } 446 } 447 448 449 // ========================================================= 450 // CREATE RELEASE - GET 451 // ========================================================= 452 453 [HttpGet] 454 public IActionResult CreateRelease() 455 { 456 if (!IsAdmin()) 457 return Forbid(); 458 459 460 LoadArtists(); 461 462 463 var model = 464 new CreateReleaseViewModel 465 { 466 ReleaseDate = 467 DateTime.Today, 468 469 ReleaseType = 470 "ALBUM", 471 472 // Start an album with one empty track. 473 Tracks = 474 new List<CreateTrackViewModel> 475 { 476 new CreateTrackViewModel() 477 } 478 }; 479 480 481 return View(model); 482 } 483 484 485 // ========================================================= 486 // CREATE RELEASE - POST 487 // ========================================================= 488 489 [HttpPost] 490 [ValidateAntiForgeryToken] 491 public IActionResult CreateRelease( 492 CreateReleaseViewModel model) 493 { 494 if (!IsAdmin()) 495 return Forbid(); 496 497 498 // ===================================================== 499 // RELEASE TYPE VALIDATION 500 // ===================================================== 501 502 if (model.ReleaseType != "ALBUM" && 503 model.ReleaseType != "SINGLE") 504 { 505 ModelState.AddModelError( 506 nameof(model.ReleaseType), 507 "Please select Album or Single."); 508 } 509 510 511 // ===================================================== 512 // MAIN ARTIST VALIDATION 513 // ===================================================== 514 515 var mainArtistExists = 516 _context.Artists.Any(a => 517 a.ArtistId == 518 model.MainArtistId); 519 520 521 if (!mainArtistExists) 522 { 523 ModelState.AddModelError( 524 nameof(model.MainArtistId), 525 "Please select a valid main artist."); 526 } 527 528 529 // ===================================================== 530 // FEATURED ARTISTS 531 // ===================================================== 532 533 model.FeaturedArtistIds ??= 534 new List<long>(); 535 536 537 model.FeaturedArtistIds = 538 model.FeaturedArtistIds 539 .Distinct() 540 .ToList(); 541 542 543 // Do not allow the main artist 544 // to also be a featured artist. 545 model.FeaturedArtistIds.Remove( 546 model.MainArtistId); 547 548 549 // Validate that featured artists exist. 550 if (model.FeaturedArtistIds.Count > 0) 551 { 552 var validFeaturedArtistCount = 553 _context.Artists.Count(a => 554 model.FeaturedArtistIds 555 .Contains(a.ArtistId)); 556 557 558 if (validFeaturedArtistCount != 559 model.FeaturedArtistIds.Count) 560 { 561 ModelState.AddModelError( 562 nameof(model.FeaturedArtistIds), 563 "One or more featured artists are invalid."); 564 } 565 } 566 567 568 // ===================================================== 569 // SINGLE VALIDATION 570 // ===================================================== 571 572 if (model.ReleaseType == "SINGLE") 573 { 574 if (string.IsNullOrWhiteSpace( 575 model.SingleDuration)) 576 { 577 ModelState.AddModelError( 578 nameof(model.SingleDuration), 579 "Duration is required for a single."); 580 } 581 } 582 583 584 // ===================================================== 585 // ALBUM VALIDATION 586 // ===================================================== 587 588 if (model.ReleaseType == "ALBUM") 589 { 590 model.Tracks ??= 591 new List<CreateTrackViewModel>(); 592 593 594 // Remove completely empty rows. 595 model.Tracks = 596 model.Tracks 597 .Where(t => 598 !string.IsNullOrWhiteSpace( 599 t.SongName) || 600 !string.IsNullOrWhiteSpace( 601 t.SongDuration)) 602 .ToList(); 603 604 605 if (model.Tracks.Count == 0) 606 { 607 ModelState.AddModelError( 608 nameof(model.Tracks), 609 "An album must contain at least one track."); 610 } 611 612 613 for (var i = 0; 614 i < model.Tracks.Count; 615 i++) 616 { 617 var track = 618 model.Tracks[i]; 619 620 621 if (string.IsNullOrWhiteSpace( 622 track.SongName)) 623 { 624 ModelState.AddModelError( 625 $"Tracks[{i}].SongName", 626 "Track name is required."); 627 } 628 629 630 if (string.IsNullOrWhiteSpace( 631 track.SongDuration)) 632 { 633 ModelState.AddModelError( 634 $"Tracks[{i}].SongDuration", 635 "Track duration is required."); 636 } 637 638 639 track.ArtistIds ??= 640 new List<long>(); 641 642 643 track.ArtistIds = 644 track.ArtistIds 645 .Distinct() 646 .ToList(); 647 648 649 // Validate selected track artists. 650 if (track.ArtistIds.Count > 0) 651 { 652 var validTrackArtistCount = 653 _context.Artists.Count(a => 654 track.ArtistIds.Contains( 655 a.ArtistId)); 656 657 658 if (validTrackArtistCount != 659 track.ArtistIds.Count) 660 { 661 ModelState.AddModelError( 662 $"Tracks[{i}].ArtistIds", 663 "One or more selected track artists are invalid."); 664 } 665 } 666 } 667 } 668 669 670 if (!ModelState.IsValid) 671 { 672 LoadArtists(); 673 674 return View(model); 675 } 676 677 678 using var transaction = 679 _context.Database.BeginTransaction(); 680 681 682 try 683 { 684 // ================================================= 685 // CREATE RELEASE 686 // ================================================= 687 688 var release = 689 new Release 690 { 691 ReleaseId = 692 GetNextReleaseId(), 693 694 Title = 695 model.Title.Trim(), 696 697 RecordLabel = 698 string.IsNullOrWhiteSpace( 699 model.RecordLabel) 700 ? null 701 : model.RecordLabel.Trim(), 702 703 Genre = 704 model.Genre.Trim(), 705 706 ReleaseDate = 707 model.ReleaseDate, 708 709 CoverPhoto = 710 model.CoverPhoto.Trim() 711 }; 712 713 714 _context.Releases.Add( 715 release); 716 717 _context.SaveChanges(); 718 719 720 // ================================================= 721 // MAIN RELEASE ARTIST 722 // ================================================= 723 724 var mainReleaseArtist = 725 new ReleaseArtist 726 { 727 ReleaseId = 728 release.ReleaseId, 729 730 ArtistId = 731 model.MainArtistId, 732 733 ReleaseOrdinal = 1, 734 735 Type = 736 ArtistReleaseType.MAIN 737 }; 738 739 740 _context.ReleaseArtists.Add( 741 mainReleaseArtist); 742 743 744 // ================================================= 745 // FEATURED RELEASE ARTISTS 746 // ================================================= 747 748 long releaseOrdinal = 2; 749 750 751 foreach (var artistId 752 in model.FeaturedArtistIds) 753 { 754 var featuredArtist = 755 new ReleaseArtist 756 { 757 ReleaseId = 758 release.ReleaseId, 759 760 ArtistId = 761 artistId, 762 763 ReleaseOrdinal = 764 releaseOrdinal++, 765 766 Type = 767 ArtistReleaseType.FEATURE 768 }; 769 770 771 _context.ReleaseArtists.Add( 772 featuredArtist); 773 } 774 775 776 _context.SaveChanges(); 777 778 779 // ================================================= 780 // ALBUM 781 // ================================================= 782 783 if (model.ReleaseType == "ALBUM") 784 { 785 var album = 786 new Album 787 { 788 ReleaseId = 789 release.ReleaseId 790 }; 791 792 793 _context.Albums.Add( 794 album); 795 796 _context.SaveChanges(); 797 798 799 // ============================================= 800 // TRACKS 801 // ============================================= 802 803 foreach (var trackModel 804 in model.Tracks) 805 { 806 var song = 807 new Song 808 { 809 SongId = 810 GetNextSongId(), 811 812 SongName = 813 trackModel 814 .SongName 815 .Trim(), 816 817 SongDuration = 818 trackModel 819 .SongDuration 820 .Trim() 821 }; 822 823 824 _context.Songs.Add( 825 song); 826 827 _context.SaveChanges(); 828 829 830 // ========================================= 831 // LINK SONG TO ALBUM 832 // ========================================= 833 834 var albumSong = 835 new AlbumSong 836 { 837 AlbumId = 838 release.ReleaseId, 839 840 SongId = 841 song.SongId 842 }; 843 844 845 _context.AlbumSongs.Add( 846 albumSong); 847 848 849 // ========================================= 850 // SONG ARTISTS 851 // ========================================= 852 853 var trackArtistIds = 854 trackModel.ArtistIds 855 .Distinct() 856 .ToList(); 857 858 859 // If no track artists were selected, 860 // automatically use the release's 861 // main artist. 862 if (trackArtistIds.Count == 0) 863 { 864 trackArtistIds.Add( 865 model.MainArtistId); 866 } 867 868 869 long songOrdinal = 1; 870 871 872 foreach (var artistId 873 in trackArtistIds) 874 { 875 var songArtist = 876 new SongArtist 877 { 878 SongId = 879 song.SongId, 880 881 ArtistId = 882 artistId, 883 884 SongOrdinal = 885 songOrdinal++ 886 }; 887 888 889 _context.SongArtists.Add( 890 songArtist); 891 } 892 893 894 _context.SaveChanges(); 895 } 896 } 897 898 899 // ================================================= 900 // SINGLE 901 // ================================================= 902 903 else 904 { 905 var single = 906 new SingleRelease 907 { 908 ReleaseId = 909 release.ReleaseId, 910 911 Duration = 912 model.SingleDuration! 913 .Trim() 914 }; 915 916 917 _context.SingleReleases.Add( 918 single); 919 920 _context.SaveChanges(); 921 } 922 923 924 transaction.Commit(); 925 926 927 TempData["Success"] = 928 $"{release.Title} was created successfully."; 929 930 931 return RedirectToAction( 932 nameof(Index)); 933 } 934 catch 935 { 936 transaction.Rollback(); 937 938 throw; 939 } 940 } 941 942 943 // ========================================================= 944 // AUTHORIZATION HELPERS 945 // ========================================================= 147 946 148 947 private bool IsAdmin() 149 948 { 150 949 return HttpContext.Session 151 .GetString("Role") == "Admin"; 950 .GetString("Role") == 951 "Admin"; 152 952 } 153 953 … … 155 955 private bool IsProductManager() 156 956 { 157 var role = HttpContext.Session 158 .GetString("Role"); 159 160 var adminType = HttpContext.Session 161 .GetString("AdminType"); 957 var role = 958 HttpContext.Session 959 .GetString("Role"); 960 961 var adminType = 962 HttpContext.Session 963 .GetString("AdminType"); 964 162 965 163 966 return role == "Admin" && 164 (adminType == "PRODUCT_MANAGER" || 165 adminType == "SUPER_ADMIN"); 166 } 167 967 ( 968 adminType == "PRODUCT_MANAGER" || 969 adminType == "SUPER_ADMIN" 970 ); 971 } 972 973 974 // ========================================================= 975 // CURRENT ADMIN ID 976 // ========================================================= 168 977 169 978 private long? GetCurrentUserId() 170 979 { 171 var value = HttpContext.Session 172 .GetString("UserId"); 173 174 if (long.TryParse(value, out var userId)) 175 return userId; 176 177 return null; 178 } 179 980 var userId = 981 HttpContext.Session 982 .GetInt32("UserId"); 983 984 985 if (!userId.HasValue) 986 return null; 987 988 989 return userId.Value; 990 } 991 992 993 // ========================================================= 994 // LOAD RELEASES 995 // ========================================================= 996 997 private void LoadReleases() 998 { 999 ViewBag.Releases = 1000 _context.Releases 1001 .OrderBy(x => 1002 x.Title) 1003 .ToList(); 1004 } 1005 1006 1007 // ========================================================= 1008 // LOAD ARTISTS 1009 // ========================================================= 1010 1011 private void LoadArtists() 1012 { 1013 ViewBag.Artists = 1014 _context.Artists 1015 .OrderBy(a => 1016 a.ArtistName) 1017 .ToList(); 1018 } 1019 1020 1021 // ========================================================= 1022 // NEXT PRODUCT ID 1023 // ========================================================= 180 1024 181 1025 private long GetNextProductId() 182 1026 { 183 var maxId = _context.Products 184 .Select(x => (long?)x.ProductId) 185 .Max(); 1027 var maxId = 1028 _context.Products 1029 .Select(x => 1030 (long?)x.ProductId) 1031 .Max(); 1032 186 1033 187 1034 return (maxId ?? 0) + 1; … … 189 1036 190 1037 191 private void LoadReleases() 192 { 193 ViewBag.Releases = _context.Releases 194 .OrderBy(x => x.Title) 195 .ToList(); 196 } 197 1038 // ========================================================= 1039 // NEXT RELEASE ID 1040 // ========================================================= 1041 1042 private long GetNextReleaseId() 1043 { 1044 var maxId = 1045 _context.Releases 1046 .Select(x => 1047 (long?)x.ReleaseId) 1048 .Max(); 1049 1050 1051 return (maxId ?? 0) + 1; 1052 } 1053 1054 1055 // ========================================================= 1056 // NEXT SONG ID 1057 // ========================================================= 1058 1059 private long GetNextSongId() 1060 { 1061 var maxId = 1062 _context.Songs 1063 .Select(x => 1064 (long?)x.SongId) 1065 .Max(); 1066 1067 1068 return (maxId ?? 0) + 1; 1069 } 1070 1071 1072 // ========================================================= 1073 // NEXT MODIFICATION ID 1074 // ========================================================= 1075 1076 private long GetNextModificationId() 1077 { 1078 var maxId = 1079 _context.Modifications 1080 .Select(x => 1081 (long?)x.ModificationId) 1082 .Max(); 1083 1084 1085 return (maxId ?? 0) + 1; 1086 } 1087 1088 1089 // ========================================================= 1090 // CREATE MODIFICATION RECORD 1091 // ========================================================= 198 1092 199 1093 private void CreateModification( … … 202 1096 decimal? discount = null) 203 1097 { 204 var adminId = GetCurrentUserId(); 1098 var adminId = 1099 GetCurrentUserId(); 1100 205 1101 206 1102 if (adminId == null) 207 return; 208 209 210 var modification = new Modification 211 { 212 ModificationId = 213 GetNextModificationId(), 214 215 AdminId = adminId.Value, 216 217 DateModified = DateTime.Today, 218 219 TypeOfModification = type, 220 221 Discount = discount 222 }; 223 224 225 _context.Modifications.Add(modification); 1103 { 1104 throw new InvalidOperationException( 1105 "The current admin could not be identified."); 1106 } 1107 1108 1109 var modification = 1110 new Modification 1111 { 1112 ModificationId = 1113 GetNextModificationId(), 1114 1115 AdminId = 1116 adminId.Value, 1117 1118 DateModified = 1119 DateTime.Today, 1120 1121 TypeOfModification = 1122 type, 1123 1124 Discount = 1125 discount 1126 }; 1127 1128 1129 _context.Modifications.Add( 1130 modification); 226 1131 227 1132 _context.SaveChanges(); … … 234 1139 modification.ModificationId, 235 1140 236 ProductId = productId 1141 ProductId = 1142 productId 237 1143 }; 238 1144 … … 243 1149 _context.SaveChanges(); 244 1150 } 245 246 247 private long GetNextModificationId()248 {249 var maxId = _context.Modifications250 .Select(x => (long?)x.ModificationId)251 .Max();252 253 return (maxId ?? 0) + 1;254 }255 // ==========================================256 // UC009257 // MODIFY PRODUCT - GET258 // ==========================================259 260 [HttpGet]261 public IActionResult EditProduct(long id)262 {263 if (!IsProductManager())264 return Forbid();265 266 var product = _context.Products267 .Include(p => p.Release)268 .FirstOrDefault(p => p.ProductId == id);269 270 if (product == null)271 return NotFound();272 273 var model = new EditProductViewModel274 {275 ProductId = product.ProductId,276 ReleaseId = product.ReleaseId,277 ReleaseTitle = product.Release.Title,278 Format = product.Format,279 Price = product.Price,280 ProductDescription = product.ProductDescription,281 Stock = product.Stock282 };283 284 return View(model);285 }286 // ==========================================287 // UC009288 // MODIFY PRODUCT - POST289 // ==========================================290 291 [HttpPost]292 [ValidateAntiForgeryToken]293 public IActionResult EditProduct(294 EditProductViewModel model)295 {296 if (!IsProductManager())297 return Forbid();298 299 var product = _context.Products300 .Include(p => p.Release)301 .FirstOrDefault(p =>302 p.ProductId == model.ProductId);303 304 if (product == null)305 return NotFound();306 307 if (!ModelState.IsValid)308 {309 model.ReleaseTitle = product.Release.Title;310 model.ReleaseId = product.ReleaseId;311 model.Format = product.Format;312 313 return View(model);314 }315 316 317 // ==========================================318 // DISCOUNT319 // ==========================================320 321 if (model.ModificationType ==322 ModificationType.DISCOUNT)323 {324 if (!model.Discount.HasValue)325 {326 ModelState.AddModelError(327 "Discount",328 "Please enter a discount percentage.");329 330 model.ReleaseTitle = product.Release.Title;331 model.ReleaseId = product.ReleaseId;332 model.Format = product.Format;333 334 return View(model);335 }336 337 product.Price =338 product.Price *339 (1 - model.Discount.Value / 100m);340 }341 else342 {343 // ==========================================344 // NORMAL UPDATE345 // ==========================================346 347 product.Price = model.Price;348 349 product.ProductDescription =350 model.ProductDescription;351 352 product.Stock = model.Stock;353 }354 355 356 _context.SaveChanges();357 358 359 // ==========================================360 // RECORD MODIFICATION361 // ==========================================362 363 CreateModification(364 model.ModificationType,365 product.ProductId,366 model.Discount);367 368 369 TempData["Success"] =370 "Product modified successfully.";371 372 return RedirectToAction(nameof(Index));373 1151 } 374 [HttpGet]375 public IActionResult Products()376 {377 if (!IsProductManager())378 return Forbid();379 380 var products = _context.Products381 .Include(p => p.Release)382 .OrderBy(p => p.Release.Title)383 .ThenBy(p => p.Format)384 .ToList();385 386 return View(products);387 }388 } -
KernelRecordsMVC.Web/Controllers/HomeController.cs
r08aefc6 rfa0fbaf 1 1 using System.Diagnostics; 2 using KernelRecordsMVC.Models; 2 3 using Microsoft.AspNetCore.Mvc; 3 using KernelRecordsMVC.Models;4 using Microsoft.Extensions.Logging;5 4 6 namespace KernelRecordsMVC. Controllers;5 namespace KernelRecordsMVC.Web.Controllers; 7 6 8 7 public class HomeController : Controller -
KernelRecordsMVC.Web/Controllers/OrderController.cs
r08aefc6 rfa0fbaf 1 using Microsoft.AspNetCore.Http;2 3 namespace KernelRecordsMVC.Controllers;4 1 using KernelRecordsMVC.Application.ViewModels; 2 using KernelRecordsMVC.Domain.Enums; 3 using KernelRecordsMVC.Infrastructure.Data; 4 using KernelRecordsMVC.Models; 5 5 using Microsoft.AspNetCore.Mvc; 6 6 using Microsoft.EntityFrameworkCore; 7 using KernelRecordsMVC.Data; 8 using KernelRecordsMVC.Models; 9 using KernelRecordsMVC.Domain.Enums; 10 using KernelRecordsMVC.Application.ViewModels; 7 8 namespace KernelRecordsMVC.Web.Controllers; 11 9 12 10 public class OrderController : Controller … … 67 65 order = new Order 68 66 { 69 OrderId = GetNextOrderId(),70 67 UserId = userId.Value, 71 72 // The actual payment method will be73 // selected during checkout.74 68 PaymentMethod = PaymentMethodType.CARD, 75 76 69 PurchaseDate = DateTime.Today, 77 78 70 PointsEarned = 0, 79 80 71 PointsUsed = null, 81 82 72 Status = OrderStatusType.PENDING 83 73 }; … … 113 103 else 114 104 { 105 var discount = _context.ModificationProducts 106 .Include(mp => mp.Modification) 107 .Where(mp => 108 mp.ProductId == product.ProductId && 109 mp.Modification.TypeOfModification == 110 ModificationType.DISCOUNT && 111 mp.Modification.Discount.HasValue && 112 mp.Modification.Discount.Value > 0) 113 .Select(mp => mp.Modification) 114 .OrderByDescending(m => m.DateModified) 115 .FirstOrDefault(); 116 117 var priceAtPurchase = product.Price; 118 119 if (discount != null) 120 { 121 priceAtPurchase = 122 product.Price - 123 (product.Price * discount.Discount!.Value / 100m); 124 125 priceAtPurchase = Math.Round( 126 priceAtPurchase, 127 2); 128 } 129 115 130 var orderProduct = new OrderProduct 116 131 { … … 186 201 187 202 Stock = 188 op.Product.Stock 203 op.Product.Stock, 204 205 CoverPhoto = op.Product.Release.CoverPhoto 189 206 }) 190 207 .ToList(); … … 294 311 private long? GetUserId() 295 312 { 296 var value = 297 HttpContext.Session.GetString("UserId"); 298 299 if (long.TryParse(value, out var userId)) 300 return userId; 313 var userId = HttpContext.Session.GetInt32("UserId"); 314 315 if (userId.HasValue) 316 return userId.Value; 301 317 302 318 return null; 303 319 } 304 305 306 // ==========================================307 // ORDER ID308 // ==========================================309 310 private long GetNextOrderId()311 {312 var maxId = _context.Orders313 .Select(x => (long?)x.OrderId)314 .Max();315 316 return (maxId ?? 0) + 1;317 }318 320 } -
KernelRecordsMVC.Web/Controllers/ReleaseController.cs
r08aefc6 rfa0fbaf 1 using Microsoft.AspNetCore.Mvc; 1 using KernelRecordsMVC.Domain.Enums; 2 using KernelRecordsMVC.Infrastructure.Data; 3 using Microsoft.AspNetCore.Mvc; 2 4 using Microsoft.EntityFrameworkCore; 3 using KernelRecordsMVC.Data;4 5 5 namespace KernelRecordsMVC. Controllers;6 namespace KernelRecordsMVC.Web.Controllers; 6 7 7 8 public class ReleaseController : Controller … … 14 15 } 15 16 16 // UC003 - Browse Releases 17 public IActionResult Index(string? search, string? genre) 17 // ========================================================= 18 // BROWSE RELEASES 19 // ========================================================= 20 21 public IActionResult Index( 22 string? search, 23 string? genre, 24 string? format, 25 string? type, 26 string? sort) 18 27 { 19 28 var query = _context.Releases 20 29 .Include(r => r.Products) 21 30 .Include(r => r.ReleaseArtists) 22 .ThenInclude(ra => ra.Artist) 31 .ThenInclude(ra => ra.Artist) 32 .Include(r => r.Album) 33 .Include(r => r.SingleRelease) 23 34 .AsQueryable(); 24 35 36 // SEARCH 25 37 if (!string.IsNullOrWhiteSpace(search)) 26 38 { 39 search = search.Trim(); 40 27 41 query = query.Where(r => 28 r.Title.ToLower().Contains(search.ToLower())); 42 r.Title.ToLower().Contains(search.ToLower()) || 43 r.Genre.ToLower().Contains(search.ToLower()) || 44 r.ReleaseArtists.Any(ra => 45 ra.Artist.ArtistName.ToLower() 46 .Contains(search.ToLower()))); 29 47 } 30 48 49 // GENRE 31 50 if (!string.IsNullOrWhiteSpace(genre)) 32 51 { … … 35 54 } 36 55 37 var releases = query 38 .OrderBy(r => r.Title) 39 .ToList(); 56 // FORMAT 57 if (!string.IsNullOrWhiteSpace(format)) 58 { 59 query = query.Where(r => 60 r.Products.Any(p => 61 p.Format.ToString() == format)); 62 } 63 64 // TYPE 65 if (!string.IsNullOrWhiteSpace(type)) 66 { 67 if (type.Equals("Album", StringComparison.OrdinalIgnoreCase)) 68 { 69 query = query.Where(r => r.Album != null); 70 } 71 else if (type.Equals("Single", StringComparison.OrdinalIgnoreCase)) 72 { 73 query = query.Where(r => r.SingleRelease != null); 74 } 75 } 76 77 // SORT 78 query = sort switch 79 { 80 "titleDesc" => 81 query.OrderByDescending(r => r.Title), 82 83 "newest" => 84 query.OrderByDescending(r => r.ReleaseDate), 85 86 "oldest" => 87 query.OrderBy(r => r.ReleaseDate), 88 89 "format" => 90 query.OrderBy(r => 91 r.Products 92 .Select(p => p.Format.ToString()) 93 .FirstOrDefault()), 94 95 "priceAsc" => 96 query.OrderBy(r => 97 r.Products 98 .Select(p => (decimal?)p.Price) 99 .Min()), 100 101 "priceDesc" => 102 query.OrderByDescending(r => 103 r.Products 104 .Select(p => (decimal?)p.Price) 105 .Max()), 106 107 _ => 108 query.OrderBy(r => r.Title) 109 }; 110 111 var releases = query.ToList(); 40 112 41 113 ViewBag.Search = search; 42 114 ViewBag.Genre = genre; 115 ViewBag.Format = format; 116 ViewBag.Type = type; 117 ViewBag.Sort = sort; 43 118 44 119 ViewBag.Genres = _context.Releases … … 48 123 .ToList(); 49 124 125 ViewBag.Formats = Enum 126 .GetValues<ProductFormat>() 127 .Select(x => x.ToString()) 128 .ToList(); 129 50 130 return View(releases); 51 131 } 52 132 53 133 54 // Release details 134 // ========================================================= 135 // DETAILS 136 // ========================================================= 137 55 138 public IActionResult Details(long id) 56 139 { 57 140 var release = _context.Releases 58 141 .Include(r => r.Products) 142 59 143 .Include(r => r.ReleaseArtists) 60 .ThenInclude(ra => ra.Artist) 144 .ThenInclude(ra => ra.Artist) 145 61 146 .Include(r => r.Album) 62 .ThenInclude(a => a!.AlbumSongs) 63 .ThenInclude(x => x.Song) 147 .ThenInclude(a => a!.AlbumSongs) 148 .ThenInclude(x => x.Song) 149 .ThenInclude(s => s.SongArtists) 150 .ThenInclude(sa => sa.Artist) 151 64 152 .Include(r => r.SingleRelease) 65 .ThenInclude(s => s!.SingleFeatures) 66 .ThenInclude(x => x.Song) 153 67 154 .FirstOrDefault(r => r.ReleaseId == id); 68 155 -
KernelRecordsMVC.Web/KernelRecordsMVC.Web.csproj
r08aefc6 rfa0fbaf 5 5 <ImplicitUsings>enable</ImplicitUsings> 6 6 <Nullable>enable</Nullable> 7 </PropertyGroup> 7 <UserSecretsId>7996030f-d5d7-4616-a943-1e4992329169</UserSecretsId> 8 </PropertyGroup> 8 9 9 10 <ItemGroup> … … 37 38 <ItemGroup> 38 39 <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> 40 <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" /> 39 41 </ItemGroup> 40 42 -
KernelRecordsMVC.Web/Program.cs
r08aefc6 rfa0fbaf 1 1 using Microsoft.EntityFrameworkCore; 2 using KernelRecordsMVC.Data; 2 using Npgsql; 3 using Npgsql.NameTranslation; 4 using KernelRecordsMVC.Domain.Enums; 5 using KernelRecordsMVC.Infrastructure.Data; 3 6 4 7 var builder = WebApplication.CreateBuilder(args); 5 8 9 // ========================================== 10 // AUTHENTICATION 11 // ========================================== 12 13 builder.Services 14 .AddAuthentication("Cookies") 15 .AddCookie("Cookies", options => 16 { 17 options.LoginPath = "/Account/Login"; 18 options.AccessDeniedPath = "/Account/Login"; 19 options.ExpireTimeSpan = TimeSpan.FromHours(8); 20 options.SlidingExpiration = true; 21 }); 22 23 builder.Services.AddAuthorization(); 24 6 25 builder.Services.AddControllersWithViews(); 7 26 27 // ========================================== 28 // DATABASE 29 // ========================================== 30 31 var dataSourceBuilder = new NpgsqlDataSourceBuilder( 32 builder.Configuration.GetConnectionString("KernelRecords")); 33 34 var nameTranslator = new NpgsqlNullNameTranslator(); 35 36 dataSourceBuilder.MapEnum<AdminType>( 37 "project.admin_type", 38 nameTranslator); 39 40 dataSourceBuilder.MapEnum<ProductFormat>( 41 "project.product_format", 42 nameTranslator); 43 44 dataSourceBuilder.MapEnum<PaymentMethodType>( 45 "project.payment_method_type", 46 nameTranslator); 47 48 dataSourceBuilder.MapEnum<OrderStatusType>( 49 "project.order_status_type", 50 nameTranslator); 51 52 dataSourceBuilder.MapEnum<ModificationType>( 53 "project.modification_type", 54 nameTranslator); 55 56 dataSourceBuilder.MapEnum<ArtistReleaseType>( 57 "project.artist_release_type", 58 nameTranslator); 59 60 var dataSource = dataSourceBuilder.Build(); 61 8 62 builder.Services.AddDbContext<KernelRecordsContext>(options => 9 options.UseNpgsql( 10 builder.Configuration.GetConnectionString("KernelRecords"))); 63 options.UseNpgsql(dataSource)); 11 64 12 builder.Services.AddSession(); 65 // ========================================== 66 // SESSION 67 // ========================================== 68 69 builder.Services.AddSession(options => 70 { 71 options.IdleTimeout = TimeSpan.FromHours(2); 72 options.Cookie.HttpOnly = true; 73 options.Cookie.IsEssential = true; 74 }); 13 75 14 76 var app = builder.Build(); 77 78 // ========================================== 79 // MIDDLEWARE 80 // ========================================== 15 81 16 82 if (!app.Environment.IsDevelopment()) … … 21 87 22 88 app.UseHttpsRedirection(); 89 23 90 app.UseStaticFiles(); 24 91 … … 26 93 27 94 app.UseSession(); 95 96 app.UseAuthentication(); 28 97 29 98 app.UseAuthorization(); -
KernelRecordsMVC.Web/Views/Admin/CreateProduct.cshtml
r08aefc6 rfa0fbaf 1 1 @model KernelRecordsMVC.Application.ViewModels.CreateProductViewModel 2 2 3 <h1>New Product</h1> 4 5 <p class="text-muted"> 6 Add a physical product format for an existing release. 7 </p> 8 9 <form asp-action="CreateProduct" 10 method="post"> 11 12 @Html.AntiForgeryToken() 13 14 <div asp-validation-summary="ModelOnly" 15 class="text-danger mb-3"> 3 @{ 4 ViewData["Title"] = "New Product"; 5 } 6 7 <div class="admin-page admin-form-page"> 8 9 <!-- HEADER --> 10 11 <div class="admin-header"> 12 13 <div> 14 15 <a asp-action="Products" 16 class="admin-back-link"> 17 18 ← Products 19 20 </a> 21 22 <span class="admin-eyebrow"> 23 INVENTORY 24 </span> 25 26 <h1>New Product</h1> 27 28 <p> 29 Add a new physical format to an existing release. 30 </p> 31 32 </div> 33 16 34 </div> 17 35 18 36 19 <!-- RELEASE --> 20 21 <div class="mb-3"> 22 23 <label asp-for="ReleaseId" 24 class="form-label"> 25 </label> 26 27 <select asp-for="ReleaseId" 28 class="form-select"> 29 30 <option value=""> 31 -- Select Release -- 32 </option> 33 34 @foreach (var release in ViewBag.Releases) 35 { 36 <option value="@release.ReleaseId"> 37 38 @release.Title 39 (@release.ReleaseDate.ToString("yyyy")) 40 41 </option> 42 } 43 44 </select> 45 46 <span asp-validation-for="ReleaseId" 47 class="text-danger"> 48 </span> 37 <div class="admin-form-layout"> 38 39 40 <!-- ================================================= 41 FORM 42 ================================================= --> 43 44 <section class="admin-form-card"> 45 46 <div class="admin-form-card-header"> 47 48 <span class="admin-eyebrow"> 49 PRODUCT INFORMATION 50 </span> 51 52 <h2>Product Details</h2> 53 54 </div> 55 56 57 <form asp-action="CreateProduct" 58 method="post"> 59 60 @Html.AntiForgeryToken() 61 62 63 <div asp-validation-summary="ModelOnly" 64 class="admin-validation"> 65 </div> 66 67 68 <!-- RELEASE --> 69 70 <div class="admin-form-field"> 71 72 <label asp-for="ReleaseId"> 73 Release 74 </label> 75 76 <select asp-for="ReleaseId"> 77 78 <option value=""> 79 Select a release 80 </option> 81 82 @foreach (var release in ViewBag.Releases) 83 { 84 <option value="@release.ReleaseId"> 85 86 @release.Title 87 (@release.ReleaseDate.ToString("yyyy")) 88 89 </option> 90 } 91 92 </select> 93 94 <span asp-validation-for="ReleaseId" 95 class="admin-field-error"> 96 </span> 97 98 <small> 99 Select the release this physical 100 product belongs to. 101 </small> 102 103 </div> 104 105 106 <div class="admin-form-row"> 107 108 109 <!-- FORMAT --> 110 111 <div class="admin-form-field"> 112 113 <label asp-for="Format"> 114 Format 115 </label> 116 117 <select asp-for="Format" 118 asp-items="Html.GetEnumSelectList<KernelRecordsMVC.Domain.Enums.ProductFormat>()"> 119 120 <option value=""> 121 Select format 122 </option> 123 124 </select> 125 126 <span asp-validation-for="Format" 127 class="admin-field-error"> 128 </span> 129 130 </div> 131 132 133 <!-- PRICE --> 134 135 <div class="admin-form-field"> 136 137 <label asp-for="Price"> 138 Price 139 </label> 140 141 <div class="admin-input-prefix"> 142 143 <span>$</span> 144 145 <input asp-for="Price" 146 type="number" 147 min="0" 148 step="0.01" 149 placeholder="0.00" /> 150 151 </div> 152 153 <span asp-validation-for="Price" 154 class="admin-field-error"> 155 </span> 156 157 </div> 158 159 </div> 160 161 162 <!-- STOCK --> 163 164 <div class="admin-form-field"> 165 166 <label asp-for="Stock"> 167 Initial Stock 168 </label> 169 170 <input asp-for="Stock" 171 type="number" 172 min="0" 173 placeholder="0" /> 174 175 <span asp-validation-for="Stock" 176 class="admin-field-error"> 177 </span> 178 179 </div> 180 181 182 <!-- DESCRIPTION --> 183 184 <div class="admin-form-field"> 185 186 <label asp-for="ProductDescription"> 187 Product Description 188 </label> 189 190 <textarea asp-for="ProductDescription" 191 rows="5" 192 placeholder="Describe this physical edition..."></textarea> 193 194 <span asp-validation-for="ProductDescription" 195 class="admin-field-error"> 196 </span> 197 198 </div> 199 200 201 <!-- ACTIONS --> 202 203 <div class="admin-form-actions"> 204 205 <button type="submit" 206 class="admin-button primary"> 207 208 Create Product 209 210 </button> 211 212 213 <a asp-action="Products" 214 class="admin-button secondary"> 215 216 Cancel 217 218 </a> 219 220 </div> 221 222 </form> 223 224 </section> 225 226 227 <!-- ================================================= 228 SIDE INFO 229 ================================================= --> 230 231 <aside class="admin-help-card"> 232 233 <span class="admin-eyebrow"> 234 PRODUCT CREATION 235 </span> 236 237 <h3>Before you create</h3> 238 239 <div class="admin-help-item"> 240 241 <strong>01</strong> 242 243 <p> 244 A release can only have one product 245 for each physical format. 246 </p> 247 248 </div> 249 250 251 <div class="admin-help-item"> 252 253 <strong>02</strong> 254 255 <p> 256 Stock can be updated later from 257 Product Management. 258 </p> 259 260 </div> 261 262 263 <div class="admin-help-item"> 264 265 <strong>03</strong> 266 267 <p> 268 Creating the product will automatically 269 create a modification record. 270 </p> 271 272 </div> 273 274 </aside> 49 275 50 276 </div> 51 277 52 53 <!-- FORMAT --> 54 55 <div class="mb-3"> 56 57 <label asp-for="Format" 58 class="form-label"> 59 </label> 60 61 <select asp-for="Format" 62 asp-items="Html.GetEnumSelectList<KernelRecordsMVC.Domain.Enums.ProductFormat>()" 63 class="form-select"> 64 65 <option value=""> 66 -- Select Format -- 67 </option> 68 69 </select> 70 71 <span asp-validation-for="Format" 72 class="text-danger"> 73 </span> 74 75 </div> 76 77 78 <!-- PRICE --> 79 80 <div class="mb-3"> 81 82 <label asp-for="Price" 83 class="form-label"> 84 </label> 85 86 <div class="input-group"> 87 88 <span class="input-group-text"> 89 $ 90 </span> 91 92 <input asp-for="Price" 93 class="form-control" 94 step="0.01"/> 95 96 </div> 97 98 <span asp-validation-for="Price" 99 class="text-danger"> 100 </span> 101 102 </div> 103 104 105 <!-- DESCRIPTION --> 106 107 <div class="mb-3"> 108 109 <label asp-for="ProductDescription" 110 class="form-label"> 111 </label> 112 113 <textarea asp-for="ProductDescription" 114 class="form-control" 115 rows="4"> 116 </textarea> 117 118 <span asp-validation-for="ProductDescription" 119 class="text-danger"> 120 </span> 121 122 </div> 123 124 125 <!-- STOCK --> 126 127 <div class="mb-3"> 128 129 <label asp-for="Stock" 130 class="form-label"> 131 </label> 132 133 <input asp-for="Stock" 134 type="number" 135 min="0" 136 class="form-control"/> 137 138 <span asp-validation-for="Stock" 139 class="text-danger"> 140 </span> 141 142 </div> 143 144 145 <div class="d-flex gap-2"> 146 147 <button type="submit" 148 class="btn btn-success"> 149 150 Create Product 151 152 </button> 153 154 <a asp-action="Index" 155 class="btn btn-secondary"> 156 157 Cancel 158 159 </a> 160 161 </div> 162 163 </form> 278 </div> 279 164 280 165 281 @section Scripts 166 282 { 167 <partial name="_ValidationScriptsPartial" />283 <partial name="_ValidationScriptsPartial" /> 168 284 } -
KernelRecordsMVC.Web/Views/Admin/EditProduct.cshtml
r08aefc6 rfa0fbaf 1 1 @model KernelRecordsMVC.Application.ViewModels.EditProductViewModel 2 2 3 <h1>Modify Product</h1> 4 5 <div class="card mb-4"> 6 7 <div class="card-body"> 8 9 <h4>@Model.ReleaseTitle</h4> 10 11 <p class="text-muted mb-0"> 12 13 Format: 14 15 <strong> 16 @Model.Format 17 </strong> 18 19 </p> 3 @{ 4 ViewData["Title"] = "Modify Product"; 5 } 6 7 <div class="admin-page admin-form-page"> 8 9 <!-- HEADER --> 10 11 <div class="admin-header"> 12 13 <div> 14 15 <a asp-action="Products" 16 class="admin-back-link"> 17 18 ← Products 19 20 </a> 21 22 <span class="admin-eyebrow"> 23 PRODUCT MANAGEMENT 24 </span> 25 26 <h1>Modify Product</h1> 27 28 <p> 29 Update inventory information or apply a discount. 30 </p> 31 32 </div> 20 33 21 34 </div> 22 35 36 37 <!-- PRODUCT SUMMARY --> 38 39 <div class="admin-product-summary"> 40 41 <div> 42 43 <span class="admin-eyebrow"> 44 SELECTED PRODUCT 45 </span> 46 47 <h2> 48 @Model.ReleaseTitle 49 </h2> 50 51 </div> 52 53 <div class="admin-product-summary-meta"> 54 55 <div> 56 57 <span>FORMAT</span> 58 59 <strong> 60 @Model.Format 61 </strong> 62 63 </div> 64 65 <div> 66 67 <span>CURRENT PRICE</span> 68 69 <strong> 70 @Model.Price.ToString("C") 71 </strong> 72 73 </div> 74 75 <div> 76 77 <span>STOCK</span> 78 79 <strong> 80 @Model.Stock 81 </strong> 82 83 </div> 84 85 </div> 86 87 </div> 88 89 90 <section class="admin-form-card admin-edit-card"> 91 92 <form asp-action="EditProduct" 93 method="post" 94 id="productEditForm"> 95 96 @Html.AntiForgeryToken() 97 98 <input type="hidden" 99 asp-for="ProductId" /> 100 101 102 <div asp-validation-summary="ModelOnly" 103 class="admin-validation"> 104 </div> 105 106 107 <!-- ================================================= 108 MODIFICATION TYPE 109 ================================================= --> 110 111 <div class="admin-form-field"> 112 113 <label asp-for="ModificationType"> 114 What do you want to do? 115 </label> 116 117 <div class="admin-modification-options"> 118 119 <label class="admin-modification-option"> 120 121 <input type="radio" 122 asp-for="ModificationType" 123 value="UPDATE" 124 id="updateOption" /> 125 126 <span> 127 128 <strong> 129 Update Product 130 </strong> 131 132 <small> 133 Change price, description or stock. 134 </small> 135 136 </span> 137 138 </label> 139 140 141 <label class="admin-modification-option"> 142 143 <input type="radio" 144 asp-for="ModificationType" 145 value="DISCOUNT" 146 id="discountOption" /> 147 148 <span> 149 150 <strong> 151 Apply Discount 152 </strong> 153 154 <small> 155 Reduce the current selling price. 156 </small> 157 158 </span> 159 160 </label> 161 162 </div> 163 164 <span asp-validation-for="ModificationType" 165 class="admin-field-error"> 166 </span> 167 168 </div> 169 170 171 <!-- ================================================= 172 UPDATE FIELDS 173 ================================================= --> 174 175 <div id="updateFields" 176 class="admin-edit-section"> 177 178 <div class="admin-edit-section-heading"> 179 180 <span class="admin-eyebrow"> 181 PRODUCT UPDATE 182 </span> 183 184 <h3>Product Information</h3> 185 186 </div> 187 188 189 <div class="admin-form-row"> 190 191 <!-- PRICE --> 192 193 <div class="admin-form-field"> 194 195 <label asp-for="Price"> 196 Price 197 </label> 198 199 <div class="admin-input-prefix"> 200 201 <span>$</span> 202 203 <input asp-for="Price" 204 type="number" 205 min="0" 206 step="0.01" /> 207 208 </div> 209 210 <span asp-validation-for="Price" 211 class="admin-field-error"> 212 </span> 213 214 </div> 215 216 217 <!-- STOCK --> 218 219 <div class="admin-form-field"> 220 221 <label asp-for="Stock"> 222 Stock 223 </label> 224 225 <input asp-for="Stock" 226 type="number" 227 min="0" /> 228 229 <span asp-validation-for="Stock" 230 class="admin-field-error"> 231 </span> 232 233 </div> 234 235 </div> 236 237 238 <div class="admin-form-field"> 239 240 <label asp-for="ProductDescription"> 241 Description 242 </label> 243 244 <textarea asp-for="ProductDescription" 245 rows="5"></textarea> 246 247 <span asp-validation-for="ProductDescription" 248 class="admin-field-error"> 249 </span> 250 251 </div> 252 253 </div> 254 255 256 <!-- ================================================= 257 DISCOUNT 258 ================================================= --> 259 260 <div id="discountFields" 261 class="admin-edit-section"> 262 263 <div class="admin-edit-section-heading"> 264 265 <span class="admin-eyebrow"> 266 PROMOTION 267 </span> 268 269 <h3>Apply Discount</h3> 270 271 <p> 272 The percentage will be deducted 273 from the product's current price. 274 </p> 275 276 </div> 277 278 279 <div class="admin-form-field"> 280 281 <label asp-for="Discount"> 282 Discount Percentage 283 </label> 284 285 <div class="admin-input-suffix"> 286 287 <input asp-for="Discount" 288 type="number" 289 min="0.01" 290 max="99.99" 291 step="0.01" 292 placeholder="10" /> 293 294 <span>%</span> 295 296 </div> 297 298 <span asp-validation-for="Discount" 299 class="admin-field-error"> 300 </span> 301 302 </div> 303 304 305 <div class="admin-discount-preview"> 306 307 <span> 308 CURRENT PRICE 309 </span> 310 311 <strong id="currentPrice" 312 data-price="@Model.Price"> 313 @Model.Price.ToString("C") 314 </strong> 315 316 317 <span> 318 NEW PRICE 319 </span> 320 321 <strong id="discountedPrice"> 322 — 323 </strong> 324 325 </div> 326 327 </div> 328 329 330 <!-- ACTIONS --> 331 332 <div class="admin-form-actions"> 333 334 <button type="submit" 335 class="admin-button primary"> 336 337 Save Changes 338 339 </button> 340 341 <a asp-action="Products" 342 class="admin-button secondary"> 343 344 Cancel 345 346 </a> 347 348 </div> 349 350 </form> 351 352 </section> 353 23 354 </div> 24 355 25 26 <form asp-action="EditProduct"27 method="post">28 29 @Html.AntiForgeryToken()30 31 <input type="hidden"32 asp-for="ProductId" />33 34 <div asp-validation-summary="ModelOnly"35 class="text-danger mb-3">36 </div>37 38 39 <!-- MODIFICATION TYPE -->40 41 <div class="mb-3">42 43 <label asp-for="ModificationType"44 class="form-label">45 46 Modification Type47 48 </label>49 50 <select asp-for="ModificationType"51 class="form-select">52 53 <option value="UPDATE">54 Update Product55 </option>56 57 <option value="DISCOUNT">58 Apply Discount59 </option>60 61 </select>62 63 <span asp-validation-for="ModificationType"64 class="text-danger">65 </span>66 67 </div>68 69 70 <!-- PRICE -->71 72 <div class="mb-3">73 74 <label asp-for="Price"75 class="form-label">76 77 Price78 79 </label>80 81 <div class="input-group">82 83 <span class="input-group-text">84 $85 </span>86 87 <input asp-for="Price"88 class="form-control"89 step="0.01" />90 91 </div>92 93 <span asp-validation-for="Price"94 class="text-danger">95 </span>96 97 </div>98 99 100 <!-- DISCOUNT -->101 102 <div class="mb-3">103 104 <label asp-for="Discount"105 class="form-label">106 107 Discount Percentage108 109 </label>110 111 <div class="input-group">112 113 <input asp-for="Discount"114 class="form-control"115 type="number"116 min="0"117 max="100"118 step="0.01" />119 120 <span class="input-group-text">121 %122 </span>123 124 </div>125 126 <small class="text-muted">127 Only used when "Apply Discount" is selected.128 </small>129 130 <span asp-validation-for="Discount"131 class="text-danger">132 </span>133 134 </div>135 136 137 <!-- DESCRIPTION -->138 139 <div class="mb-3">140 141 <label asp-for="ProductDescription"142 class="form-label">143 144 Description145 146 </label>147 148 <textarea asp-for="ProductDescription"149 class="form-control"150 rows="4">151 </textarea>152 153 <span asp-validation-for="ProductDescription"154 class="text-danger">155 </span>156 157 </div>158 159 160 <!-- STOCK -->161 162 <div class="mb-3">163 164 <label asp-for="Stock"165 class="form-label">166 167 Stock168 169 </label>170 171 <input asp-for="Stock"172 class="form-control"173 type="number"174 min="0" />175 176 <span asp-validation-for="Stock"177 class="text-danger">178 </span>179 180 </div>181 182 183 <div class="d-flex gap-2">184 185 <button type="submit"186 class="btn btn-success">187 188 Save Changes189 190 </button>191 192 <a asp-action="Index"193 class="btn btn-secondary">194 195 Cancel196 197 </a>198 199 </div>200 201 </form>202 356 203 357 @section Scripts 204 358 { 205 <partial name="_ValidationScriptsPartial"/> 359 <partial name="_ValidationScriptsPartial" /> 360 361 <script> 362 363 document.addEventListener("DOMContentLoaded", function () 364 { 365 const updateOption = 366 document.getElementById("updateOption"); 367 368 const discountOption = 369 document.getElementById("discountOption"); 370 371 const updateFields = 372 document.getElementById("updateFields"); 373 374 const discountFields = 375 document.getElementById("discountFields"); 376 377 const discountInput = 378 document.getElementById("Discount"); 379 380 const currentPriceElement = 381 document.getElementById("currentPrice"); 382 383 const discountedPriceElement = 384 document.getElementById("discountedPrice"); 385 386 387 function updateMode() 388 { 389 const isDiscount = 390 discountOption.checked; 391 392 updateFields.style.display = 393 isDiscount ? "none" : "block"; 394 395 discountFields.style.display = 396 isDiscount ? "block" : "none"; 397 } 398 399 400 function updateDiscountPreview() 401 { 402 const currentPrice = 403 parseFloat( 404 currentPriceElement.dataset.price); 405 406 const discount = 407 parseFloat(discountInput.value); 408 409 410 if (isNaN(discount) || 411 discount <= 0 || 412 discount >= 100) 413 { 414 discountedPriceElement.textContent = 415 "—"; 416 417 return; 418 } 419 420 421 const newPrice = 422 currentPrice * 423 (1 - discount / 100); 424 425 426 discountedPriceElement.textContent = 427 "$" + newPrice.toFixed(2); 428 } 429 430 431 updateOption.addEventListener( 432 "change", 433 updateMode); 434 435 discountOption.addEventListener( 436 "change", 437 updateMode); 438 439 discountInput.addEventListener( 440 "input", 441 updateDiscountPreview); 442 443 444 updateMode(); 445 updateDiscountPreview(); 446 }); 447 448 </script> 206 449 } -
KernelRecordsMVC.Web/Views/Admin/Index.cshtml
r08aefc6 rfa0fbaf 3 3 } 4 4 5 <h1>Admin Dashboard</h1> 6 7 @if (TempData["Success"] != null) 8 { 9 <div class="alert alert-success"> 10 @TempData["Success"] 5 <div class="admin-page"> 6 7 <!-- ===================================================== 8 HEADER 9 ===================================================== --> 10 11 <div class="admin-header"> 12 13 <div> 14 15 <span class="admin-eyebrow"> 16 KERNEL RECORDS 17 </span> 18 19 <h1>Admin Dashboard</h1> 20 21 <p> 22 Manage catalog, inventory and store operations. 23 </p> 24 25 </div> 26 27 28 <div class="admin-header-actions"> 29 30 <a asp-action="CreateRelease" 31 class="admin-button secondary"> 32 33 <span>♪</span> 34 New Release 35 36 </a> 37 38 39 <a asp-action="CreateProduct" 40 class="admin-button primary"> 41 42 <span>+</span> 43 New Product 44 45 </a> 46 47 </div> 48 11 49 </div> 12 } 13 14 <div class="row mt-4"> 15 16 <div class="col-md-6 mb-3"> 17 18 <div class="card h-100"> 19 20 <div class="card-body"> 21 22 <h4>Products</h4> 50 51 52 @if (TempData["Success"] != null) 53 { 54 <div class="admin-alert success"> 55 56 <span>✓</span> 57 58 @TempData["Success"] 59 60 </div> 61 } 62 63 64 @if (TempData["Error"] != null) 65 { 66 <div class="admin-alert danger"> 67 68 <span>!</span> 69 70 @TempData["Error"] 71 72 </div> 73 } 74 75 76 <!-- ===================================================== 77 STATISTICS 78 ===================================================== --> 79 80 <div class="admin-stat-grid"> 81 82 <div class="admin-stat-card"> 83 84 <span class="admin-stat-label"> 85 PRODUCTS 86 </span> 87 88 <strong> 89 @ViewBag.ProductCount 90 </strong> 91 92 <p> 93 Physical products in the store 94 </p> 95 96 </div> 97 98 99 <div class="admin-stat-card"> 100 101 <span class="admin-stat-label"> 102 RELEASES 103 </span> 104 105 <strong> 106 @ViewBag.ReleaseCount 107 </strong> 108 109 <p> 110 Albums and singles in the catalog 111 </p> 112 113 </div> 114 115 116 <div class="admin-stat-card warning"> 117 118 <span class="admin-stat-label"> 119 LOW STOCK 120 </span> 121 122 <strong> 123 @ViewBag.LowStockCount 124 </strong> 125 126 <p> 127 Products with 5 or fewer remaining 128 </p> 129 130 </div> 131 132 133 <div class="admin-stat-card danger"> 134 135 <span class="admin-stat-label"> 136 OUT OF STOCK 137 </span> 138 139 <strong> 140 @ViewBag.OutOfStockCount 141 </strong> 142 143 <p> 144 Products requiring restocking 145 </p> 146 147 </div> 148 149 </div> 150 151 152 <!-- ===================================================== 153 CATALOG MANAGEMENT 154 ===================================================== --> 155 156 <section class="admin-section"> 157 158 <div class="admin-section-heading"> 159 160 <div> 161 162 <span class="admin-eyebrow"> 163 CATALOG MANAGEMENT 164 </span> 165 166 <h2>Releases & Products</h2> 23 167 24 168 <p> 25 Create and modify products available 26 in the music store. 169 Create music releases and manage their physical formats. 27 170 </p> 28 171 29 <a asp-action="Products" 30 class="btn btn-primary"> 31 32 Manage Products 33 34 </a> 35 36 <a asp-action="CreateProduct" 37 class="btn btn-success"> 38 39 New Product 40 41 </a> 42 43 </div> 44 45 </div> 46 47 </div> 172 </div> 173 174 </div> 175 176 177 <div class="admin-action-grid"> 178 179 <!-- CREATE RELEASE --> 180 181 <a asp-action="CreateRelease" 182 class="admin-action-card"> 183 184 <div class="admin-action-icon"> 185 ♪ 186 </div> 187 188 <div> 189 190 <h3>Create Release</h3> 191 192 <p> 193 Create an album or single, 194 assign artists and add album tracks. 195 </p> 196 197 </div> 198 199 <span class="admin-action-arrow"> 200 → 201 </span> 202 203 </a> 204 205 206 <!-- CREATE PRODUCT --> 207 208 <a asp-action="CreateProduct" 209 class="admin-action-card"> 210 211 <div class="admin-action-icon"> 212 + 213 </div> 214 215 <div> 216 217 <h3>Create Product</h3> 218 219 <p> 220 Add vinyl, CD or cassette 221 editions to an existing release. 222 </p> 223 224 </div> 225 226 <span class="admin-action-arrow"> 227 → 228 </span> 229 230 </a> 231 232 233 <!-- MANAGE PRODUCTS --> 234 235 <a asp-action="Products" 236 class="admin-action-card"> 237 238 <div class="admin-action-icon"> 239 ◫ 240 </div> 241 242 <div> 243 244 <h3>Manage Products</h3> 245 246 <p> 247 Update pricing, inventory, 248 descriptions and discounts. 249 </p> 250 251 </div> 252 253 <span class="admin-action-arrow"> 254 → 255 </span> 256 257 </a> 258 259 </div> 260 261 </section> 262 263 264 <!-- ===================================================== 265 WORKFLOW INFO 266 ===================================================== --> 267 268 <section class="admin-section"> 269 270 <div class="admin-section-heading"> 271 272 <div> 273 274 <span class="admin-eyebrow"> 275 ADMIN WORKFLOW 276 </span> 277 278 <h2>Catalog Process</h2> 279 280 </div> 281 282 </div> 283 284 285 <div class="admin-workflow-grid"> 286 287 <div class="admin-workflow-card"> 288 289 <span class="admin-workflow-number"> 290 01 291 </span> 292 293 <div> 294 295 <strong> 296 Create Release 297 </strong> 298 299 <p> 300 Add the album or single, 301 artists and track information. 302 </p> 303 304 </div> 305 306 </div> 307 308 309 <div class="admin-workflow-card"> 310 311 <span class="admin-workflow-number"> 312 02 313 </span> 314 315 <div> 316 317 <strong> 318 Create Product 319 </strong> 320 321 <p> 322 Add its vinyl, CD or cassette 323 store edition. 324 </p> 325 326 </div> 327 328 </div> 329 330 331 <div class="admin-workflow-card"> 332 333 <span class="admin-workflow-number"> 334 03 335 </span> 336 337 <div> 338 339 <strong> 340 Manage Inventory 341 </strong> 342 343 <p> 344 Change prices, stock, 345 descriptions or apply discounts. 346 </p> 347 348 </div> 349 350 </div> 351 352 </div> 353 354 </section> 48 355 49 356 </div> -
KernelRecordsMVC.Web/Views/Admin/Products.cshtml
r08aefc6 rfa0fbaf 1 1 @model IEnumerable<KernelRecordsMVC.Models.Product> 2 2 3 <div class="d-flex justify-content-between align-items-center mb-4"> 4 5 <div> 6 7 <h1>Product Management</h1> 8 9 <p class="text-muted"> 10 Create and modify products in the store. 11 </p> 3 @{ 4 ViewData["Title"] = "Product Management"; 5 } 6 7 <div class="admin-page"> 8 9 <!-- HEADER --> 10 11 <div class="admin-header"> 12 13 <div> 14 15 <a asp-action="Index" 16 class="admin-back-link"> 17 18 ← Dashboard 19 20 </a> 21 22 <span class="admin-eyebrow"> 23 INVENTORY 24 </span> 25 26 <h1>Product Management</h1> 27 28 <p> 29 Manage formats, prices and inventory. 30 </p> 31 32 </div> 33 34 35 <a asp-action="CreateProduct" 36 class="admin-button primary"> 37 38 <span>+</span> 39 New Product 40 41 </a> 12 42 13 43 </div> 14 44 15 45 16 <a asp-action="CreateProduct" 17 class="btn btn-success"> 18 19 + New Product 20 21 </a> 46 @if (TempData["Success"] != null) 47 { 48 <div class="admin-alert success"> 49 50 <span>✓</span> 51 52 @TempData["Success"] 53 54 </div> 55 } 56 57 58 <!-- SUMMARY --> 59 60 <div class="admin-list-summary"> 61 62 <span> 63 @Model.Count() products 64 </span> 65 66 <span> 67 @Model.Count(x => x.Stock <= 0) out of stock 68 </span> 69 70 </div> 71 72 73 <!-- TABLE --> 74 75 <div class="admin-table-wrapper"> 76 77 <table class="admin-table"> 78 79 <thead> 80 81 <tr> 82 83 <th>Release</th> 84 85 <th>Format</th> 86 87 <th>Price</th> 88 89 <th>Stock</th> 90 91 <th>Status</th> 92 93 <th></th> 94 95 </tr> 96 97 </thead> 98 99 100 <tbody> 101 102 @foreach (var product in Model) 103 { 104 <tr> 105 106 <!-- RELEASE --> 107 108 <td> 109 110 <div class="admin-product-release"> 111 112 <div class="admin-product-cover"> 113 114 @if (!string.IsNullOrWhiteSpace( 115 product.Release.CoverPhoto)) 116 { 117 <img src="@product.Release.CoverPhoto" 118 alt="@product.Release.Title" /> 119 } 120 else 121 { 122 <span>♪</span> 123 } 124 125 </div> 126 127 128 <div> 129 130 <strong> 131 @product.Release.Title 132 </strong> 133 134 <small> 135 Product #@product.ProductId 136 </small> 137 138 </div> 139 140 </div> 141 142 </td> 143 144 145 <!-- FORMAT --> 146 147 <td> 148 149 <span class="admin-format-badge"> 150 @product.Format 151 </span> 152 153 </td> 154 155 156 <!-- PRICE --> 157 158 <td class="admin-price"> 159 160 @product.Price.ToString("C") 161 162 </td> 163 164 165 <!-- STOCK --> 166 167 <td> 168 169 <strong> 170 @product.Stock 171 </strong> 172 173 </td> 174 175 176 <!-- STATUS --> 177 178 <td> 179 180 @if (product.Stock <= 0) 181 { 182 <span class="admin-stock danger"> 183 Out of stock 184 </span> 185 } 186 else if (product.Stock <= 5) 187 { 188 <span class="admin-stock warning"> 189 Low stock 190 </span> 191 } 192 else 193 { 194 <span class="admin-stock success"> 195 In stock 196 </span> 197 } 198 199 </td> 200 201 202 <!-- ACTION --> 203 204 <td class="admin-table-action"> 205 206 <a asp-action="EditProduct" 207 asp-route-id="@product.ProductId" 208 class="admin-button secondary small"> 209 210 Modify 211 212 </a> 213 214 </td> 215 216 </tr> 217 } 218 219 </tbody> 220 221 </table> 222 223 </div> 22 224 23 225 </div> 24 25 26 @if (TempData["Success"] != null)27 {28 <div class="alert alert-success">29 30 @TempData["Success"]31 32 </div>33 }34 35 36 <div class="table-responsive">37 38 <table class="table table-hover align-middle">39 40 <thead>41 42 <tr>43 44 <th>Release</th>45 46 <th>Format</th>47 48 <th>Price</th>49 50 <th>Stock</th>51 52 <th></th>53 54 </tr>55 56 </thead>57 58 59 <tbody>60 61 @foreach (var product in Model)62 {63 <tr>64 65 <td>66 67 <strong>68 @product.Release.Title69 </strong>70 71 </td>72 73 74 <td>75 76 <span class="badge bg-secondary">77 78 @product.Format79 80 </span>81 82 </td>83 84 85 <td>86 87 $@product.Price.ToString("0.00")88 89 </td>90 91 92 <td>93 94 @if (product.Stock > 0)95 {96 <span class="text-success">97 98 @product.Stock99 100 </span>101 }102 else103 {104 <span class="text-danger">105 Out of stock106 </span>107 }108 109 </td>110 111 112 <td>113 114 <a asp-action="EditProduct"115 asp-route-id="@product.ProductId"116 class="btn btn-outline-primary">117 118 Modify119 120 </a>121 122 </td>123 124 </tr>125 }126 127 </tbody>128 129 </table>130 131 </div> -
KernelRecordsMVC.Web/Views/Home/Index.cshtml
r08aefc6 rfa0fbaf 1 <div class="text-center py-5"> 1 @{ 2 ViewData["Title"] = "Home"; 3 } 2 4 3 <h1 class="display-3"> 4 Music Store 5 </h1> 5 <div class="home-page"> 6 6 7 < p class="lead mt-3">8 CDs · Vinyl · Cassettes9 </p>7 <!-- ===================================================== 8 HERO 9 ===================================================== --> 10 10 11 <p class="text-muted"> 12 Discover your next favorite release. 13 </p> 11 <section class="home-hero"> 14 12 15 <a asp-controller="Release" 16 asp-action="Index" 17 class="btn btn-primary btn-lg mt-3"> 13 <div class="home-hero-content"> 18 14 19 Browse Releases 15 <span class="home-eyebrow"> 16 KERNEL RECORDS 17 </span> 20 18 21 </a> 19 <h1> 20 MUSIC 21 <span>FOR</span> 22 YOUR COLLECTION. 23 </h1> 24 25 <p> 26 Discover vinyl records, CDs and cassettes 27 from artists and releases worth keeping. 28 </p> 29 30 31 <div class="home-hero-actions"> 32 33 <a asp-controller="Release" 34 asp-action="Index" 35 class="home-button primary"> 36 37 Browse Collection 38 39 </a> 40 41 <a asp-controller="Sale" 42 asp-action="Index" 43 class="home-button secondary"> 44 45 Shop Sale 46 47 </a> 48 49 </div> 50 51 </div> 52 53 54 <div class="home-hero-mark"> 55 K 56 </div> 57 58 </section> 59 60 61 <!-- ===================================================== 62 QUICK LINKS 63 ===================================================== --> 64 65 <section class="home-section"> 66 67 <div class="home-section-header"> 68 69 <div> 70 71 <span class="section-label"> 72 SHOP 73 </span> 74 75 <h2> 76 Find something you like. 77 </h2> 78 79 </div> 80 81 </div> 82 83 84 <div class="home-feature-grid"> 85 86 <a asp-controller="Release" 87 asp-action="Index" 88 class="home-feature-card"> 89 90 <span>01</span> 91 92 <h3> 93 Browse Releases 94 </h3> 95 96 <p> 97 Explore albums and singles. 98 </p> 99 100 <strong> 101 Explore → 102 </strong> 103 104 </a> 105 106 107 <a asp-controller="TopSellers" 108 asp-action="Index" 109 class="home-feature-card"> 110 111 <span>02</span> 112 113 <h3> 114 Top Sellers 115 </h3> 116 117 <p> 118 See what other collectors are buying. 119 </p> 120 121 <strong> 122 View Top Sellers → 123 </strong> 124 125 </a> 126 127 128 <a asp-controller="Sale" 129 asp-action="Index" 130 class="home-feature-card sale-feature"> 131 132 <span>03</span> 133 134 <h3> 135 Sale 136 </h3> 137 138 <p> 139 Find discounted physical releases. 140 </p> 141 142 <strong> 143 Shop Sale → 144 </strong> 145 146 </a> 147 148 </div> 149 150 </section> 151 152 153 <!-- ===================================================== 154 COLLECTION INFO 155 ===================================================== --> 156 157 <section class="home-collection"> 158 159 <div> 160 161 <span class="section-label"> 162 PHYSICAL MUSIC 163 </span> 164 165 <h2> 166 Vinyl. CDs. Cassettes. 167 </h2> 168 169 </div> 170 171 <p> 172 Build a physical collection with releases 173 available in the formats you actually want. 174 </p> 175 176 </section> 22 177 23 178 </div> -
KernelRecordsMVC.Web/Views/Order/Cart.cshtml
r08aefc6 rfa0fbaf 1 1 @model KernelRecordsMVC.Application.ViewModels.CartViewModel 2 2 3 <div class="d-flex justify-content-between align-items-center mb-4"> 4 5 <h1>Your Cart</h1> 6 7 <a asp-controller="Release" 8 asp-action="Index" 9 class="btn btn-outline-primary"> 10 11 Continue Shopping 12 13 </a> 14 15 </div> 16 17 18 @if (TempData["Error"] != null) 19 { 20 <div class="alert alert-danger"> 21 22 @TempData["Error"] 23 24 </div> 3 @{ 4 ViewData["Title"] = "Cart"; 25 5 } 26 6 27 28 @if (!Model.Items.Any()) 29 { 30 <div class="text-center py-5"> 31 32 <h3>Your cart is empty.</h3> 33 34 <p class="text-muted"> 35 Browse our releases and add something to your order. 36 </p> 7 <div class="cart-page"> 8 9 <div class="cart-header"> 10 11 <div> 12 13 <span class="page-eyebrow"> 14 YOUR ORDER 15 </span> 16 17 <h1>Your Cart</h1> 18 19 <p> 20 Review your selected physical releases. 21 </p> 22 23 </div> 37 24 38 25 <a asp-controller="Release" 39 26 asp-action="Index" 40 class=" btn btn-primary">41 42 Browse Releases27 class="store-button secondary"> 28 29 Continue Shopping 43 30 44 31 </a> … … 46 33 </div> 47 34 48 return; 49 } 50 51 52 <div class="table-responsive"> 53 54 <table class="table align-middle"> 55 56 <thead> 57 58 <tr> 59 60 <th>Release</th> 61 62 <th>Format</th> 63 64 <th>Price</th> 65 66 <th>Quantity</th> 67 68 <th>Total</th> 69 70 <th></th> 71 72 </tr> 73 74 </thead> 75 76 77 <tbody> 78 79 @foreach (var item in Model.Items) 80 { 81 <tr> 82 83 <td> 84 85 <a asp-controller="Release" 86 asp-action="Details" 87 asp-route-id="@item.ReleaseId"> 88 89 @item.ReleaseTitle 90 91 </a> 92 93 </td> 94 95 96 <td> 97 98 <span class="badge bg-secondary"> 99 100 @item.Format 101 35 36 @if (TempData["Error"] != null) 37 { 38 <div class="profile-alert" 39 style="background:#f9e8e8;color:#a33;"> 40 @TempData["Error"] 41 </div> 42 } 43 44 45 @if (!Model.Items.Any()) 46 { 47 <div class="empty-store"> 48 49 <div class="empty-store-icon"> 50 🛒 51 </div> 52 53 <h2>Your cart is empty</h2> 54 55 <p> 56 Browse our releases and add something to your order. 57 </p> 58 59 <a asp-controller="Release" 60 asp-action="Index" 61 class="store-button primary"> 62 63 Browse Releases 64 65 </a> 66 67 </div> 68 } 69 else 70 { 71 <div class="cart-layout"> 72 73 <div class="cart-items"> 74 75 <div class="cart-items-header"> 76 @Model.Items.Count item(s) in your cart 77 </div> 78 79 80 @foreach (var item in Model.Items) 81 { 82 <div class="cart-item"> 83 84 <a asp-controller="Release" 85 asp-action="Details" 86 asp-route-id="@item.ReleaseId" 87 class="cart-item-cover"> 88 89 @if (!string.IsNullOrWhiteSpace(item.CoverPhoto)) 90 { 91 <img src="@item.CoverPhoto" 92 alt="@item.ReleaseTitle" /> 93 } 94 else 95 { 96 <div class="cart-cover-placeholder"> 97 ♪ 98 </div> 99 } 100 101 </a> 102 103 104 <div class="cart-item-info"> 105 106 <span class="cart-item-type"> 107 @item.Format 108 </span> 109 110 <h2> 111 112 <a asp-controller="Release" 113 asp-action="Details" 114 asp-route-id="@item.ReleaseId"> 115 116 @item.ReleaseTitle 117 118 </a> 119 120 </h2> 121 122 <div class="cart-item-price"> 123 124 @item.Price.ToString("C") 125 each 126 127 </div> 128 129 </div> 130 131 132 <div class="cart-item-quantity"> 133 134 <form asp-action="UpdateQuantity" 135 method="post"> 136 137 @Html.AntiForgeryToken() 138 139 <input type="hidden" 140 name="id" 141 value="@item.ProductId" /> 142 143 <input type="number" 144 name="quantity" 145 value="@item.Quantity" 146 min="1" 147 max="@item.Stock" /> 148 149 <button type="submit"> 150 Update 151 </button> 152 153 </form> 154 155 </div> 156 157 158 <div class="cart-item-total"> 159 160 @item.Total.ToString("C") 161 162 </div> 163 164 165 <form asp-action="RemoveProduct" 166 method="post"> 167 168 @Html.AntiForgeryToken() 169 170 <input type="hidden" 171 name="id" 172 value="@item.ProductId" /> 173 174 <button type="submit" 175 class="cart-remove" 176 title="Remove"> 177 178 × 179 180 </button> 181 182 </form> 183 184 </div> 185 } 186 187 </div> 188 189 190 <aside class="cart-summary"> 191 192 <span class="section-label"> 193 ORDER SUMMARY 194 </span> 195 196 <h2> 197 Your Order 198 </h2> 199 200 201 <div class="summary-line"> 202 203 <span> 204 Items 102 205 </span> 103 206 104 </td>105 106 107 <td>108 109 $@item.Price.ToString("0.00")110 111 </td>112 113 114 <td>115 116 <form asp-action="UpdateQuantity"117 method="post"118 class="d-flex">119 120 @Html.AntiForgeryToken()121 122 <input type="hidden"123 name="id"124 value="@item.ProductId" />125 126 <input type="number"127 name="quantity"128 value="@item.Quantity"129 min="1"130 max="@item.Stock"131 class="form-control"132 style="width:90px;" />133 134 <button type="submit"135 class="btn btn-outline-secondary ms-2">136 137 Update138 139 </button>140 141 </form>142 143 </td>144 145 146 <td>147 148 207 <strong> 149 150 $@item.Total.ToString("0.00") 151 208 @Model.Items.Sum(x => x.Quantity) 152 209 </strong> 153 210 154 </td> 155 156 157 <td> 158 159 <form asp-action="RemoveProduct" 160 method="post"> 161 162 @Html.AntiForgeryToken() 163 164 <input type="hidden" 165 name="id" 166 value="@item.ProductId" /> 167 168 <button type="submit" 169 class="btn btn-outline-danger"> 170 171 Remove 172 173 </button> 174 175 </form> 176 177 </td> 178 179 </tr> 180 } 181 182 </tbody> 183 184 </table> 211 </div> 212 213 214 <div class="summary-line total"> 215 216 <span> 217 Total 218 </span> 219 220 <strong> 221 @Model.Total.ToString("C") 222 </strong> 223 224 </div> 225 226 227 <a asp-action="Checkout" 228 class="store-button primary full"> 229 230 Proceed to Checkout 231 232 </a> 233 234 </aside> 235 236 </div> 237 } 185 238 186 239 </div> 187 188 189 <div class="row justify-content-end">190 191 <div class="col-md-4">192 193 <div class="card">194 195 <div class="card-body">196 197 <h4>Order Summary</h4>198 199 <hr />200 201 <div class="d-flex justify-content-between">202 203 <span>Total</span>204 205 <strong>206 $@Model.Total.ToString("0.00")207 </strong>208 209 </div>210 211 212 <a asp-action="Checkout"213 class="btn btn-success w-100 mt-3">214 215 Proceed to Checkout216 217 </a>218 219 </div>220 221 </div>222 223 </div>224 225 </div> -
KernelRecordsMVC.Web/Views/Release/Details.cshtml
r08aefc6 rfa0fbaf 1 1 @model KernelRecordsMVC.Models.Release 2 2 3 <div class="row"> 4 5 <div class="col-md-5"> 6 7 <img src="@Model.CoverPhoto" 8 class="img-fluid rounded" 9 alt="@Model.Title" /> 3 @{ 4 ViewData["Title"] = Model.Title; 5 6 var isAlbum = Model.Album != null; 7 var isSingle = Model.SingleRelease != null; 8 9 var releaseType = 10 isAlbum ? "ALBUM" : 11 isSingle ? "SINGLE" : 12 "RELEASE"; 13 14 var artists = string.Join( 15 ", ", 16 Model.ReleaseArtists 17 .OrderBy(x => x.ReleaseOrdinal) 18 .Select(x => x.Artist.ArtistName) 19 ); 20 } 21 22 <div class="release-details-page"> 23 24 <div class="release-hero"> 25 26 <div class="release-hero-cover"> 27 28 @if (!string.IsNullOrWhiteSpace(Model.CoverPhoto)) 29 { 30 <img src="@Model.CoverPhoto" 31 alt="@Model.Title" /> 32 } 33 else 34 { 35 <div class="release-no-cover"> 36 <span>♪</span> 37 </div> 38 } 39 40 </div> 41 42 43 <div class="release-hero-info"> 44 45 <span class="page-eyebrow"> 46 @releaseType 47 </span> 48 49 <h1> 50 @Model.Title 51 </h1> 52 53 <p class="release-artists-main"> 54 @artists 55 </p> 56 57 58 <div class="release-facts"> 59 60 <div> 61 62 <span>Genre</span> 63 64 <strong> 65 @Model.Genre 66 </strong> 67 68 </div> 69 70 <div> 71 72 <span>Released</span> 73 74 <strong> 75 @Model.ReleaseDate.ToString("MMM dd, yyyy") 76 </strong> 77 78 </div> 79 80 <div> 81 82 <span>Label</span> 83 84 <strong> 85 @Model.RecordLabel 86 </strong> 87 88 </div> 89 90 </div> 91 92 93 <div class="artist-section"> 94 95 <span class="section-label"> 96 ARTISTS 97 </span> 98 99 <div class="release-artists"> 100 101 @foreach (var artist in Model.ReleaseArtists 102 .OrderBy(x => x.ReleaseOrdinal)) 103 { 104 <span class="artist-tag"> 105 106 @artist.Artist.ArtistName 107 108 @if (artist.Type.ToString() == "FEATURE") 109 { 110 <span class="artist-role"> 111 Featured 112 </span> 113 } 114 115 </span> 116 } 117 118 </div> 119 120 </div> 121 122 </div> 10 123 11 124 </div> 12 125 13 126 14 <div class="col-md-7"> 15 16 <h1>@Model.Title</h1> 17 18 <p class="lead"> 19 @Model.Genre 20 </p> 21 22 <p> 23 <strong>Record Label:</strong> 24 @Model.RecordLabel 25 </p> 26 27 <p> 28 <strong>Release Date:</strong> 29 @Model.ReleaseDate.ToString("MMMM dd, yyyy") 30 </p> 31 32 33 <h5>Artists</h5> 34 35 <p> 36 37 @string.Join( 38 ", ", 39 Model.ReleaseArtists 40 .OrderBy(x => x.ReleaseOrdinal) 41 .Select(x => x.Artist.ArtistName) 42 ) 43 44 </p> 45 46 </div> 127 <!-- ===================================================== 128 PRODUCTS 129 ===================================================== --> 130 131 <section class="release-section"> 132 133 <div class="section-heading"> 134 135 <div> 136 137 <span class="section-label"> 138 AVAILABLE FORMATS 139 </span> 140 141 <h2> 142 Choose your format 143 </h2> 144 145 </div> 146 147 <span class="track-count"> 148 @Model.Products.Count product(s) 149 </span> 150 151 </div> 152 153 154 @if (!Model.Products.Any()) 155 { 156 <div class="no-products"> 157 No physical products are currently available. 158 </div> 159 } 160 else 161 { 162 <div class="product-grid"> 163 164 @foreach (var product in Model.Products 165 .OrderBy(x => x.Format)) 166 { 167 <div class="store-product-card"> 168 169 <div class="product-card-top"> 170 171 <span class="product-format"> 172 @product.Format 173 </span> 174 175 @if (product.Stock > 0) 176 { 177 <span class="product-stock available"> 178 In stock 179 </span> 180 } 181 else 182 { 183 <span class="product-stock unavailable"> 184 Out of stock 185 </span> 186 } 187 188 </div> 189 190 191 <div class="product-price"> 192 @product.Price.ToString("C") 193 </div> 194 195 196 <p class="product-description"> 197 @product.ProductDescription 198 </p> 199 200 201 @if (product.Stock > 0) 202 { 203 <div class="product-actions"> 204 205 <a asp-controller="Order" 206 asp-action="AddProduct" 207 asp-route-id="@product.ProductId" 208 class="store-button primary"> 209 210 Add to Cart 211 212 </a> 213 214 <a asp-controller="Wishlist" 215 asp-action="Add" 216 asp-route-productId="@product.ProductId" 217 class="store-button secondary"> 218 219 ♡ Wishlist 220 221 </a> 222 223 </div> 224 } 225 else 226 { 227 <div class="product-actions"> 228 229 <a asp-controller="Wishlist" 230 asp-action="Add" 231 asp-route-productId="@product.ProductId" 232 class="store-button secondary full"> 233 234 ♡ Add to Wishlist 235 236 </a> 237 238 </div> 239 } 240 241 </div> 242 } 243 244 </div> 245 } 246 247 </section> 248 249 250 <!-- ===================================================== 251 ALBUM 252 ===================================================== --> 253 254 @if (Model.Album != null) 255 { 256 <section class="release-section"> 257 258 <div class="section-heading"> 259 260 <div> 261 262 <span class="section-label"> 263 ALBUM 264 </span> 265 266 <h2> 267 Track List 268 </h2> 269 270 </div> 271 272 <span class="track-count"> 273 @Model.Album.AlbumSongs.Count tracks 274 </span> 275 276 </div> 277 278 279 <ol class="track-list"> 280 281 @{ 282 var trackNumber = 1; 283 } 284 285 @foreach (var albumSong in Model.Album.AlbumSongs 286 .OrderBy(x => x.SongId)) 287 { 288 <li class="track-row"> 289 290 <span class="track-number"> 291 @trackNumber.ToString("00") 292 </span> 293 294 <div class="track-main"> 295 296 <strong> 297 @albumSong.Song.SongName 298 </strong> 299 300 @if (albumSong.Song.SongArtists.Any()) 301 { 302 <div class="song-artists"> 303 304 @string.Join( 305 ", ", 306 albumSong.Song.SongArtists 307 .OrderBy(x => x.SongOrdinal) 308 .Select(x => x.Artist.ArtistName) 309 ) 310 311 </div> 312 } 313 314 </div> 315 316 <span class="track-duration"> 317 @albumSong.Song.SongDuration 318 </span> 319 320 </li> 321 322 trackNumber++; 323 } 324 325 </ol> 326 327 </section> 328 } 329 330 331 <!-- ===================================================== 332 SINGLE 333 ===================================================== --> 334 335 @if (Model.SingleRelease != null) 336 { 337 <section class="release-section"> 338 339 <div class="section-heading"> 340 341 <div> 342 343 <span class="section-label"> 344 SINGLE 345 </span> 346 347 <h2> 348 Single Information 349 </h2> 350 351 </div> 352 353 </div> 354 355 356 <div class="single-info-card"> 357 358 <div> 359 360 <span class="section-label"> 361 RELEASE TYPE 362 </span> 363 364 <h2> 365 Single 366 </h2> 367 368 </div> 369 370 <div class="single-duration"> 371 372 <span> 373 Duration 374 </span> 375 376 <strong> 377 @Model.SingleRelease.Duration 378 </strong> 379 380 </div> 381 382 </div> 383 384 </section> 385 } 47 386 48 387 </div> 49 50 51 <hr />52 53 54 <h2>Available Products</h2>55 56 <div class="row">57 58 @foreach (var product in Model.Products)59 {60 <div class="col-md-4 mb-3">61 62 <div class="card h-100">63 64 <div class="card-body">65 66 <h4>67 @product.Format68 </h4>69 70 <h5>71 $@product.Price72 </h5>73 74 <p>75 @product.ProductDescription76 </p>77 78 79 @if (product.Stock > 0)80 {81 <p class="text-success">82 In stock: @product.Stock83 </p>84 85 <a asp-controller="Order"86 asp-action="AddProduct"87 asp-route-id="@product.ProductId"88 class="btn btn-success">89 90 Add to Cart91 92 </a>93 }94 else95 {96 <p class="text-danger">97 Out of stock98 </p>99 }100 101 </div>102 103 </div>104 105 </div>106 }107 108 </div>109 110 111 @if (Model.Album != null)112 {113 <hr />114 115 <h2>Track List</h2>116 117 <ol>118 119 @foreach (var albumSong in Model.Album.AlbumSongs120 .OrderBy(x => x.SongId))121 {122 <li>123 124 @albumSong.Song.SongName125 126 <span class="text-muted">127 (@albumSong.Song.SongDuration)128 </span>129 130 </li>131 }132 133 </ol>134 }135 136 137 @if (Model.SingleRelease != null)138 {139 <hr />140 141 <h2>Single</h2>142 143 <p>144 Duration:145 @Model.SingleRelease.Duration146 </p>147 } -
KernelRecordsMVC.Web/Views/Release/Index.cshtml
r08aefc6 rfa0fbaf 1 1 @model IEnumerable<KernelRecordsMVC.Models.Release> 2 2 3 <div class="d-flex justify-content-between align-items-center mb-4"> 4 5 <div> 6 <h1>Music Store</h1> 7 <p class="text-muted"> 8 Browse CDs, vinyl records and cassettes. 9 </p> 3 @{ 4 ViewData["Title"] = "Browse Releases"; 5 } 6 7 <div class="releases-page"> 8 9 <!-- ===================================================== 10 HEADER 11 ===================================================== --> 12 13 <div class="releases-header"> 14 15 <div> 16 <span class="page-eyebrow">KERNEL RECORDS</span> 17 18 <h1>Browse Releases</h1> 19 20 <p> 21 Explore vinyl, CDs and cassettes from our collection. 22 </p> 23 </div> 24 25 <div class="release-count"> 26 @Model.Count() releases 27 </div> 28 10 29 </div> 11 30 31 32 <!-- ===================================================== 33 FILTERS 34 ===================================================== --> 35 36 <form asp-action="Index" 37 method="get" 38 class="release-filters"> 39 40 <div class="filter-search"> 41 42 <label>Search</label> 43 44 <input type="text" 45 name="search" 46 value="@ViewBag.Search" 47 placeholder="Search by title or artist..." /> 48 49 </div> 50 51 52 <div> 53 54 <label>Genre</label> 55 56 <select name="genre"> 57 58 <option value="">All Genres</option> 59 60 @foreach (var item in ViewBag.Genres) 61 { 62 <option value="@item" 63 selected="@(ViewBag.Genre == item ? "selected" : null)"> 64 @item 65 </option> 66 } 67 68 </select> 69 70 </div> 71 72 73 <div> 74 75 <label>Format</label> 76 77 <select name="format"> 78 79 <option value="">All Formats</option> 80 81 @foreach (var item in ViewBag.Formats) 82 { 83 <option value="@item" 84 selected="@(ViewBag.Format == item ? "selected" : null)"> 85 @item 86 </option> 87 } 88 89 </select> 90 91 </div> 92 93 94 <div> 95 96 <label>Type</label> 97 98 <select name="type"> 99 100 <option value="">All Types</option> 101 102 <option value="Album" 103 selected="@(ViewBag.Type == "Album" ? "selected" : null)"> 104 Album 105 </option> 106 107 <option value="Single" 108 selected="@(ViewBag.Type == "Single" ? "selected" : null)"> 109 Single 110 </option> 111 112 </select> 113 114 </div> 115 116 117 <div> 118 119 <label>Sort</label> 120 121 <select name="sort"> 122 123 <option value="">Title A–Z</option> 124 125 <option value="titleDesc" 126 selected="@(ViewBag.Sort == "titleDesc" ? "selected" : null)"> 127 Title Z–A 128 </option> 129 130 <option value="newest" 131 selected="@(ViewBag.Sort == "newest" ? "selected" : null)"> 132 Newest 133 </option> 134 135 <option value="oldest" 136 selected="@(ViewBag.Sort == "oldest" ? "selected" : null)"> 137 Oldest 138 </option> 139 140 <option value="format" 141 selected="@(ViewBag.Sort == "format" ? "selected" : null)"> 142 Format 143 </option> 144 145 <option value="priceAsc" 146 selected="@(ViewBag.Sort == "priceAsc" ? "selected" : null)"> 147 Price: Low to High 148 </option> 149 150 <option value="priceDesc" 151 selected="@(ViewBag.Sort == "priceDesc" ? "selected" : null)"> 152 Price: High to Low 153 </option> 154 155 </select> 156 157 </div> 158 159 160 <div class="filter-button"> 161 162 <button type="submit" 163 class="store-button primary"> 164 165 Apply Filters 166 167 </button> 168 169 </div> 170 171 172 @if (!string.IsNullOrWhiteSpace(ViewBag.Search as string) || 173 !string.IsNullOrWhiteSpace(ViewBag.Genre as string) || 174 !string.IsNullOrWhiteSpace(ViewBag.Format as string) || 175 !string.IsNullOrWhiteSpace(ViewBag.Type as string) || 176 !string.IsNullOrWhiteSpace(ViewBag.Sort as string)) 177 { 178 <div class="clear-filter"> 179 180 <a asp-action="Index"> 181 Clear all filters 182 </a> 183 184 </div> 185 } 186 187 </form> 188 189 190 <!-- ===================================================== 191 RELEASE GRID 192 ===================================================== --> 193 194 @if (!Model.Any()) 195 { 196 <div class="empty-store"> 197 198 <div class="empty-store-icon"> 199 ♪ 200 </div> 201 202 <h2>No releases found</h2> 203 204 <p> 205 Try changing your search or filters. 206 </p> 207 208 <a asp-action="Index" 209 class="store-button primary"> 210 View All Releases 211 </a> 212 213 </div> 214 } 215 else 216 { 217 <div class="release-grid"> 218 219 @foreach (var release in Model) 220 { 221 var isAlbum = release.Album != null; 222 var isSingle = release.SingleRelease != null; 223 224 var releaseType = 225 isAlbum ? "ALBUM" : 226 isSingle ? "SINGLE" : 227 "RELEASE"; 228 229 var artists = string.Join( 230 ", ", 231 release.ReleaseArtists 232 .OrderBy(x => x.ReleaseOrdinal) 233 .Select(x => x.Artist.ArtistName) 234 ); 235 236 var lowestPrice = release.Products 237 .Select(x => (decimal?)x.Price) 238 .Min(); 239 240 <article class="release-card"> 241 242 <a asp-action="Details" 243 asp-route-id="@release.ReleaseId" 244 class="release-card-cover"> 245 246 @if (!string.IsNullOrWhiteSpace(release.CoverPhoto)) 247 { 248 <img src="@release.CoverPhoto" 249 alt="@release.Title" /> 250 } 251 else 252 { 253 <div class="release-no-cover"> 254 <span>♪</span> 255 </div> 256 } 257 258 <span class="release-type-badge"> 259 @releaseType 260 </span> 261 262 </a> 263 264 265 <div class="release-card-body"> 266 267 <div class="release-card-meta"> 268 269 <span> 270 @release.Genre 271 </span> 272 273 <span> 274 @release.ReleaseDate.ToString("yyyy") 275 </span> 276 277 </div> 278 279 280 <h2> 281 282 <a asp-action="Details" 283 asp-route-id="@release.ReleaseId"> 284 285 @release.Title 286 287 </a> 288 289 </h2> 290 291 292 <p class="release-card-artist"> 293 294 @artists 295 296 </p> 297 298 299 <div class="format-list"> 300 301 @foreach (var product in release.Products 302 .GroupBy(x => x.Format) 303 .Select(x => x.First())) 304 { 305 <span class="format-pill"> 306 @product.Format 307 </span> 308 } 309 310 </div> 311 312 313 <div class="release-card-bottom"> 314 315 <div class="release-starting"> 316 317 Starting at 318 319 <strong> 320 @(lowestPrice?.ToString("C") ?? "N/A") 321 </strong> 322 323 </div> 324 325 326 <a asp-action="Details" 327 asp-route-id="@release.ReleaseId" 328 class="store-button primary small"> 329 330 View 331 332 </a> 333 334 </div> 335 336 </div> 337 338 </article> 339 } 340 341 </div> 342 } 343 12 344 </div> 13 14 15 <!-- SEARCH -->16 17 <form asp-action="Index"18 method="get"19 class="row g-2 mb-4">20 21 <div class="col-md-6">22 23 <input type="text"24 name="search"25 value="@ViewBag.Search"26 class="form-control"27 placeholder="Search releases..." />28 29 </div>30 31 32 <div class="col-md-3">33 34 <select name="genre"35 class="form-select">36 37 <option value="">38 All genres39 </option>40 41 @foreach (var genre in ViewBag.Genres)42 {43 <option value="@genre"44 selected="@(ViewBag.Genre == genre)">45 46 @genre47 48 </option>49 }50 51 </select>52 53 </div>54 55 56 <div class="col-md-3">57 58 <button type="submit"59 class="btn btn-primary w-100">60 61 Search62 63 </button>64 65 </div>66 67 </form>68 69 70 <!-- RELEASES -->71 72 <div class="row">73 74 @foreach (var release in Model)75 {76 <div class="col-md-4 mb-4">77 78 <div class="card h-100 shadow-sm">79 80 @if (!string.IsNullOrEmpty(release.CoverPhoto))81 {82 <img src="@release.CoverPhoto"83 class="card-img-top"84 style="height:300px; object-fit:cover;"85 alt="@release.Title" />86 }87 88 <div class="card-body">89 90 <h4 class="card-title">91 @release.Title92 </h4>93 94 <p class="text-muted mb-1">95 @release.Genre96 </p>97 98 <p class="small">99 @release.ReleaseDate.ToString("yyyy")100 </p>101 102 103 <!-- ARTISTS -->104 105 <p>106 107 @string.Join(108 ", ",109 release.ReleaseArtists110 .OrderBy(x => x.ReleaseOrdinal)111 .Select(x => x.Artist.ArtistName)112 )113 114 </p>115 116 117 <!-- AVAILABLE FORMATS -->118 119 <div class="mb-3">120 121 @foreach (var product in release.Products)122 {123 <span class="badge bg-secondary me-1">124 125 @product.Format126 127 </span>128 }129 130 </div>131 132 133 <a asp-action="Details"134 asp-route-id="@release.ReleaseId"135 class="btn btn-primary">136 137 View Release138 139 </a>140 141 </div>142 143 </div>144 145 </div>146 }147 148 </div> -
KernelRecordsMVC.Web/Views/Shared/_Layout.cshtml
r08aefc6 rfa0fbaf 1 <!DOCTYPE html> 1 @using Microsoft.AspNetCore.Http 2 3 <!DOCTYPE html> 4 2 5 <html lang="en"> 3 6 <head> 4 <meta charset="utf-8"/> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 6 <title>@ViewData["Title"] - KernelRecordsMVC</title> 7 <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> 8 <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> 9 <link rel="stylesheet" href="~/KernelRecordsMVC.styles.css" asp-append-version="true"/> 7 <meta charset="utf-8" /> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 9 10 11 <title>@ViewData["Title"] - Kernel Records</title> 12 13 <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> 14 15 10 16 </head> 17 11 18 <body> 12 <header> 13 <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> 14 <div class="container-fluid"> 15 <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">KernelRecordsMVC</a> 16 <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent" 17 aria-expanded="false" aria-label="Toggle navigation"> 18 <span class="navbar-toggler-icon"></span> 19 </button> 20 <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> 21 <ul class="navbar-nav flex-grow-1"> 22 <li class="nav-item"> 23 <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> 24 </li> 25 <li class="nav-item"> 26 <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> 27 </li> 28 </ul> 19 20 <header class="site-header"> 21 22 23 <!-- ===================================================== 24 MAIN NAVBAR 25 ===================================================== --> 26 27 <nav class="navbar"> 28 29 <div class="navbar-container"> 30 31 <!-- LOGO --> 32 33 <a class="brand" 34 asp-controller="Home" 35 asp-action="Index"> 36 37 <span class="brand-main"> 38 KERNEL 39 </span> 40 41 <span class="brand-sub"> 42 RECORDS 43 </span> 44 45 </a> 46 47 48 <!-- MOBILE MENU BUTTON --> 49 50 <button class="mobile-menu-button" 51 type="button" 52 aria-label="Toggle navigation" 53 onclick="toggleMobileMenu()"> 54 55 ☰ 56 57 </button> 58 59 60 <!-- NAVBAR CONTENT --> 61 62 <div class="navbar-content" 63 id="navbarContent"> 64 65 66 <!-- ================================================= 67 MAIN NAVIGATION 68 ================================================= --> 69 70 <ul class="nav-links"> 71 72 <!-- Browse --> 73 74 <li> 75 76 <a asp-controller="Release" 77 asp-action="Index"> 78 79 Browse 80 81 </a> 82 83 </li> 84 85 86 <!-- Top Sellers --> 87 88 <li> 89 90 <a asp-controller="TopSellers" 91 asp-action="Index"> 92 93 Top Sellers 94 95 </a> 96 97 </li> 98 99 100 <!-- Artists --> 101 102 <li> 103 104 <a asp-controller="Artist" 105 asp-action="Index"> 106 107 Artists 108 109 </a> 110 111 </li> 112 113 114 <!-- Genres --> 115 116 <li class="nav-dropdown"> 117 118 <a href="#" 119 class="dropdown-toggle"> 120 121 Genres 122 123 <span class="arrow"> 124 ⌄ 125 </span> 126 127 </a> 128 129 130 <div class="dropdown-menu"> 131 132 <a asp-controller="Release" 133 asp-action="Index" 134 asp-route-genre="Rock"> 135 136 Rock 137 138 </a> 139 140 <a asp-controller="Release" 141 asp-action="Index" 142 asp-route-genre="Pop"> 143 144 Pop 145 146 </a> 147 148 <a asp-controller="Release" 149 asp-action="Index" 150 asp-route-genre="Hip-Hop"> 151 152 Hip-Hop 153 154 </a> 155 156 <a asp-controller="Release" 157 asp-action="Index" 158 asp-route-genre="Jazz"> 159 160 Jazz 161 162 </a> 163 164 <a asp-controller="Release" 165 asp-action="Index" 166 asp-route-genre="Electronic"> 167 168 Electronic 169 170 </a> 171 172 <a asp-controller="Release" 173 asp-action="Index"> 174 175 All Genres 176 177 </a> 178 179 </div> 180 181 </li> 182 183 184 <!-- Sale --> 185 186 <li> 187 188 <a asp-controller="Sale" 189 asp-action="Index" 190 class="nav-sale"> 191 192 Sale 193 194 </a> 195 196 </li> 197 198 </ul> 199 200 201 <!-- ================================================= 202 SEARCH 203 ================================================= --> 204 205 <form class="navbar-search" 206 asp-controller="Release" 207 asp-action="Index" 208 method="get"> 209 210 <input type="text" 211 name="search" 212 placeholder="Search releases..." 213 autocomplete="off" /> 214 215 <button type="submit" 216 aria-label="Search"> 217 218 <span> 219 ⌕ 220 </span> 221 222 </button> 223 224 </form> 225 226 227 <!-- ================================================= 228 RIGHT SIDE ACTIONS 229 ================================================= --> 230 231 <div class="navbar-actions"> 232 233 234 <!-- ================================================= 235 WISHLIST 236 ================================================= --> 237 238 <a class="nav-icon" 239 asp-controller="Wishlist" 240 asp-action="Index" 241 title="Wishlist"> 242 243 <span class="icon"> 244 ♡ 245 </span> 246 247 <span class="icon-label"> 248 Wishlist 249 </span> 250 251 </a> 252 253 254 <!-- ================================================= 255 CART 256 ================================================= --> 257 258 <a class="nav-icon" 259 asp-controller="Order" 260 asp-action="Cart" 261 title="Shopping Cart"> 262 263 <span class="icon cart-icon"> 264 🛒 265 </span> 266 267 <span class="icon-label"> 268 Cart 269 </span> 270 271 </a> 272 273 274 <!-- ================================================= 275 ACCOUNT 276 ================================================= --> 277 278 @if (Context.Session.GetInt32("UserId") != null) 279 { 280 281 <!-- LOGGED IN --> 282 283 <div class="account-dropdown"> 284 285 286 <button class="account-button" 287 type="button"> 288 289 <span class="user-icon"> 290 ● 291 </span> 292 293 <span class="account-name"> 294 295 @Context.Session.GetString("Username") 296 297 </span> 298 299 <span class="arrow"> 300 ⌄ 301 </span> 302 303 </button> 304 305 306 <!-- ACCOUNT MENU --> 307 308 <div class="account-menu"> 309 310 311 <!-- PROFILE --> 312 313 <a asp-controller="Account" 314 asp-action="Profile"> 315 316 <span> 317 👤 318 </span> 319 320 My Profile 321 322 </a> 323 324 325 <!-- ORDERS --> 326 327 <a asp-controller="Order" 328 asp-action="Cart"> 329 330 <span> 331 📦 332 </span> 333 334 My Orders 335 336 </a> 337 338 339 <!-- WISHLIST --> 340 341 <a asp-controller="Wishlist" 342 asp-action="Index"> 343 344 <span> 345 ♡ 346 </span> 347 348 My Wishlist 349 350 </a> 351 352 353 <!-- ADMIN --> 354 355 @if (Context.Session.GetString("Role") == "Admin") 356 { 357 358 <div class="menu-divider"></div> 359 360 361 <a asp-controller="Admin" 362 asp-action="Index" 363 class="admin-link"> 364 365 <span> 366 ⚙ 367 </span> 368 369 Admin Dashboard 370 371 </a> 372 373 } 374 375 376 <div class="menu-divider"></div> 377 378 379 <!-- LOGOUT --> 380 381 <form asp-controller="Account" 382 asp-action="Logout" 383 method="post"> 384 385 @Html.AntiForgeryToken() 386 387 <button type="submit" 388 class="logout-button"> 389 390 <span>↪</span> 391 392 Logout 393 394 </button> 395 396 </form> 397 398 399 </div> 400 401 </div> 402 403 } 404 else 405 { 406 407 <!-- ================================================= 408 NOT LOGGED IN 409 ================================================= --> 410 411 <div class="auth-buttons"> 412 413 414 <a asp-controller="Account" 415 asp-action="Login" 416 class="login-button"> 417 418 Login 419 420 </a> 421 422 423 <a asp-controller="Account" 424 asp-action="Register" 425 class="register-button"> 426 427 Register 428 429 </a> 430 431 432 </div> 433 434 } 435 29 436 </div> 437 30 438 </div> 31 </nav> 439 440 </div> 441 442 </nav> 443 444 445 <!-- ===================================================== 446 ANNOUNCEMENT BAR 447 ===================================================== --> 448 449 <div class="announcement-bar"> 450 451 <span> 452 FREE SHIPPING ON ORDERS OVER €50 453 </span> 454 455 <span class="announcement-separator"> 456 • 457 </span> 458 459 <span> 460 NEW VINYL EVERY WEEK 461 </span> 462 463 </div> 464 465 32 466 </header> 33 <div class="container"> 34 <main role="main" class="pb-3"> 35 @RenderBody() 36 </main> 467 468 <!-- ========================================================= 469 MAIN CONTENT 470 ========================================================= --> 471 472 <main class="main-content"> 473 474 475 @RenderBody() 476 477 478 </main> 479 480 <!-- ========================================================= 481 FOOTER 482 ========================================================= --> 483 484 <footer class="site-footer"> 485 486 487 <div class="footer-container"> 488 489 490 <!-- BRAND --> 491 492 <div class="footer-brand"> 493 494 <div class="footer-logo"> 495 KERNEL RECORDS 496 </div> 497 498 <p> 499 Your destination for vinyl, CDs and cassettes. 500 </p> 501 502 </div> 503 504 505 <!-- SHOP --> 506 507 <div class="footer-column"> 508 509 <h4> 510 Shop 511 </h4> 512 513 514 <a asp-controller="Release" 515 asp-action="Index"> 516 517 Releases 518 519 </a> 520 521 522 <a asp-controller="Release" 523 asp-action="Index"> 524 525 New Releases 526 527 </a> 528 529 530 <a asp-controller="Artist" 531 asp-action="Index"> 532 533 Artists 534 535 </a> 536 537 </div> 538 539 540 <!-- ACCOUNT --> 541 542 <div class="footer-column"> 543 544 <h4> 545 Account 546 </h4> 547 548 549 @if (Context.Session.GetInt32("UserId") != null) 550 { 551 552 <a asp-controller="Account" 553 asp-action="Profile"> 554 555 My Profile 556 557 </a> 558 559 560 <a asp-controller="Order" 561 asp-action="Cart"> 562 563 My Orders 564 565 </a> 566 567 568 <a asp-controller="Wishlist" 569 asp-action="Index"> 570 571 Wishlist 572 573 </a> 574 575 } 576 else 577 { 578 579 <a asp-controller="Account" 580 asp-action="Login"> 581 582 Login 583 584 </a> 585 586 587 <a asp-controller="Account" 588 asp-action="Register"> 589 590 Register 591 592 </a> 593 594 } 595 596 </div> 597 598 599 <!-- HELP --> 600 601 <div class="footer-column"> 602 603 <h4> 604 Help 605 </h4> 606 607 <a href="#"> 608 Shipping 609 </a> 610 611 <a href="#"> 612 Returns 613 </a> 614 615 <a href="#"> 616 Contact 617 </a> 618 619 </div> 620 37 621 </div> 38 622 39 <footer class="border-top footer text-muted"> 40 <div class="container"> 41 © 2026 - KernelRecordsMVC - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> 42 </div> 623 624 <!-- FOOTER BOTTOM --> 625 626 <div class="footer-bottom"> 627 628 <span> 629 © @DateTime.Now.Year Kernel Records 630 </span> 631 632 <span> 633 All rights reserved. 634 </span> 635 636 </div> 637 638 43 639 </footer> 44 <script src="~/lib/jquery/dist/jquery.min.js"></script> 45 <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> 46 <script src="~/js/site.js" asp-append-version="true"></script> 47 @await RenderSectionAsync("Scripts", required: false) 640 641 <!-- ========================================================= 642 MOBILE NAVIGATION SCRIPT 643 ========================================================= --> 644 645 <script> 646 647 function toggleMobileMenu() 648 { 649 const menu = 650 document.getElementById("navbarContent"); 651 652 menu.classList.toggle("mobile-open"); 653 } 654 655 656 // Close mobile menu when clicking outside 657 658 document.addEventListener("click", function (event) 659 { 660 const menu = 661 document.getElementById("navbarContent"); 662 663 const button = 664 document.querySelector(".mobile-menu-button"); 665 666 667 if (!menu.contains(event.target) && 668 !button.contains(event.target)) 669 { 670 menu.classList.remove("mobile-open"); 671 } 672 673 }); 674 675 </script> 676 677 @await RenderSectionAsync( 678 "Scripts", 679 required: false) 680 48 681 </body> 49 682 </html> -
KernelRecordsMVC.Web/appsettings.json
r08aefc6 rfa0fbaf 1 1 { 2 "ConnectionStrings": { 3 "KernelRecords": "" 4 }, 5 2 6 "Logging": { 3 7 "LogLevel": { … … 6 10 } 7 11 }, 12 8 13 "AllowedHosts": "*" 9 14 } -
KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Web.deps.json
r08aefc6 rfa0fbaf 11 11 "KernelRecordsMVC.Application": "1.0.0", 12 12 "KernelRecordsMVC.Infrastructure": "1.0.0", 13 "Microsoft.EntityFrameworkCore": "8.0.8" 13 "Microsoft.EntityFrameworkCore": "8.0.8", 14 "Npgsql.EntityFrameworkCore.PostgreSQL": "8.0.8" 14 15 }, 15 16 "runtime": { -
KernelRecordsMVC.Web/bin/Debug/net8.0/appsettings.json
r08aefc6 rfa0fbaf 1 1 { 2 "ConnectionStrings": { 3 "KernelRecords": "" 4 }, 5 2 6 "Logging": { 3 7 "LogLevel": { … … 6 10 } 7 11 }, 12 8 13 "AllowedHosts": "*" 9 14 } -
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.AssemblyInfo.cs
r08aefc6 rfa0fbaf 11 11 using System.Reflection; 12 12 13 [assembly: Microsoft.Extensions.Configuration.UserSecrets.UserSecretsIdAttribute("7996030f-d5d7-4616-a943-1e4992329169")] 13 14 [assembly: System.Reflection.AssemblyCompanyAttribute("KernelRecordsMVC.Web")] 14 15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] 15 16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] 16 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0 ")]17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+08aefc6f3b51f4bfcb7471a2fd319c064171b1c5")] 17 18 [assembly: System.Reflection.AssemblyProductAttribute("KernelRecordsMVC.Web")] 18 19 [assembly: System.Reflection.AssemblyTitleAttribute("KernelRecordsMVC.Web")] -
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.AssemblyInfoInputs.cache
r08aefc6 rfa0fbaf 1 a4f4eb88e517c7eefc9f4de02b54f54cf2b8d15155e7524b493c4ff6d5ef0bd5 1 c8c33cb9ccc0e5c5e6227ad0d596fbb462f0c80224c10ca755e42b9afff049e9 -
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.GeneratedMSBuildEditorConfig.editorconfig
r08aefc6 rfa0fbaf 65 65 build_metadata.AdditionalFiles.CssScope = 66 66 67 [C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Account/Profile.cshtml] 68 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWNjb3VudFxQcm9maWxlLmNzaHRtbA== 69 build_metadata.AdditionalFiles.CssScope = 70 67 71 [C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Account/Register.cshtml] 68 72 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWNjb3VudFxSZWdpc3Rlci5jc2h0bWw= … … 71 75 [C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Admin/CreateProduct.cshtml] 72 76 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWRtaW5cQ3JlYXRlUHJvZHVjdC5jc2h0bWw= 77 build_metadata.AdditionalFiles.CssScope = 78 79 [C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Admin/CreateRelease.cshtml] 80 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWRtaW5cQ3JlYXRlUmVsZWFzZS5jc2h0bWw= 73 81 build_metadata.AdditionalFiles.CssScope = 74 82 … … 96 104 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUmVsZWFzZVxJbmRleC5jc2h0bWw= 97 105 build_metadata.AdditionalFiles.CssScope = 106 107 [C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Sale/Index.cshtml] 108 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2FsZVxJbmRleC5jc2h0bWw= 109 build_metadata.AdditionalFiles.CssScope = 110 111 [C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/TopSellers/Index.cshtml] 112 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcVG9wU2VsbGVyc1xJbmRleC5jc2h0bWw= 113 build_metadata.AdditionalFiles.CssScope = 114 115 [C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Wishlist/Index.cshtml] 116 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcV2lzaGxpc3RcSW5kZXguY3NodG1s 117 build_metadata.AdditionalFiles.CssScope = -
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.csproj.CoreCompileInputs.cache
r08aefc6 rfa0fbaf 1 9f4bc3ac55251cbca4306c0fb37573f798c006e8b97657e25caacf03d029d22e 1 21c22170dbd5df356be1a77b3f77b619add1a8fb50970ec121bfff2f0bc30d5a -
KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.csproj.FileListAbsolute.txt
r08aefc6 rfa0fbaf 26 26 C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\KernelRecordsMVC.Web.RazorAssemblyInfo.cache 27 27 C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\KernelRecordsMVC.Web.RazorAssemblyInfo.cs 28 C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\KernelRecordsMVC.Web.sourcelink.json 28 29 C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\staticwebassets.build.json 29 30 C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\staticwebassets.development.json -
KernelRecordsMVC.Web/obj/KernelRecordsMVC.Web.csproj.nuget.dgspec.json
r08aefc6 rfa0fbaf 69 69 } 70 70 }, 71 "runtimeIdentifierGraphPath": "C:\\ Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"71 "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json" 72 72 } 73 73 } … … 133 133 } 134 134 }, 135 "runtimeIdentifierGraphPath": "C:\\ Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"135 "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json" 136 136 } 137 137 } … … 208 208 } 209 209 }, 210 "runtimeIdentifierGraphPath": "C:\\ Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"210 "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json" 211 211 } 212 212 } … … 261 261 "target": "Package", 262 262 "version": "[8.0.8, )" 263 }, 264 "Npgsql.EntityFrameworkCore.PostgreSQL": { 265 "target": "Package", 266 "version": "[8.0.8, )" 263 267 } 264 268 }, … … 282 286 } 283 287 }, 284 "runtimeIdentifierGraphPath": "C:\\ Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"288 "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json" 285 289 } 286 290 } -
KernelRecordsMVC.Web/obj/KernelRecordsMVC.Web.csproj.nuget.g.props
r08aefc6 rfa0fbaf 8 8 <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Marko\.nuget\packages\</NuGetPackageFolders> 9 9 <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> 10 <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' "> 7.0.0</NuGetToolVersion>10 <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.11.2</NuGetToolVersion> 11 11 </PropertyGroup> 12 12 <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> -
KernelRecordsMVC.Web/obj/project.assets.json
r08aefc6 rfa0fbaf 772 772 "KernelRecordsMVC.Application >= 1.0.0", 773 773 "KernelRecordsMVC.Infrastructure >= 1.0.0", 774 "Microsoft.EntityFrameworkCore >= 8.0.8" 774 "Microsoft.EntityFrameworkCore >= 8.0.8", 775 "Npgsql.EntityFrameworkCore.PostgreSQL >= 8.0.8" 775 776 ] 776 777 }, … … 827 828 "target": "Package", 828 829 "version": "[8.0.8, )" 830 }, 831 "Npgsql.EntityFrameworkCore.PostgreSQL": { 832 "target": "Package", 833 "version": "[8.0.8, )" 829 834 } 830 835 }, … … 848 853 } 849 854 }, 850 "runtimeIdentifierGraphPath": "C:\\ Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"855 "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json" 851 856 } 852 857 } -
KernelRecordsMVC.Web/obj/project.nuget.cache
r08aefc6 rfa0fbaf 1 1 { 2 2 "version": 2, 3 "dgSpecHash": " OEK7FBkFq/o=",3 "dgSpecHash": "iFoBIOYqVS4=", 4 4 "success": true, 5 5 "projectFilePath": "C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Web\\KernelRecordsMVC.Web.csproj", -
KernelRecordsMVC.Web/obj/project.packagespec.json
r08aefc6 rfa0fbaf 1 "restore":{"projectUniqueName":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Web\\KernelRecordsMVC.Web.csproj","projectName":"KernelRecordsMVC.Web","projectPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Web\\KernelRecordsMVC.Web.csproj","packagesPath":"","outputPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Web\\obj\\","projectStyle":"PackageReference","originalTargetFrameworks":["net8.0"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0":{"targetAlias":"net8.0","projectReferences":{"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Application\\KernelRecordsMVC.Application.csproj":{"projectPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Application\\KernelRecordsMVC.Application.csproj"},"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Infrastructure\\KernelRecordsMVC.Infrastructure.csproj":{"projectPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Infrastructure\\KernelRecordsMVC.Infrastructure.csproj"}}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"}}"frameworks":{"net8.0":{"targetAlias":"net8.0","dependencies":{"Microsoft.EntityFrameworkCore":{"target":"Package","version":"[8.0.8, )"} },"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.AspNetCore.App":{"privateAssets":"none"},"Microsoft.NETCore.App":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"C:\\Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"}}1 "restore":{"projectUniqueName":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Web\\KernelRecordsMVC.Web.csproj","projectName":"KernelRecordsMVC.Web","projectPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Web\\KernelRecordsMVC.Web.csproj","packagesPath":"","outputPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Web\\obj\\","projectStyle":"PackageReference","originalTargetFrameworks":["net8.0"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0":{"targetAlias":"net8.0","projectReferences":{"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Application\\KernelRecordsMVC.Application.csproj":{"projectPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Application\\KernelRecordsMVC.Application.csproj"},"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Infrastructure\\KernelRecordsMVC.Infrastructure.csproj":{"projectPath":"C:\\Users\\Marko\\RiderProjects\\KernelRecordsMVC\\KernelRecordsMVC.Infrastructure\\KernelRecordsMVC.Infrastructure.csproj"}}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"}}"frameworks":{"net8.0":{"targetAlias":"net8.0","dependencies":{"Microsoft.EntityFrameworkCore":{"target":"Package","version":"[8.0.8, )"},"Npgsql.EntityFrameworkCore.PostgreSQL":{"target":"Package","version":"[8.0.8, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.AspNetCore.App":{"privateAssets":"none"},"Microsoft.NETCore.App":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json"}} -
KernelRecordsMVC.Web/obj/rider.project.model.nuget.info
r08aefc6 rfa0fbaf 1 178 788995838575931 17882300160457909 -
KernelRecordsMVC.Web/obj/rider.project.restore.info
r08aefc6 rfa0fbaf 1 178 788995962747901 17882300160457909 -
KernelRecordsMVC.Web/wwwroot/css/site.css
r08aefc6 rfa0fbaf 1 /* ========================================================= 2 KERNEL RECORDS 3 MAIN STYLESHEET 4 ========================================================= */ 5 6 * { 7 box-sizing: border-box; 8 margin: 0; 9 padding: 0; 10 } 11 1 12 html { 13 font-size: 16px; 14 scroll-behavior: smooth; 15 } 16 17 body { 18 font-family: 19 Inter, 20 -apple-system, 21 BlinkMacSystemFont, 22 "Segoe UI", 23 sans-serif; 24 25 background: #f5f5f3; 26 color: #171717; 27 28 min-height: 100vh; 29 30 display: flex; 31 flex-direction: column; 32 } 33 34 a { 35 color: inherit; 36 text-decoration: none; 37 } 38 39 button, 40 input, 41 select, 42 textarea { 43 font: inherit; 44 } 45 46 button { 47 cursor: pointer; 48 } 49 50 51 /* ========================================================= 52 NAVBAR 53 ========================================================= */ 54 55 .site-header { 56 position: relative; 57 z-index: 1000; 58 } 59 60 .navbar { 61 background: #111; 62 color: white; 63 border-bottom: 1px solid #292929; 64 } 65 66 .navbar-container { 67 max-width: 1450px; 68 height: 82px; 69 70 margin: auto; 71 padding: 0 30px; 72 73 display: flex; 74 align-items: center; 75 gap: 35px; 76 } 77 78 .brand { 79 display: flex; 80 flex-direction: column; 81 flex-shrink: 0; 82 83 line-height: .9; 84 letter-spacing: 2px; 85 } 86 87 .brand-main { 88 font-size: 25px; 89 font-weight: 900; 90 } 91 92 .brand-sub { 93 margin-top: 6px; 94 95 font-size: 11px; 96 font-weight: 600; 97 letter-spacing: 5px; 98 99 opacity: .65; 100 } 101 102 .navbar-content { 103 flex: 1; 104 105 display: flex; 106 align-items: center; 107 gap: 25px; 108 } 109 110 .nav-links { 111 display: flex; 112 align-items: center; 113 gap: 25px; 114 115 list-style: none; 116 white-space: nowrap; 117 } 118 119 .nav-links a { 120 position: relative; 121 122 color: #d5d5d5; 123 124 font-size: 13px; 125 font-weight: 700; 126 } 127 128 .nav-links a:hover { 129 color: white; 130 } 131 132 .nav-links > li > a::after { 133 content: ""; 134 135 position: absolute; 136 137 left: 0; 138 bottom: -8px; 139 140 width: 0; 141 height: 2px; 142 143 background: white; 144 145 transition: width .2s ease; 146 } 147 148 .nav-links > li > a:hover::after { 149 width: 100%; 150 } 151 152 .nav-sale { 153 color: #ffb4b4 !important; 154 } 155 156 157 /* ========================================================= 158 DROPDOWNS 159 ========================================================= */ 160 161 .nav-dropdown { 162 position: relative; 163 } 164 165 .dropdown-toggle { 166 display: flex; 167 align-items: center; 168 gap: 6px; 169 } 170 171 .arrow { 172 font-size: 12px; 173 opacity: .7; 174 } 175 176 .dropdown-menu { 177 position: absolute; 178 179 top: calc(100% + 22px); 180 left: -15px; 181 182 width: 190px; 183 184 padding: 8px 0; 185 186 background: #1b1b1b; 187 188 border: 1px solid #303030; 189 border-radius: 8px; 190 191 box-shadow: 0 15px 40px rgba(0,0,0,.35); 192 193 opacity: 0; 194 visibility: hidden; 195 196 transform: translateY(-5px); 197 198 transition: 199 opacity .2s ease, 200 transform .2s ease, 201 visibility .2s ease; 202 } 203 204 .nav-dropdown:hover .dropdown-menu { 205 opacity: 1; 206 visibility: visible; 207 transform: translateY(0); 208 } 209 210 .dropdown-menu a { 211 display: block; 212 213 padding: 11px 17px; 214 215 color: #ccc; 216 } 217 218 .dropdown-menu a:hover { 219 background: #292929; 220 color: white; 221 } 222 223 224 /* ========================================================= 225 NAV SEARCH 226 ========================================================= */ 227 228 .navbar-search { 229 flex: 1; 230 231 max-width: 320px; 232 height: 40px; 233 234 margin-left: auto; 235 236 display: flex; 237 238 background: #242424; 239 240 border: 1px solid #343434; 241 border-radius: 6px; 242 243 overflow: hidden; 244 } 245 246 .navbar-search input { 247 flex: 1; 248 249 min-width: 0; 250 251 padding: 0 14px; 252 253 border: none; 254 outline: none; 255 256 background: transparent; 257 color: white; 258 259 font-size: 13px; 260 } 261 262 .navbar-search input::placeholder { 263 color: #888; 264 } 265 266 .navbar-search button { 267 width: 43px; 268 269 border: none; 270 background: transparent; 271 272 color: #aaa; 273 274 font-size: 22px; 275 } 276 277 .navbar-search button:hover { 278 background: #303030; 279 color: white; 280 } 281 282 283 /* ========================================================= 284 NAV ACTIONS 285 ========================================================= */ 286 287 .navbar-actions { 288 display: flex; 289 align-items: center; 290 gap: 17px; 291 292 flex-shrink: 0; 293 } 294 295 .nav-icon { 296 display: flex; 297 align-items: center; 298 gap: 7px; 299 300 color: #d1d1d1; 301 302 transition: color .2s ease; 303 } 304 305 .nav-icon:hover { 306 color: white; 307 } 308 309 .icon { 310 font-size: 21px; 311 } 312 313 .icon-label { 314 font-size: 12px; 315 font-weight: 700; 316 } 317 318 .cart-icon { 319 font-size: 17px; 320 } 321 322 323 /* ========================================================= 324 AUTH 325 ========================================================= */ 326 327 .auth-buttons { 328 display: flex; 329 align-items: center; 330 gap: 8px; 331 } 332 333 .login-button, 334 .register-button { 335 height: 38px; 336 337 padding: 0 15px; 338 339 display: flex; 340 align-items: center; 341 justify-content: center; 342 343 border-radius: 5px; 344 345 font-size: 12px; 346 font-weight: 800; 347 } 348 349 .login-button { 350 border: 1px solid #555; 351 background: transparent; 352 color: #eee; 353 } 354 355 .login-button:hover { 356 background: #292929; 357 } 358 359 .register-button { 360 border: 1px solid white; 361 background: white; 362 color: #111; 363 } 364 365 .register-button:hover { 366 background: #ddd; 367 } 368 369 370 /* ========================================================= 371 ACCOUNT 372 ========================================================= */ 373 374 .account-dropdown { 375 position: relative; 376 } 377 378 .account-button { 379 height: 40px; 380 381 display: flex; 382 align-items: center; 383 gap: 8px; 384 385 padding: 0 5px; 386 387 border: none; 388 background: transparent; 389 color: white; 390 } 391 392 .user-icon { 393 width: 28px; 394 height: 28px; 395 396 display: flex; 397 align-items: center; 398 justify-content: center; 399 400 border-radius: 50%; 401 402 background: #303030; 403 color: #aaa; 404 405 font-size: 10px; 406 } 407 408 .account-name { 409 max-width: 100px; 410 411 overflow: hidden; 412 text-overflow: ellipsis; 413 white-space: nowrap; 414 415 font-size: 12px; 416 font-weight: 700; 417 } 418 419 .account-menu { 420 position: absolute; 421 422 right: 0; 423 top: calc(100% + 15px); 424 425 width: 210px; 426 427 padding: 7px 0; 428 429 background: #1b1b1b; 430 431 border: 1px solid #303030; 432 border-radius: 8px; 433 434 box-shadow: 0 15px 40px rgba(0,0,0,.35); 435 436 opacity: 0; 437 visibility: hidden; 438 439 transform: translateY(-5px); 440 441 transition: 442 opacity .2s ease, 443 transform .2s ease, 444 visibility .2s ease; 445 } 446 447 .account-dropdown:hover .account-menu { 448 opacity: 1; 449 visibility: visible; 450 transform: translateY(0); 451 } 452 453 .account-menu a, 454 .logout-button { 455 width: 100%; 456 457 display: flex; 458 align-items: center; 459 gap: 11px; 460 461 padding: 11px 16px; 462 463 border: none; 464 background: transparent; 465 466 color: #d0d0d0; 467 468 font-size: 13px; 469 text-align: left; 470 } 471 472 .account-menu a:hover, 473 .logout-button:hover { 474 background: #292929; 475 color: white; 476 } 477 478 .menu-divider { 479 height: 1px; 480 481 margin: 6px 0; 482 483 background: #303030; 484 } 485 486 487 /* ========================================================= 488 ANNOUNCEMENT 489 ========================================================= */ 490 491 .announcement-bar { 492 height: 32px; 493 494 display: flex; 495 align-items: center; 496 justify-content: center; 497 gap: 10px; 498 499 background: #e8e8e8; 500 color: #333; 501 502 font-size: 9px; 503 font-weight: 900; 504 letter-spacing: 1.3px; 505 } 506 507 .announcement-separator { 508 opacity: .45; 509 } 510 511 512 /* ========================================================= 513 MAIN 514 ========================================================= */ 515 516 .main-content { 517 flex: 1; 518 width: 100%; 519 } 520 521 522 /* ========================================================= 523 GENERAL 524 ========================================================= */ 525 526 .page-eyebrow, 527 .section-label { 528 display: block; 529 530 color: #777; 531 532 font-size: 10px; 533 font-weight: 900; 534 535 letter-spacing: 1.8px; 536 text-transform: uppercase; 537 } 538 539 .store-button { 540 min-height: 42px; 541 542 display: inline-flex; 543 align-items: center; 544 justify-content: center; 545 546 padding: 0 18px; 547 548 border: 1px solid #171717; 549 border-radius: 6px; 550 551 font-size: 12px; 552 font-weight: 800; 553 554 transition: 555 background .2s ease, 556 color .2s ease, 557 transform .2s ease; 558 } 559 560 .store-button:hover { 561 transform: translateY(-1px); 562 } 563 564 .store-button.primary { 565 background: #171717; 566 color: white; 567 } 568 569 .store-button.primary:hover { 570 background: #333; 571 } 572 573 .store-button.secondary { 574 background: white; 575 color: #171717; 576 border-color: #d4d4d4; 577 } 578 579 .store-button.secondary:hover { 580 background: #eee; 581 } 582 583 .store-button.small { 584 min-height: 36px; 585 padding: 0 14px; 586 } 587 588 .store-button.full { 589 width: 100%; 590 } 591 592 593 /* ========================================================= 594 RELEASE INDEX 595 ========================================================= */ 596 597 .releases-page { 598 max-width: 1380px; 599 600 margin: 0 auto; 601 602 padding: 55px 30px 80px; 603 } 604 605 .releases-header { 606 display: flex; 607 align-items: flex-end; 608 justify-content: space-between; 609 610 margin-bottom: 35px; 611 } 612 613 .releases-header h1 { 614 margin-top: 8px; 615 616 font-size: 44px; 617 font-weight: 900; 618 letter-spacing: -1.5px; 619 } 620 621 .releases-header p { 622 margin-top: 8px; 623 624 color: #777; 625 626 font-size: 15px; 627 } 628 629 .release-count { 630 color: #777; 631 632 font-size: 12px; 633 font-weight: 800; 634 } 635 636 637 /* ========================================================= 638 FILTERS 639 ========================================================= */ 640 641 .release-filters { 642 display: grid; 643 644 grid-template-columns: 645 2fr 646 1fr 647 1fr 648 1fr 649 1fr 650 auto; 651 652 gap: 12px; 653 654 align-items: end; 655 656 padding: 18px; 657 658 margin-bottom: 35px; 659 660 background: white; 661 662 border: 1px solid #ddd; 663 border-radius: 10px; 664 665 box-shadow: 0 5px 20px rgba(0,0,0,.04); 666 } 667 668 .release-filters label { 669 display: block; 670 671 margin-bottom: 7px; 672 673 color: #666; 674 675 font-size: 9px; 676 font-weight: 900; 677 678 text-transform: uppercase; 679 letter-spacing: 1px; 680 } 681 682 .release-filters input, 683 .release-filters select { 684 width: 100%; 685 height: 44px; 686 687 padding: 0 13px; 688 689 border: 1px solid #d5d5d5; 690 border-radius: 6px; 691 692 outline: none; 693 694 background: white; 695 color: #171717; 696 697 font-size: 12px; 698 } 699 700 .release-filters input:focus, 701 .release-filters select:focus { 702 border-color: #777; 703 } 704 705 .filter-button .store-button { 706 height: 44px; 707 } 708 709 .clear-filter { 710 grid-column: 1 / -1; 711 } 712 713 .clear-filter a { 714 color: #777; 715 716 font-size: 11px; 717 font-weight: 700; 718 } 719 720 .clear-filter a:hover { 721 color: #111; 722 } 723 724 725 /* ========================================================= 726 RELEASE GRID 727 ========================================================= */ 728 729 .release-grid { 730 display: grid; 731 732 grid-template-columns: 733 repeat(4, minmax(0, 1fr)); 734 735 gap: 24px; 736 } 737 738 .release-card { 739 overflow: hidden; 740 741 background: white; 742 743 border: 1px solid #ddd; 744 border-radius: 10px; 745 746 transition: 747 transform .25s ease, 748 box-shadow .25s ease; 749 } 750 751 .release-card:hover { 752 transform: translateY(-5px); 753 754 box-shadow: 755 0 18px 40px rgba(0,0,0,.10); 756 } 757 758 .release-card-cover { 759 position: relative; 760 761 display: block; 762 763 aspect-ratio: 1 / 1; 764 765 overflow: hidden; 766 767 background: #ddd; 768 } 769 770 .release-card-cover img { 771 width: 100%; 772 height: 100%; 773 774 display: block; 775 776 object-fit: cover; 777 778 transition: transform .4s ease; 779 } 780 781 .release-card:hover .release-card-cover img { 782 transform: scale(1.04); 783 } 784 785 .release-no-cover { 786 width: 100%; 787 height: 100%; 788 789 display: flex; 790 align-items: center; 791 justify-content: center; 792 793 background: 794 linear-gradient( 795 135deg, 796 #222, 797 #555 798 ); 799 800 color: white; 801 } 802 803 .release-no-cover span { 804 font-size: 55px; 805 } 806 807 .release-type-badge { 808 position: absolute; 809 810 top: 12px; 811 left: 12px; 812 813 padding: 6px 9px; 814 815 background: rgba(17,17,17,.9); 816 color: white; 817 818 border-radius: 4px; 819 820 font-size: 9px; 821 font-weight: 900; 822 823 letter-spacing: 1px; 824 } 825 826 .release-card-body { 827 padding: 19px; 828 } 829 830 .release-card-meta { 831 display: flex; 832 justify-content: space-between; 833 834 margin-bottom: 8px; 835 836 color: #888; 837 838 font-size: 9px; 839 font-weight: 800; 840 841 text-transform: uppercase; 842 letter-spacing: .8px; 843 } 844 845 .release-card h2 { 846 margin-bottom: 5px; 847 848 font-size: 20px; 849 font-weight: 800; 850 851 line-height: 1.2; 852 } 853 854 .release-card h2 a:hover { 855 text-decoration: underline; 856 } 857 858 .release-card-artist { 859 min-height: 20px; 860 861 color: #666; 862 863 font-size: 13px; 864 } 865 866 .format-list { 867 display: flex; 868 flex-wrap: wrap; 869 870 gap: 6px; 871 872 margin-top: 14px; 873 } 874 875 .format-pill { 876 padding: 5px 8px; 877 878 background: #f0f0ee; 879 880 border-radius: 4px; 881 882 color: #555; 883 884 font-size: 9px; 885 font-weight: 900; 886 887 letter-spacing: .6px; 888 } 889 890 .release-card-bottom { 891 display: flex; 892 align-items: center; 893 justify-content: space-between; 894 895 gap: 10px; 896 897 margin-top: 18px; 898 padding-top: 15px; 899 900 border-top: 1px solid #eee; 901 } 902 903 .release-starting { 904 color: #888; 905 906 font-size: 10px; 907 } 908 909 .release-starting strong { 910 display: block; 911 912 margin-top: 2px; 913 914 color: #171717; 915 2 916 font-size: 14px; 3 917 } 4 918 5 @media (min-width: 768px) { 6 html { 7 font-size: 16px; 8 } 9 } 10 11 .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 12 box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 13 } 14 15 html { 919 920 /* ========================================================= 921 EMPTY STATES 922 ========================================================= */ 923 924 .empty-store, 925 .no-sales-data { 926 padding: 80px 25px; 927 928 text-align: center; 929 930 background: white; 931 932 border: 1px solid #ddd; 933 border-radius: 10px; 934 } 935 936 .empty-store-icon { 937 margin-bottom: 15px; 938 939 color: #777; 940 941 font-size: 48px; 942 } 943 944 .empty-store h2, 945 .no-sales-data h2 { 946 margin-bottom: 8px; 947 948 font-size: 25px; 949 } 950 951 .empty-store p, 952 .no-sales-data p { 953 margin-bottom: 22px; 954 955 color: #777; 956 } 957 958 959 /* ========================================================= 960 RELEASE DETAILS 961 ========================================================= */ 962 963 .release-details-page { 964 max-width: 1250px; 965 966 margin: 0 auto; 967 968 padding: 50px 30px 80px; 969 } 970 971 .release-hero { 972 display: grid; 973 974 grid-template-columns: 975 minmax(350px, 500px) 976 1fr; 977 978 gap: 55px; 979 980 align-items: start; 981 } 982 983 .release-hero-cover { 984 aspect-ratio: 1 / 1; 985 986 overflow: hidden; 987 988 border-radius: 8px; 989 990 background: #ddd; 991 } 992 993 .release-hero-cover img { 994 width: 100%; 995 height: 100%; 996 997 object-fit: cover; 998 } 999 1000 .release-hero-info h1 { 1001 margin-top: 8px; 1002 1003 font-size: 55px; 1004 line-height: .95; 1005 letter-spacing: -2px; 1006 font-weight: 900; 1007 } 1008 1009 .release-artists-main { 1010 margin: 22px 0 32px; 1011 1012 color: #555; 1013 1014 font-size: 19px; 1015 } 1016 1017 .release-facts { 1018 display: grid; 1019 1020 grid-template-columns: 1021 repeat(3, 1fr); 1022 1023 gap: 15px; 1024 1025 padding: 20px 0; 1026 1027 border-top: 1px solid #ddd; 1028 border-bottom: 1px solid #ddd; 1029 } 1030 1031 .release-facts div { 1032 display: flex; 1033 flex-direction: column; 1034 1035 gap: 5px; 1036 } 1037 1038 .release-facts span { 1039 color: #888; 1040 1041 font-size: 9px; 1042 font-weight: 900; 1043 1044 text-transform: uppercase; 1045 letter-spacing: 1px; 1046 } 1047 1048 .release-facts strong { 1049 font-size: 13px; 1050 } 1051 1052 .artist-section { 1053 margin-top: 25px; 1054 } 1055 1056 .release-artists { 1057 display: flex; 1058 flex-wrap: wrap; 1059 1060 gap: 7px; 1061 1062 margin-top: 10px; 1063 } 1064 1065 .artist-tag { 1066 padding: 7px 10px; 1067 1068 background: white; 1069 1070 border: 1px solid #ddd; 1071 border-radius: 5px; 1072 1073 font-size: 12px; 1074 font-weight: 700; 1075 } 1076 1077 .artist-role { 1078 color: #888; 1079 1080 font-size: 9px; 1081 1082 text-transform: uppercase; 1083 } 1084 1085 1086 /* ========================================================= 1087 RELEASE SECTIONS 1088 ========================================================= */ 1089 1090 .release-section { 1091 margin-top: 70px; 1092 } 1093 1094 .section-heading { 1095 display: flex; 1096 align-items: flex-end; 1097 justify-content: space-between; 1098 1099 margin-bottom: 22px; 1100 padding-bottom: 15px; 1101 1102 border-bottom: 1px solid #ddd; 1103 } 1104 1105 .section-heading h2 { 1106 margin-top: 5px; 1107 1108 font-size: 28px; 1109 font-weight: 900; 1110 } 1111 1112 .track-count { 1113 color: #888; 1114 1115 font-size: 12px; 1116 } 1117 1118 1119 /* ========================================================= 1120 PRODUCT CARDS 1121 ========================================================= */ 1122 1123 .product-grid { 1124 display: grid; 1125 1126 grid-template-columns: 1127 repeat(3, minmax(0, 1fr)); 1128 1129 gap: 18px; 1130 } 1131 1132 .store-product-card { 1133 padding: 23px; 1134 1135 background: white; 1136 1137 border: 1px solid #ddd; 1138 border-radius: 9px; 1139 1140 transition: 1141 transform .2s ease, 1142 box-shadow .2s ease; 1143 } 1144 1145 .store-product-card:hover { 1146 transform: translateY(-3px); 1147 1148 box-shadow: 1149 0 12px 30px rgba(0,0,0,.08); 1150 } 1151 1152 .product-card-top { 1153 display: flex; 1154 align-items: center; 1155 justify-content: space-between; 1156 1157 margin-bottom: 20px; 1158 } 1159 1160 .product-format { 1161 font-size: 13px; 1162 font-weight: 900; 1163 1164 letter-spacing: 1px; 1165 } 1166 1167 .product-stock { 1168 font-size: 10px; 1169 font-weight: 800; 1170 } 1171 1172 .product-stock.available { 1173 color: #27804d; 1174 } 1175 1176 .product-stock.unavailable { 1177 color: #a33; 1178 } 1179 1180 .product-price { 1181 margin-bottom: 10px; 1182 1183 font-size: 28px; 1184 font-weight: 900; 1185 } 1186 1187 .product-description { 1188 min-height: 48px; 1189 1190 color: #777; 1191 1192 font-size: 12px; 1193 line-height: 1.6; 1194 } 1195 1196 .product-actions { 1197 display: grid; 1198 1199 grid-template-columns: 1200 1fr 1fr; 1201 1202 gap: 8px; 1203 1204 margin-top: 20px; 1205 } 1206 1207 1208 /* ========================================================= 1209 TRACK LIST 1210 ========================================================= */ 1211 1212 .track-list { 1213 background: white; 1214 1215 border: 1px solid #ddd; 1216 border-radius: 8px; 1217 1218 overflow: hidden; 1219 1220 list-style: none; 1221 } 1222 1223 .track-row { 1224 min-height: 68px; 1225 1226 display: grid; 1227 1228 grid-template-columns: 1229 55px 1fr auto; 1230 1231 align-items: center; 1232 1233 gap: 15px; 1234 1235 padding: 10px 20px; 1236 1237 border-bottom: 1px solid #eee; 1238 } 1239 1240 .track-row:last-child { 1241 border-bottom: none; 1242 } 1243 1244 .track-number { 1245 color: #999; 1246 1247 font-size: 12px; 1248 font-weight: 800; 1249 } 1250 1251 .track-main strong { 1252 font-size: 13px; 1253 } 1254 1255 .song-artists { 1256 margin-top: 4px; 1257 1258 color: #888; 1259 1260 font-size: 11px; 1261 } 1262 1263 .track-duration { 1264 color: #777; 1265 1266 font-size: 12px; 1267 } 1268 1269 1270 /* ========================================================= 1271 SINGLE 1272 ========================================================= */ 1273 1274 .single-info-card { 1275 display: flex; 1276 align-items: center; 1277 justify-content: space-between; 1278 1279 padding: 25px; 1280 1281 background: white; 1282 1283 border: 1px solid #ddd; 1284 border-radius: 8px; 1285 } 1286 1287 .single-info-card h2 { 1288 margin-top: 5px; 1289 1290 font-size: 22px; 1291 } 1292 1293 .single-duration { 1294 text-align: right; 1295 } 1296 1297 .single-duration span { 1298 display: block; 1299 1300 color: #888; 1301 1302 font-size: 9px; 1303 font-weight: 900; 1304 1305 text-transform: uppercase; 1306 letter-spacing: 1px; 1307 } 1308 1309 .single-duration strong { 1310 display: block; 1311 1312 margin-top: 4px; 1313 1314 font-size: 20px; 1315 } 1316 1317 1318 /* ========================================================= 1319 CART 1320 ========================================================= */ 1321 1322 .cart-page { 1323 max-width: 1250px; 1324 1325 margin: 0 auto; 1326 1327 padding: 55px 30px 80px; 1328 } 1329 1330 .cart-header { 1331 display: flex; 1332 align-items: flex-end; 1333 justify-content: space-between; 1334 1335 margin-bottom: 35px; 1336 } 1337 1338 .cart-header h1 { 1339 margin-top: 7px; 1340 1341 font-size: 44px; 1342 font-weight: 900; 1343 1344 letter-spacing: -1.5px; 1345 } 1346 1347 .cart-header p { 1348 margin-top: 8px; 1349 1350 color: #777; 1351 1352 font-size: 14px; 1353 } 1354 1355 .cart-layout { 1356 display: grid; 1357 1358 grid-template-columns: 1359 1fr 330px; 1360 1361 gap: 25px; 1362 1363 align-items: start; 1364 } 1365 1366 .cart-items { 1367 background: white; 1368 1369 border: 1px solid #ddd; 1370 border-radius: 9px; 1371 1372 overflow: hidden; 1373 } 1374 1375 .cart-items-header { 1376 padding: 16px 20px; 1377 1378 background: #fafafa; 1379 1380 border-bottom: 1px solid #ddd; 1381 1382 color: #777; 1383 1384 font-size: 12px; 1385 font-weight: 800; 1386 } 1387 1388 .cart-item { 1389 min-height: 130px; 1390 1391 display: grid; 1392 1393 grid-template-columns: 1394 90px 1fr auto auto 30px; 1395 1396 align-items: center; 1397 1398 gap: 18px; 1399 1400 padding: 20px; 1401 1402 border-bottom: 1px solid #eee; 1403 } 1404 1405 .cart-item:last-child { 1406 border-bottom: none; 1407 } 1408 1409 .cart-item-cover { 1410 width: 90px; 1411 height: 90px; 1412 1413 display: flex; 1414 align-items: center; 1415 justify-content: center; 1416 1417 overflow: hidden; 1418 1419 background: 1420 linear-gradient( 1421 135deg, 1422 #222, 1423 #555 1424 ); 1425 1426 color: white; 1427 1428 border-radius: 5px; 1429 } 1430 1431 .cart-item-cover img { 1432 width: 100%; 1433 height: 100%; 1434 1435 object-fit: cover; 1436 } 1437 1438 .cart-item-type { 1439 color: #888; 1440 1441 font-size: 9px; 1442 font-weight: 900; 1443 1444 text-transform: uppercase; 1445 letter-spacing: 1px; 1446 } 1447 1448 .cart-item-info h2 { 1449 margin-top: 5px; 1450 1451 font-size: 17px; 1452 font-weight: 800; 1453 } 1454 1455 .cart-item-info h2 a:hover { 1456 text-decoration: underline; 1457 } 1458 1459 .cart-item-price { 1460 margin-top: 6px; 1461 1462 color: #777; 1463 1464 font-size: 11px; 1465 } 1466 1467 .cart-item-quantity form { 1468 display: flex; 1469 align-items: center; 1470 1471 gap: 6px; 1472 } 1473 1474 .cart-item-quantity input { 1475 width: 58px; 1476 height: 36px; 1477 1478 padding: 0 8px; 1479 1480 border: 1px solid #ccc; 1481 border-radius: 5px; 1482 1483 text-align: center; 1484 } 1485 1486 .cart-item-quantity button { 1487 height: 36px; 1488 1489 padding: 0 9px; 1490 1491 border: 1px solid #ccc; 1492 border-radius: 5px; 1493 1494 background: white; 1495 color: #555; 1496 1497 font-size: 10px; 1498 font-weight: 800; 1499 } 1500 1501 .cart-item-quantity button:hover { 1502 background: #eee; 1503 } 1504 1505 .cart-item-total { 1506 min-width: 80px; 1507 1508 text-align: right; 1509 1510 font-size: 15px; 1511 font-weight: 900; 1512 } 1513 1514 .cart-remove { 1515 width: 28px; 1516 height: 28px; 1517 1518 border: none; 1519 background: transparent; 1520 1521 color: #999; 1522 1523 font-size: 24px; 1524 } 1525 1526 .cart-remove:hover { 1527 color: #a33; 1528 } 1529 1530 .cart-summary { 1531 position: sticky; 1532 top: 25px; 1533 1534 padding: 25px; 1535 1536 background: white; 1537 1538 border: 1px solid #ddd; 1539 border-radius: 9px; 1540 1541 box-shadow: 0 8px 25px rgba(0,0,0,.05); 1542 } 1543 1544 .cart-summary h2 { 1545 margin-top: 6px; 1546 1547 font-size: 23px; 1548 } 1549 1550 .summary-line { 1551 display: flex; 1552 justify-content: space-between; 1553 1554 padding: 15px 0; 1555 1556 color: #777; 1557 1558 font-size: 13px; 1559 1560 border-bottom: 1px solid #eee; 1561 } 1562 1563 .summary-line.total { 1564 margin-bottom: 20px; 1565 1566 color: #171717; 1567 1568 font-size: 17px; 1569 font-weight: 900; 1570 } 1571 1572 1573 /* ========================================================= 1574 WISHLIST 1575 ========================================================= */ 1576 1577 .wishlist-page { 1578 max-width: 1250px; 1579 1580 margin: 0 auto; 1581 1582 padding: 55px 30px 80px; 1583 } 1584 1585 .page-header { 1586 margin-bottom: 35px; 1587 } 1588 1589 .page-header h1 { 1590 margin-top: 7px; 1591 1592 font-size: 44px; 1593 font-weight: 900; 1594 1595 letter-spacing: -1.5px; 1596 } 1597 1598 .page-header p { 1599 margin-top: 8px; 1600 1601 color: #777; 1602 1603 font-size: 14px; 1604 } 1605 1606 .wishlist-grid { 1607 display: grid; 1608 1609 grid-template-columns: 1610 repeat(2, minmax(0, 1fr)); 1611 1612 gap: 20px; 1613 } 1614 1615 .wishlist-card { 1616 display: grid; 1617 1618 grid-template-columns: 1619 190px 1fr; 1620 1621 overflow: hidden; 1622 1623 background: white; 1624 1625 border: 1px solid #ddd; 1626 border-radius: 9px; 1627 } 1628 1629 .wishlist-cover { 1630 aspect-ratio: 1 / 1; 1631 1632 overflow: hidden; 1633 1634 background: #ddd; 1635 } 1636 1637 .wishlist-cover img { 1638 width: 100%; 1639 height: 100%; 1640 1641 display: block; 1642 1643 object-fit: cover; 1644 1645 transition: transform .35s ease; 1646 } 1647 1648 .wishlist-card:hover .wishlist-cover img { 1649 transform: scale(1.04); 1650 } 1651 1652 .wishlist-info { 1653 padding: 22px; 1654 } 1655 1656 .wishlist-format { 1657 color: #888; 1658 1659 font-size: 9px; 1660 font-weight: 900; 1661 1662 text-transform: uppercase; 1663 letter-spacing: 1px; 1664 } 1665 1666 .wishlist-info h2 { 1667 margin-top: 7px; 1668 1669 font-size: 20px; 1670 font-weight: 900; 1671 } 1672 1673 .wishlist-info h2 a:hover { 1674 text-decoration: underline; 1675 } 1676 1677 .wishlist-genre { 1678 margin-top: 6px; 1679 1680 color: #777; 1681 1682 font-size: 12px; 1683 } 1684 1685 .wishlist-price { 1686 margin-top: 18px; 1687 1688 font-size: 22px; 1689 font-weight: 900; 1690 } 1691 1692 .stock { 1693 display: inline-block; 1694 1695 margin-top: 7px; 1696 1697 font-size: 10px; 1698 font-weight: 800; 1699 } 1700 1701 .stock.available { 1702 color: #27804d; 1703 } 1704 1705 .stock.unavailable { 1706 color: #a33; 1707 } 1708 1709 .wishlist-actions { 1710 display: flex; 1711 flex-wrap: wrap; 1712 1713 gap: 8px; 1714 1715 margin-top: 20px; 1716 } 1717 1718 .btn-remove { 1719 min-height: 42px; 1720 1721 padding: 0 17px; 1722 1723 border: 1px solid #ccc; 1724 border-radius: 6px; 1725 1726 background: white; 1727 color: #777; 1728 1729 font-size: 12px; 1730 font-weight: 800; 1731 } 1732 1733 .btn-remove:hover { 1734 border-color: #a33; 1735 color: #a33; 1736 } 1737 1738 1739 /* ========================================================= 1740 SALE 1741 ========================================================= */ 1742 1743 .sale-page { 1744 max-width: 1380px; 1745 1746 margin: 0 auto; 1747 1748 padding: 55px 30px 80px; 1749 } 1750 1751 .sale-header { 1752 margin-bottom: 40px; 1753 } 1754 1755 .sale-header h1 { 1756 margin-top: 8px; 1757 1758 font-size: 48px; 1759 font-weight: 900; 1760 1761 letter-spacing: -2px; 1762 } 1763 1764 .sale-header p { 1765 margin-top: 8px; 1766 1767 color: #777; 1768 } 1769 1770 .sale-grid { 1771 display: grid; 1772 1773 grid-template-columns: 1774 repeat(4, minmax(0, 1fr)); 1775 1776 gap: 24px; 1777 } 1778 1779 .sale-card { 1780 overflow: hidden; 1781 1782 background: white; 1783 1784 border: 1px solid #ddd; 1785 border-radius: 10px; 1786 1787 transition: 1788 transform .25s ease, 1789 box-shadow .25s ease; 1790 } 1791 1792 .sale-card:hover { 1793 transform: translateY(-5px); 1794 1795 box-shadow: 1796 0 18px 40px rgba(0,0,0,.10); 1797 } 1798 1799 .sale-card-cover { 16 1800 position: relative; 17 min-height: 100%; 18 } 19 20 body { 21 margin-bottom: 60px; 22 } 1801 1802 display: block; 1803 1804 aspect-ratio: 1 / 1; 1805 1806 overflow: hidden; 1807 1808 background: #ddd; 1809 } 1810 1811 .sale-card-cover img { 1812 width: 100%; 1813 height: 100%; 1814 1815 object-fit: cover; 1816 1817 transition: transform .4s ease; 1818 } 1819 1820 .sale-card:hover .sale-card-cover img { 1821 transform: scale(1.04); 1822 } 1823 1824 .sale-badge { 1825 position: absolute; 1826 1827 top: 12px; 1828 right: 12px; 1829 1830 padding: 7px 9px; 1831 1832 background: #171717; 1833 color: white; 1834 1835 border-radius: 4px; 1836 1837 font-size: 10px; 1838 font-weight: 900; 1839 } 1840 1841 .sale-card-body { 1842 padding: 20px; 1843 } 1844 1845 .sale-card-body h2 { 1846 margin-top: 7px; 1847 1848 font-size: 20px; 1849 font-weight: 900; 1850 } 1851 1852 .sale-card-body h2 a:hover { 1853 text-decoration: underline; 1854 } 1855 1856 .sale-price-row { 1857 display: flex; 1858 align-items: baseline; 1859 gap: 9px; 1860 1861 margin-top: 18px; 1862 } 1863 1864 .sale-old-price { 1865 color: #999; 1866 1867 font-size: 13px; 1868 1869 text-decoration: line-through; 1870 } 1871 1872 .sale-price { 1873 font-size: 25px; 1874 font-weight: 900; 1875 } 1876 1877 .sale-actions { 1878 display: grid; 1879 1880 gap: 8px; 1881 1882 margin-top: 20px; 1883 } 1884 1885 1886 /* ========================================================= 1887 TOP SELLERS 1888 ========================================================= */ 1889 1890 .top-sellers-page { 1891 max-width: 1380px; 1892 1893 margin: 0 auto; 1894 1895 padding: 55px 30px 80px; 1896 } 1897 1898 .top-sellers-header { 1899 margin-bottom: 40px; 1900 } 1901 1902 .top-sellers-header h1 { 1903 margin-top: 8px; 1904 1905 font-size: 48px; 1906 font-weight: 900; 1907 1908 letter-spacing: -2px; 1909 } 1910 1911 .top-sellers-header p { 1912 margin-top: 8px; 1913 1914 color: #777; 1915 } 1916 1917 1918 /* ========================================================= 1919 HOME 1920 ========================================================= */ 1921 1922 .home-page { 1923 max-width: 1400px; 1924 1925 margin: 0 auto; 1926 1927 padding: 35px 30px 80px; 1928 } 1929 1930 .home-hero { 1931 min-height: 540px; 1932 1933 position: relative; 1934 1935 display: flex; 1936 align-items: center; 1937 1938 padding: 75px; 1939 1940 overflow: hidden; 1941 1942 background: #111; 1943 color: white; 1944 1945 border-radius: 16px; 1946 } 1947 1948 .home-hero-content { 1949 position: relative; 1950 z-index: 2; 1951 1952 max-width: 680px; 1953 } 1954 1955 .home-eyebrow { 1956 color: #aaa; 1957 1958 font-size: 11px; 1959 font-weight: 900; 1960 1961 letter-spacing: 3px; 1962 } 1963 1964 .home-hero h1 { 1965 margin-top: 16px; 1966 1967 font-size: clamp(48px, 6vw, 82px); 1968 1969 line-height: .93; 1970 1971 font-weight: 900; 1972 1973 letter-spacing: -4px; 1974 } 1975 1976 .home-hero h1 span { 1977 color: #777; 1978 } 1979 1980 .home-hero p { 1981 max-width: 540px; 1982 1983 margin-top: 25px; 1984 1985 color: #bbb; 1986 1987 font-size: 17px; 1988 line-height: 1.7; 1989 } 1990 1991 .home-hero-actions { 1992 display: flex; 1993 gap: 10px; 1994 1995 margin-top: 30px; 1996 } 1997 1998 .home-button { 1999 min-height: 46px; 2000 2001 display: inline-flex; 2002 align-items: center; 2003 justify-content: center; 2004 2005 padding: 0 21px; 2006 2007 border-radius: 6px; 2008 2009 font-size: 12px; 2010 font-weight: 900; 2011 } 2012 2013 .home-button.primary { 2014 background: white; 2015 color: #111; 2016 } 2017 2018 .home-button.primary:hover { 2019 background: #ddd; 2020 } 2021 2022 .home-button.secondary { 2023 border: 1px solid #555; 2024 color: white; 2025 } 2026 2027 .home-button.secondary:hover { 2028 background: #292929; 2029 } 2030 2031 .home-hero-mark { 2032 position: absolute; 2033 2034 right: 5%; 2035 bottom: -60px; 2036 2037 color: #1e1e1e; 2038 2039 font-size: 420px; 2040 font-weight: 900; 2041 2042 line-height: 1; 2043 } 2044 2045 .home-section { 2046 margin-top: 70px; 2047 } 2048 2049 .home-section-header { 2050 margin-bottom: 25px; 2051 } 2052 2053 .home-section-header h2 { 2054 margin-top: 7px; 2055 2056 font-size: 32px; 2057 font-weight: 900; 2058 } 2059 2060 .home-feature-grid { 2061 display: grid; 2062 2063 grid-template-columns: 2064 repeat(3, 1fr); 2065 2066 gap: 18px; 2067 } 2068 2069 .home-feature-card { 2070 min-height: 220px; 2071 2072 padding: 27px; 2073 2074 background: white; 2075 2076 border: 1px solid #ddd; 2077 border-radius: 10px; 2078 2079 transition: 2080 transform .2s ease, 2081 box-shadow .2s ease; 2082 } 2083 2084 .home-feature-card:hover { 2085 transform: translateY(-4px); 2086 2087 box-shadow: 2088 0 15px 35px rgba(0,0,0,.08); 2089 } 2090 2091 .home-feature-card > span { 2092 color: #999; 2093 2094 font-size: 10px; 2095 font-weight: 900; 2096 } 2097 2098 .home-feature-card h3 { 2099 margin-top: 35px; 2100 2101 font-size: 23px; 2102 font-weight: 900; 2103 } 2104 2105 .home-feature-card p { 2106 margin-top: 8px; 2107 2108 color: #777; 2109 2110 font-size: 13px; 2111 } 2112 2113 .home-feature-card strong { 2114 display: block; 2115 2116 margin-top: 25px; 2117 2118 font-size: 12px; 2119 } 2120 2121 .home-feature-card.sale-feature { 2122 background: #171717; 2123 color: white; 2124 } 2125 2126 .home-feature-card.sale-feature p { 2127 color: #aaa; 2128 } 2129 2130 .home-collection { 2131 margin-top: 70px; 2132 padding: 40px; 2133 2134 display: flex; 2135 justify-content: space-between; 2136 gap: 40px; 2137 2138 background: #e9e9e7; 2139 2140 border-radius: 10px; 2141 } 2142 2143 .home-collection h2 { 2144 margin-top: 7px; 2145 2146 font-size: 32px; 2147 font-weight: 900; 2148 } 2149 2150 .home-collection p { 2151 max-width: 450px; 2152 2153 color: #666; 2154 2155 line-height: 1.7; 2156 } 2157 2158 2159 /* ========================================================= 2160 PROFILE 2161 ========================================================= */ 2162 2163 .profile-page { 2164 max-width: 1100px; 2165 2166 margin: 0 auto; 2167 2168 padding: 55px 30px 80px; 2169 } 2170 2171 .profile-header { 2172 margin-bottom: 35px; 2173 } 2174 2175 .profile-header h1 { 2176 margin-top: 8px; 2177 2178 font-size: 44px; 2179 font-weight: 900; 2180 2181 letter-spacing: -1.5px; 2182 } 2183 2184 .profile-header p { 2185 margin-top: 8px; 2186 2187 color: #777; 2188 } 2189 2190 .profile-layout { 2191 display: grid; 2192 2193 grid-template-columns: 2194 280px 1fr; 2195 2196 gap: 25px; 2197 2198 align-items: start; 2199 } 2200 2201 .profile-sidebar, 2202 .profile-card { 2203 background: white; 2204 2205 border: 1px solid #ddd; 2206 border-radius: 10px; 2207 } 2208 2209 .profile-sidebar { 2210 padding: 30px; 2211 2212 text-align: center; 2213 } 2214 2215 .profile-avatar { 2216 width: 80px; 2217 height: 80px; 2218 2219 margin: 0 auto 18px; 2220 2221 display: flex; 2222 align-items: center; 2223 justify-content: center; 2224 2225 border-radius: 50%; 2226 2227 background: #171717; 2228 color: white; 2229 2230 font-size: 32px; 2231 font-weight: 900; 2232 } 2233 2234 .profile-sidebar h2 { 2235 font-size: 21px; 2236 } 2237 2238 .profile-sidebar > p { 2239 margin-top: 5px; 2240 2241 color: #777; 2242 2243 font-size: 12px; 2244 2245 word-break: break-word; 2246 } 2247 2248 .profile-member, 2249 .profile-points { 2250 margin-top: 28px; 2251 padding-top: 20px; 2252 2253 border-top: 1px solid #eee; 2254 } 2255 2256 .profile-member span, 2257 .profile-points span { 2258 display: block; 2259 2260 color: #888; 2261 2262 font-size: 9px; 2263 font-weight: 900; 2264 2265 letter-spacing: 1px; 2266 } 2267 2268 .profile-member strong, 2269 .profile-points strong { 2270 display: block; 2271 2272 margin-top: 6px; 2273 2274 font-size: 14px; 2275 } 2276 2277 .profile-points strong { 2278 font-size: 25px; 2279 } 2280 2281 .profile-card { 2282 padding: 30px; 2283 } 2284 2285 .profile-card-header { 2286 padding-bottom: 20px; 2287 2288 border-bottom: 1px solid #eee; 2289 } 2290 2291 .profile-card-header h2 { 2292 margin-top: 6px; 2293 2294 font-size: 24px; 2295 } 2296 2297 .profile-form { 2298 padding-top: 25px; 2299 } 2300 2301 .profile-field { 2302 margin-bottom: 20px; 2303 } 2304 2305 .profile-field label { 2306 display: block; 2307 2308 margin-bottom: 7px; 2309 2310 color: #555; 2311 2312 font-size: 10px; 2313 font-weight: 900; 2314 2315 text-transform: uppercase; 2316 letter-spacing: 1px; 2317 } 2318 2319 .profile-field input, 2320 .profile-field textarea { 2321 width: 100%; 2322 2323 padding: 12px 13px; 2324 2325 border: 1px solid #d4d4d4; 2326 border-radius: 6px; 2327 2328 outline: none; 2329 2330 background: white; 2331 color: #171717; 2332 2333 font-size: 13px; 2334 } 2335 2336 .profile-field textarea { 2337 resize: vertical; 2338 } 2339 2340 .profile-field input:focus, 2341 .profile-field textarea:focus { 2342 border-color: #777; 2343 } 2344 2345 .profile-field input[readonly] { 2346 background: #f3f3f3; 2347 color: #777; 2348 } 2349 2350 .profile-field small { 2351 display: block; 2352 2353 margin-top: 5px; 2354 2355 color: #999; 2356 2357 font-size: 10px; 2358 } 2359 2360 .field-error, 2361 .validation-summary { 2362 color: #a33; 2363 2364 font-size: 11px; 2365 } 2366 2367 .profile-form-actions { 2368 display: flex; 2369 gap: 8px; 2370 2371 margin-top: 25px; 2372 } 2373 2374 .profile-alert { 2375 margin-bottom: 20px; 2376 2377 padding: 14px 16px; 2378 2379 border-radius: 6px; 2380 2381 font-size: 12px; 2382 font-weight: 700; 2383 } 2384 2385 .profile-alert.success { 2386 background: #e8f5ed; 2387 color: #28734a; 2388 2389 border: 1px solid #c9e6d4; 2390 } 2391 2392 2393 /* ========================================================= 2394 FOOTER 2395 ========================================================= */ 2396 2397 .site-footer { 2398 margin-top: 80px; 2399 2400 background: #111; 2401 color: #aaa; 2402 } 2403 2404 .footer-container { 2405 max-width: 1200px; 2406 2407 margin: 0 auto; 2408 2409 padding: 55px 30px; 2410 2411 display: grid; 2412 2413 grid-template-columns: 2414 2fr 1fr 1fr 1fr; 2415 2416 gap: 50px; 2417 } 2418 2419 .footer-logo { 2420 color: white; 2421 2422 font-size: 20px; 2423 font-weight: 900; 2424 2425 letter-spacing: 2px; 2426 } 2427 2428 .footer-brand p { 2429 max-width: 260px; 2430 2431 margin-top: 13px; 2432 2433 color: #777; 2434 2435 font-size: 13px; 2436 line-height: 1.7; 2437 } 2438 2439 .footer-column { 2440 display: flex; 2441 flex-direction: column; 2442 2443 gap: 10px; 2444 } 2445 2446 .footer-column h4 { 2447 margin-bottom: 8px; 2448 2449 color: white; 2450 2451 font-size: 11px; 2452 font-weight: 900; 2453 2454 text-transform: uppercase; 2455 letter-spacing: 1.3px; 2456 } 2457 2458 .footer-column a { 2459 width: fit-content; 2460 2461 color: #777; 2462 2463 font-size: 12px; 2464 } 2465 2466 .footer-column a:hover { 2467 color: white; 2468 } 2469 2470 .footer-bottom { 2471 max-width: 1200px; 2472 2473 margin: auto; 2474 padding: 18px 30px; 2475 2476 border-top: 1px solid #292929; 2477 2478 display: flex; 2479 justify-content: space-between; 2480 2481 color: #666; 2482 2483 font-size: 10px; 2484 } 2485 2486 2487 /* ========================================================= 2488 RESPONSIVE 2489 ========================================================= */ 2490 2491 .mobile-menu-button { 2492 display: none; 2493 } 2494 2495 @media (max-width: 1200px) { 2496 2497 .navbar-container { 2498 gap: 20px; 2499 } 2500 2501 .nav-links { 2502 gap: 15px; 2503 } 2504 2505 .icon-label { 2506 display: none; 2507 } 2508 2509 .release-grid, 2510 .sale-grid { 2511 grid-template-columns: 2512 repeat(3, minmax(0, 1fr)); 2513 } 2514 2515 .release-filters { 2516 grid-template-columns: 2517 2fr 1fr 1fr 1fr; 2518 } 2519 2520 .filter-search { 2521 grid-column: 1 / -1; 2522 } 2523 2524 .filter-button { 2525 grid-column: 1 / -1; 2526 } 2527 } 2528 2529 2530 @media (max-width: 1000px) { 2531 2532 .release-grid, 2533 .sale-grid { 2534 grid-template-columns: 2535 repeat(2, minmax(0, 1fr)); 2536 } 2537 2538 .wishlist-grid { 2539 grid-template-columns: 1fr; 2540 } 2541 2542 .release-hero { 2543 grid-template-columns: 2544 1fr 1fr; 2545 2546 gap: 30px; 2547 } 2548 2549 .product-grid { 2550 grid-template-columns: 2551 repeat(2, minmax(0, 1fr)); 2552 } 2553 2554 .cart-layout { 2555 grid-template-columns: 1fr; 2556 } 2557 2558 .cart-summary { 2559 position: static; 2560 } 2561 2562 .profile-layout { 2563 grid-template-columns: 1fr; 2564 } 2565 2566 .profile-sidebar { 2567 text-align: left; 2568 } 2569 2570 .profile-avatar { 2571 margin-left: 0; 2572 } 2573 2574 .home-feature-grid { 2575 grid-template-columns: 1fr 1fr; 2576 } 2577 } 2578 2579 2580 @media (max-width: 950px) { 2581 2582 .navbar-container { 2583 height: 70px; 2584 } 2585 2586 .mobile-menu-button { 2587 display: block; 2588 2589 margin-left: auto; 2590 2591 width: 42px; 2592 height: 42px; 2593 2594 border: 1px solid #444; 2595 border-radius: 6px; 2596 2597 background: #222; 2598 color: white; 2599 2600 font-size: 21px; 2601 } 2602 2603 .navbar-content { 2604 display: none; 2605 2606 position: absolute; 2607 2608 top: 70px; 2609 left: 0; 2610 right: 0; 2611 2612 padding: 20px 25px 25px; 2613 2614 background: #111; 2615 2616 border-top: 1px solid #292929; 2617 2618 flex-direction: column; 2619 align-items: stretch; 2620 2621 gap: 20px; 2622 } 2623 2624 .navbar-content.mobile-open { 2625 display: flex; 2626 } 2627 2628 .nav-links { 2629 flex-direction: column; 2630 align-items: stretch; 2631 2632 gap: 0; 2633 } 2634 2635 .nav-links li { 2636 border-bottom: 1px solid #292929; 2637 } 2638 2639 .nav-links a { 2640 display: block; 2641 2642 padding: 13px 0; 2643 } 2644 2645 .nav-links > li > a::after { 2646 display: none; 2647 } 2648 2649 .dropdown-menu { 2650 position: static; 2651 2652 width: 100%; 2653 2654 display: none; 2655 2656 opacity: 1; 2657 visibility: visible; 2658 2659 transform: none; 2660 2661 border: none; 2662 2663 box-shadow: none; 2664 2665 background: #181818; 2666 } 2667 2668 .nav-dropdown:hover .dropdown-menu { 2669 display: block; 2670 } 2671 2672 .navbar-search { 2673 width: 100%; 2674 max-width: none; 2675 2676 margin: 0; 2677 } 2678 2679 .navbar-actions { 2680 width: 100%; 2681 2682 justify-content: flex-start; 2683 } 2684 2685 .icon-label { 2686 display: inline; 2687 } 2688 2689 .auth-buttons { 2690 margin-left: auto; 2691 } 2692 } 2693 2694 2695 @media (max-width: 768px) { 2696 2697 .releases-page, 2698 .release-details-page, 2699 .cart-page, 2700 .wishlist-page, 2701 .sale-page, 2702 .top-sellers-page, 2703 .profile-page, 2704 .home-page { 2705 padding: 35px 18px 60px; 2706 } 2707 2708 .releases-header, 2709 .cart-header { 2710 align-items: flex-start; 2711 2712 flex-direction: column; 2713 2714 gap: 18px; 2715 } 2716 2717 .releases-header h1, 2718 .cart-header h1, 2719 .page-header h1, 2720 .sale-header h1, 2721 .top-sellers-header h1, 2722 .profile-header h1 { 2723 font-size: 36px; 2724 } 2725 2726 .release-filters { 2727 grid-template-columns: 1fr; 2728 } 2729 2730 .filter-search, 2731 .filter-button, 2732 .clear-filter { 2733 grid-column: auto; 2734 } 2735 2736 .release-grid, 2737 .sale-grid { 2738 grid-template-columns: 1fr; 2739 } 2740 2741 .release-hero { 2742 grid-template-columns: 1fr; 2743 2744 gap: 30px; 2745 } 2746 2747 .release-hero-cover { 2748 max-width: 550px; 2749 2750 margin: 0 auto; 2751 } 2752 2753 .release-hero-info h1 { 2754 font-size: 43px; 2755 } 2756 2757 .release-facts { 2758 grid-template-columns: 2759 1fr 1fr; 2760 } 2761 2762 .product-grid { 2763 grid-template-columns: 1fr; 2764 } 2765 2766 .product-actions { 2767 grid-template-columns: 1fr; 2768 } 2769 2770 .track-row { 2771 grid-template-columns: 2772 35px 1fr; 2773 2774 padding: 14px; 2775 } 2776 2777 .track-duration { 2778 grid-column: 2; 2779 } 2780 2781 .cart-item { 2782 grid-template-columns: 2783 70px 1fr 30px; 2784 2785 gap: 12px; 2786 } 2787 2788 .cart-item-cover { 2789 width: 70px; 2790 height: 70px; 2791 } 2792 2793 .cart-item-quantity, 2794 .cart-item-total { 2795 grid-column: 2; 2796 } 2797 2798 .cart-item-total { 2799 text-align: left; 2800 } 2801 2802 .cart-remove { 2803 grid-column: 3; 2804 grid-row: 1; 2805 } 2806 2807 .wishlist-grid { 2808 grid-template-columns: 1fr; 2809 } 2810 2811 .wishlist-card { 2812 grid-template-columns: 2813 120px 1fr; 2814 } 2815 2816 .home-hero { 2817 min-height: 500px; 2818 2819 padding: 45px 30px; 2820 } 2821 2822 .home-hero-mark { 2823 font-size: 250px; 2824 } 2825 2826 .home-feature-grid { 2827 grid-template-columns: 1fr; 2828 } 2829 2830 .home-collection { 2831 flex-direction: column; 2832 } 2833 2834 .profile-sidebar { 2835 text-align: center; 2836 } 2837 2838 .profile-avatar { 2839 margin-left: auto; 2840 } 2841 2842 .footer-container { 2843 grid-template-columns: 2844 1fr 1fr; 2845 2846 gap: 35px; 2847 2848 padding: 40px 25px; 2849 } 2850 2851 .footer-brand { 2852 grid-column: 1 / -1; 2853 } 2854 2855 .footer-bottom { 2856 padding: 18px 25px; 2857 2858 flex-direction: column; 2859 2860 gap: 5px; 2861 } 2862 } 2863 2864 2865 @media (max-width: 500px) { 2866 2867 .wishlist-card { 2868 grid-template-columns: 1fr; 2869 } 2870 2871 .wishlist-cover { 2872 max-height: 300px; 2873 } 2874 2875 .wishlist-actions { 2876 flex-direction: column; 2877 } 2878 2879 .wishlist-actions .store-button, 2880 .wishlist-actions .btn-remove { 2881 width: 100%; 2882 } 2883 2884 .home-hero { 2885 padding: 35px 22px; 2886 } 2887 2888 .home-hero h1 { 2889 font-size: 45px; 2890 } 2891 2892 .home-hero-actions { 2893 flex-direction: column; 2894 } 2895 2896 .home-button { 2897 width: 100%; 2898 } 2899 2900 .profile-card { 2901 padding: 20px; 2902 } 2903 2904 .profile-form-actions { 2905 flex-direction: column; 2906 } 2907 2908 .profile-form-actions .store-button { 2909 width: 100%; 2910 } 2911 } 2912 2913 /* ========================================================= 2914 ADMIN 2915 ========================================================= */ 2916 2917 .admin-page { 2918 max-width: 1250px; 2919 2920 margin: 0 auto; 2921 2922 padding: 55px 30px 80px; 2923 } 2924 2925 2926 /* ========================================================= 2927 ADMIN HEADER 2928 ========================================================= */ 2929 2930 .admin-header { 2931 display: flex; 2932 align-items: flex-end; 2933 justify-content: space-between; 2934 2935 gap: 30px; 2936 2937 margin-bottom: 35px; 2938 } 2939 2940 .admin-eyebrow { 2941 display: block; 2942 2943 margin-bottom: 7px; 2944 2945 color: #888; 2946 2947 font-size: 9px; 2948 font-weight: 900; 2949 2950 letter-spacing: 1.7px; 2951 text-transform: uppercase; 2952 } 2953 2954 .admin-header h1 { 2955 font-size: 44px; 2956 font-weight: 900; 2957 2958 letter-spacing: -1.5px; 2959 } 2960 2961 .admin-header p { 2962 margin-top: 8px; 2963 2964 color: #777; 2965 2966 font-size: 14px; 2967 } 2968 2969 .admin-back-link { 2970 display: block; 2971 2972 margin-bottom: 22px; 2973 2974 color: #777; 2975 2976 font-size: 12px; 2977 font-weight: 700; 2978 } 2979 2980 .admin-back-link:hover { 2981 color: #111; 2982 } 2983 2984 2985 /* ========================================================= 2986 ADMIN BUTTONS 2987 ========================================================= */ 2988 2989 .admin-button { 2990 min-height: 41px; 2991 2992 display: inline-flex; 2993 align-items: center; 2994 justify-content: center; 2995 2996 gap: 7px; 2997 2998 padding: 0 17px; 2999 3000 border-radius: 6px; 3001 3002 font-size: 12px; 3003 font-weight: 800; 3004 3005 transition: 3006 background .2s ease, 3007 border-color .2s ease, 3008 color .2s ease, 3009 transform .2s ease; 3010 } 3011 3012 .admin-button:hover { 3013 transform: translateY(-1px); 3014 } 3015 3016 .admin-button.primary { 3017 border: 1px solid #171717; 3018 3019 background: #171717; 3020 color: white; 3021 } 3022 3023 .admin-button.primary:hover { 3024 background: #333; 3025 } 3026 3027 .admin-button.secondary { 3028 border: 1px solid #d4d4d4; 3029 3030 background: white; 3031 color: #333; 3032 } 3033 3034 .admin-button.secondary:hover { 3035 background: #eee; 3036 } 3037 3038 .admin-button.small { 3039 min-height: 34px; 3040 3041 padding: 0 13px; 3042 3043 font-size: 10px; 3044 } 3045 3046 3047 /* ========================================================= 3048 ALERT 3049 ========================================================= */ 3050 3051 .admin-alert { 3052 display: flex; 3053 align-items: center; 3054 3055 gap: 9px; 3056 3057 margin-bottom: 25px; 3058 3059 padding: 14px 17px; 3060 3061 border-radius: 7px; 3062 3063 font-size: 12px; 3064 font-weight: 700; 3065 } 3066 3067 .admin-alert.success { 3068 border: 1px solid #c9e6d3; 3069 3070 background: #ecf8f0; 3071 3072 color: #287348; 3073 } 3074 3075 3076 /* ========================================================= 3077 DASHBOARD STATS 3078 ========================================================= */ 3079 3080 .admin-stat-grid { 3081 display: grid; 3082 3083 grid-template-columns: 3084 repeat(4, minmax(0, 1fr)); 3085 3086 gap: 16px; 3087 3088 margin-bottom: 55px; 3089 } 3090 3091 .admin-stat-card { 3092 min-height: 150px; 3093 3094 padding: 22px; 3095 3096 background: white; 3097 3098 border: 1px solid #ddd; 3099 border-radius: 9px; 3100 } 3101 3102 .admin-stat-label { 3103 display: block; 3104 3105 color: #888; 3106 3107 font-size: 9px; 3108 font-weight: 900; 3109 3110 letter-spacing: 1.2px; 3111 } 3112 3113 .admin-stat-card > strong { 3114 display: block; 3115 3116 margin-top: 13px; 3117 3118 font-size: 34px; 3119 font-weight: 900; 3120 } 3121 3122 .admin-stat-card p { 3123 margin-top: 7px; 3124 3125 color: #888; 3126 3127 font-size: 11px; 3128 line-height: 1.5; 3129 } 3130 3131 .admin-stat-card.warning { 3132 border-top: 3px solid #c88a27; 3133 } 3134 3135 .admin-stat-card.danger { 3136 border-top: 3px solid #ad4545; 3137 } 3138 3139 3140 /* ========================================================= 3141 DASHBOARD ACTIONS 3142 ========================================================= */ 3143 3144 .admin-section { 3145 margin-top: 20px; 3146 } 3147 3148 .admin-section-heading { 3149 margin-bottom: 20px; 3150 3151 padding-bottom: 15px; 3152 3153 border-bottom: 1px solid #ddd; 3154 } 3155 3156 .admin-section-heading h2 { 3157 margin-top: 5px; 3158 3159 font-size: 27px; 3160 font-weight: 900; 3161 } 3162 3163 .admin-action-grid { 3164 display: grid; 3165 3166 grid-template-columns: 3167 repeat(2, minmax(0, 1fr)); 3168 3169 gap: 16px; 3170 } 3171 3172 .admin-action-card { 3173 min-height: 170px; 3174 3175 display: grid; 3176 3177 grid-template-columns: 3178 55px 1fr 25px; 3179 3180 align-items: center; 3181 3182 gap: 18px; 3183 3184 padding: 25px; 3185 3186 background: white; 3187 3188 border: 1px solid #ddd; 3189 border-radius: 9px; 3190 3191 transition: 3192 transform .2s ease, 3193 box-shadow .2s ease; 3194 } 3195 3196 .admin-action-card:hover { 3197 transform: translateY(-3px); 3198 3199 box-shadow: 3200 0 12px 30px rgba(0,0,0,.07); 3201 } 3202 3203 .admin-action-icon { 3204 width: 48px; 3205 height: 48px; 3206 3207 display: flex; 3208 align-items: center; 3209 justify-content: center; 3210 3211 border-radius: 7px; 3212 3213 background: #171717; 3214 color: white; 3215 3216 font-size: 21px; 3217 } 3218 3219 .admin-action-card h3 { 3220 font-size: 18px; 3221 font-weight: 900; 3222 } 3223 3224 .admin-action-card p { 3225 margin-top: 6px; 3226 3227 color: #777; 3228 3229 font-size: 12px; 3230 line-height: 1.5; 3231 } 3232 3233 .admin-action-arrow { 3234 color: #999; 3235 3236 font-size: 18px; 3237 } 3238 3239 3240 /* ========================================================= 3241 PRODUCT TABLE 3242 ========================================================= */ 3243 3244 .admin-list-summary { 3245 display: flex; 3246 3247 gap: 20px; 3248 3249 margin-bottom: 13px; 3250 3251 color: #777; 3252 3253 font-size: 11px; 3254 font-weight: 700; 3255 } 3256 3257 .admin-table-wrapper { 3258 overflow-x: auto; 3259 3260 background: white; 3261 3262 border: 1px solid #ddd; 3263 border-radius: 9px; 3264 } 3265 3266 .admin-table { 3267 width: 100%; 3268 3269 border-collapse: collapse; 3270 } 3271 3272 .admin-table th { 3273 padding: 14px 18px; 3274 3275 background: #fafafa; 3276 3277 border-bottom: 1px solid #ddd; 3278 3279 color: #777; 3280 3281 font-size: 9px; 3282 font-weight: 900; 3283 3284 text-align: left; 3285 text-transform: uppercase; 3286 letter-spacing: 1px; 3287 } 3288 3289 .admin-table td { 3290 padding: 14px 18px; 3291 3292 border-bottom: 1px solid #eee; 3293 3294 font-size: 12px; 3295 3296 vertical-align: middle; 3297 } 3298 3299 .admin-table tbody tr:last-child td { 3300 border-bottom: none; 3301 } 3302 3303 .admin-table tbody tr:hover { 3304 background: #fcfcfc; 3305 } 3306 3307 .admin-product-release { 3308 display: flex; 3309 align-items: center; 3310 3311 gap: 13px; 3312 } 3313 3314 .admin-product-cover { 3315 width: 48px; 3316 height: 48px; 3317 3318 flex-shrink: 0; 3319 3320 display: flex; 3321 align-items: center; 3322 justify-content: center; 3323 3324 overflow: hidden; 3325 3326 border-radius: 4px; 3327 3328 background: #eee; 3329 } 3330 3331 .admin-product-cover img { 3332 width: 100%; 3333 height: 100%; 3334 3335 object-fit: cover; 3336 } 3337 3338 .admin-product-release strong { 3339 display: block; 3340 3341 font-size: 12px; 3342 } 3343 3344 .admin-product-release small { 3345 display: block; 3346 3347 margin-top: 3px; 3348 3349 color: #999; 3350 3351 font-size: 9px; 3352 } 3353 3354 .admin-format-badge { 3355 display: inline-flex; 3356 3357 padding: 5px 8px; 3358 3359 border-radius: 4px; 3360 3361 background: #eee; 3362 3363 color: #555; 3364 3365 font-size: 9px; 3366 font-weight: 900; 3367 } 3368 3369 .admin-price { 3370 font-weight: 800; 3371 } 3372 3373 .admin-stock { 3374 display: inline-flex; 3375 3376 padding: 5px 8px; 3377 3378 border-radius: 20px; 3379 3380 font-size: 9px; 3381 font-weight: 800; 3382 } 3383 3384 .admin-stock.success { 3385 background: #e7f5ec; 3386 color: #287148; 3387 } 3388 3389 .admin-stock.warning { 3390 background: #fbf1df; 3391 color: #976317; 3392 } 3393 3394 .admin-stock.danger { 3395 background: #fae8e8; 3396 color: #983b3b; 3397 } 3398 3399 .admin-table-action { 3400 text-align: right; 3401 } 3402 3403 3404 /* ========================================================= 3405 FORMS 3406 ========================================================= */ 3407 3408 .admin-form-layout { 3409 display: grid; 3410 3411 grid-template-columns: 3412 minmax(0, 1fr) 3413 300px; 3414 3415 gap: 22px; 3416 3417 align-items: start; 3418 } 3419 3420 .admin-form-card, 3421 .admin-help-card, 3422 .admin-product-summary { 3423 background: white; 3424 3425 border: 1px solid #ddd; 3426 border-radius: 9px; 3427 } 3428 3429 .admin-form-card { 3430 padding: 30px; 3431 } 3432 3433 .admin-form-card-header { 3434 margin-bottom: 27px; 3435 3436 padding-bottom: 19px; 3437 3438 border-bottom: 1px solid #eee; 3439 } 3440 3441 .admin-form-card-header h2 { 3442 margin-top: 5px; 3443 3444 font-size: 23px; 3445 font-weight: 900; 3446 } 3447 3448 .admin-form-row { 3449 display: grid; 3450 3451 grid-template-columns: 3452 1fr 1fr; 3453 3454 gap: 16px; 3455 } 3456 3457 .admin-form-field { 3458 margin-bottom: 20px; 3459 } 3460 3461 .admin-form-field label { 3462 display: block; 3463 3464 margin-bottom: 7px; 3465 3466 color: #555; 3467 3468 font-size: 10px; 3469 font-weight: 900; 3470 3471 text-transform: uppercase; 3472 letter-spacing: .8px; 3473 } 3474 3475 .admin-form-field input, 3476 .admin-form-field select, 3477 .admin-form-field textarea { 3478 width: 100%; 3479 3480 padding: 11px 12px; 3481 3482 border: 1px solid #d4d4d4; 3483 border-radius: 6px; 3484 3485 outline: none; 3486 3487 background: white; 3488 color: #171717; 3489 3490 font-size: 12px; 3491 } 3492 3493 .admin-form-field select, 3494 .admin-form-field input { 3495 min-height: 42px; 3496 } 3497 3498 .admin-form-field textarea { 3499 resize: vertical; 3500 3501 line-height: 1.6; 3502 } 3503 3504 .admin-form-field input:focus, 3505 .admin-form-field select:focus, 3506 .admin-form-field textarea:focus { 3507 border-color: #777; 3508 } 3509 3510 .admin-form-field small { 3511 display: block; 3512 3513 margin-top: 6px; 3514 3515 color: #999; 3516 3517 font-size: 10px; 3518 line-height: 1.5; 3519 } 3520 3521 .admin-input-prefix, 3522 .admin-input-suffix { 3523 display: flex; 3524 3525 overflow: hidden; 3526 3527 border: 1px solid #d4d4d4; 3528 border-radius: 6px; 3529 } 3530 3531 .admin-input-prefix:focus-within, 3532 .admin-input-suffix:focus-within { 3533 border-color: #777; 3534 } 3535 3536 .admin-input-prefix > span, 3537 .admin-input-suffix > span { 3538 min-width: 42px; 3539 3540 display: flex; 3541 align-items: center; 3542 justify-content: center; 3543 3544 background: #f5f5f5; 3545 3546 color: #777; 3547 3548 font-size: 12px; 3549 font-weight: 800; 3550 } 3551 3552 .admin-input-prefix input, 3553 .admin-input-suffix input { 3554 border: none; 3555 border-radius: 0; 3556 } 3557 3558 .admin-field-error, 3559 .admin-validation { 3560 display: block; 3561 3562 margin-top: 5px; 3563 3564 color: #a33; 3565 3566 font-size: 10px; 3567 } 3568 3569 .admin-validation { 3570 margin-bottom: 18px; 3571 } 3572 3573 .admin-form-actions { 3574 display: flex; 3575 3576 gap: 8px; 3577 3578 margin-top: 28px; 3579 3580 padding-top: 22px; 3581 3582 border-top: 1px solid #eee; 3583 } 3584 3585 3586 /* ========================================================= 3587 HELP CARD 3588 ========================================================= */ 3589 3590 .admin-help-card { 3591 padding: 25px; 3592 } 3593 3594 .admin-help-card h3 { 3595 margin-top: 6px; 3596 margin-bottom: 22px; 3597 3598 font-size: 18px; 3599 } 3600 3601 .admin-help-item { 3602 display: grid; 3603 3604 grid-template-columns: 3605 30px 1fr; 3606 3607 gap: 11px; 3608 3609 padding: 15px 0; 3610 3611 border-top: 1px solid #eee; 3612 } 3613 3614 .admin-help-item strong { 3615 color: #999; 3616 3617 font-size: 10px; 3618 } 3619 3620 .admin-help-item p { 3621 color: #777; 3622 3623 font-size: 11px; 3624 line-height: 1.6; 3625 } 3626 3627 3628 /* ========================================================= 3629 EDIT PRODUCT 3630 ========================================================= */ 3631 3632 .admin-product-summary { 3633 display: flex; 3634 align-items: center; 3635 justify-content: space-between; 3636 3637 gap: 30px; 3638 3639 padding: 24px; 3640 3641 margin-bottom: 20px; 3642 } 3643 3644 .admin-product-summary h2 { 3645 margin-top: 5px; 3646 3647 font-size: 23px; 3648 } 3649 3650 .admin-product-summary-meta { 3651 display: flex; 3652 3653 gap: 35px; 3654 } 3655 3656 .admin-product-summary-meta div { 3657 text-align: right; 3658 } 3659 3660 .admin-product-summary-meta span { 3661 display: block; 3662 3663 color: #999; 3664 3665 font-size: 8px; 3666 font-weight: 900; 3667 3668 letter-spacing: 1px; 3669 } 3670 3671 .admin-product-summary-meta strong { 3672 display: block; 3673 3674 margin-top: 5px; 3675 3676 font-size: 13px; 3677 } 3678 3679 .admin-edit-card { 3680 max-width: 900px; 3681 } 3682 3683 .admin-modification-options { 3684 display: grid; 3685 3686 grid-template-columns: 3687 1fr 1fr; 3688 3689 gap: 10px; 3690 } 3691 3692 .admin-modification-option { 3693 display: flex !important; 3694 align-items: flex-start; 3695 3696 gap: 10px; 3697 3698 padding: 15px; 3699 3700 border: 1px solid #ddd; 3701 border-radius: 7px; 3702 3703 cursor: pointer; 3704 3705 text-transform: none !important; 3706 letter-spacing: normal !important; 3707 } 3708 3709 .admin-modification-option:hover { 3710 border-color: #999; 3711 } 3712 3713 .admin-modification-option input { 3714 width: auto; 3715 3716 min-height: 0; 3717 3718 margin-top: 3px; 3719 } 3720 3721 .admin-modification-option strong { 3722 display: block; 3723 3724 color: #222; 3725 3726 font-size: 12px; 3727 } 3728 3729 .admin-modification-option small { 3730 margin-top: 4px; 3731 3732 color: #888; 3733 3734 font-size: 10px; 3735 } 3736 3737 .admin-edit-section { 3738 margin-top: 28px; 3739 3740 padding-top: 25px; 3741 3742 border-top: 1px solid #eee; 3743 } 3744 3745 .admin-edit-section-heading { 3746 margin-bottom: 22px; 3747 } 3748 3749 .admin-edit-section-heading h3 { 3750 margin-top: 5px; 3751 3752 font-size: 19px; 3753 } 3754 3755 .admin-edit-section-heading p { 3756 margin-top: 5px; 3757 3758 color: #777; 3759 3760 font-size: 11px; 3761 } 3762 3763 .admin-discount-preview { 3764 display: grid; 3765 3766 grid-template-columns: 3767 1fr 1fr; 3768 3769 gap: 8px 20px; 3770 3771 padding: 18px; 3772 3773 background: #f7f7f7; 3774 3775 border-radius: 7px; 3776 } 3777 3778 .admin-discount-preview span { 3779 color: #888; 3780 3781 font-size: 8px; 3782 font-weight: 900; 3783 3784 letter-spacing: 1px; 3785 } 3786 3787 .admin-discount-preview strong { 3788 font-size: 18px; 3789 } 3790 3791 3792 /* ========================================================= 3793 ADMIN RESPONSIVE 3794 ========================================================= */ 3795 3796 @media (max-width: 1000px) { 3797 3798 .admin-stat-grid { 3799 grid-template-columns: 3800 repeat(2, 1fr); 3801 } 3802 3803 .admin-form-layout { 3804 grid-template-columns: 1fr; 3805 } 3806 3807 .admin-help-card { 3808 order: -1; 3809 } 3810 3811 } 3812 3813 3814 @media (max-width: 750px) { 3815 3816 .admin-page { 3817 padding: 35px 18px 60px; 3818 } 3819 3820 .admin-header { 3821 flex-direction: column; 3822 align-items: flex-start; 3823 } 3824 3825 .admin-header h1 { 3826 font-size: 35px; 3827 } 3828 3829 .admin-stat-grid, 3830 .admin-action-grid { 3831 grid-template-columns: 1fr; 3832 } 3833 3834 .admin-form-row, 3835 .admin-modification-options { 3836 grid-template-columns: 1fr; 3837 } 3838 3839 .admin-product-summary { 3840 align-items: flex-start; 3841 flex-direction: column; 3842 } 3843 3844 .admin-product-summary-meta { 3845 width: 100%; 3846 3847 justify-content: space-between; 3848 } 3849 3850 .admin-product-summary-meta div { 3851 text-align: left; 3852 } 3853 3854 } 3855 3856 3857 @media (max-width: 500px) { 3858 3859 .admin-stat-grid { 3860 grid-template-columns: 1fr; 3861 } 3862 3863 .admin-form-card { 3864 padding: 20px; 3865 } 3866 3867 .admin-form-actions { 3868 flex-direction: column; 3869 } 3870 3871 .admin-form-actions .admin-button { 3872 width: 100%; 3873 } 3874 3875 } 3876 /* ========================================================= 3877 ADMIN RELEASE CREATOR 3878 ========================================================= */ 3879 3880 .release-create-section { 3881 margin-bottom: 20px; 3882 } 3883 3884 .admin-section-description { 3885 margin-top: 7px; 3886 3887 color: #888; 3888 3889 font-size: 11px; 3890 line-height: 1.6; 3891 } 3892 3893 3894 /* ========================================================= 3895 MULTI SELECT 3896 ========================================================= */ 3897 3898 .admin-multi-select { 3899 min-height: 130px !important; 3900 3901 padding: 7px !important; 3902 } 3903 3904 .admin-multi-select option { 3905 padding: 8px 9px; 3906 3907 border-radius: 4px; 3908 } 3909 3910 3911 /* ========================================================= 3912 COVER PREVIEW 3913 ========================================================= */ 3914 3915 .admin-cover-preview { 3916 margin-top: 25px; 3917 3918 padding-top: 22px; 3919 3920 border-top: 1px solid #eee; 3921 } 3922 3923 .admin-cover-preview-image { 3924 width: 150px; 3925 height: 150px; 3926 3927 margin-top: 10px; 3928 3929 display: flex; 3930 align-items: center; 3931 justify-content: center; 3932 3933 overflow: hidden; 3934 3935 background: 3936 linear-gradient( 3937 135deg, 3938 #222, 3939 #555 3940 ); 3941 3942 color: white; 3943 3944 border-radius: 7px; 3945 3946 font-size: 35px; 3947 } 3948 3949 .admin-cover-preview-image img { 3950 width: 100%; 3951 height: 100%; 3952 3953 display: block; 3954 3955 object-fit: cover; 3956 } 3957 3958 3959 /* ========================================================= 3960 TRACK HEADER 3961 ========================================================= */ 3962 3963 .track-header { 3964 display: flex; 3965 align-items: center; 3966 justify-content: space-between; 3967 3968 gap: 25px; 3969 } 3970 3971 3972 /* ========================================================= 3973 TRACK CARDS 3974 ========================================================= */ 3975 3976 .admin-track-card { 3977 position: relative; 3978 3979 display: grid; 3980 3981 grid-template-columns: 3982 42px 3983 minmax(0, 1fr) 3984 35px; 3985 3986 gap: 15px; 3987 3988 margin-bottom: 12px; 3989 padding: 20px; 3990 3991 background: #fafafa; 3992 3993 border: 1px solid #ddd; 3994 border-radius: 8px; 3995 } 3996 3997 .admin-track-number { 3998 width: 36px; 3999 height: 36px; 4000 4001 display: flex; 4002 align-items: center; 4003 justify-content: center; 4004 4005 background: #171717; 4006 color: white; 4007 4008 border-radius: 50%; 4009 4010 font-size: 11px; 4011 font-weight: 900; 4012 } 4013 4014 .admin-track-content { 4015 min-width: 0; 4016 } 4017 4018 .admin-track-remove { 4019 width: 32px; 4020 height: 32px; 4021 4022 display: flex; 4023 align-items: center; 4024 justify-content: center; 4025 4026 border: 1px solid #ddd; 4027 border-radius: 5px; 4028 4029 background: white; 4030 color: #888; 4031 4032 font-size: 20px; 4033 } 4034 4035 .admin-track-remove:hover { 4036 border-color: #a44; 4037 4038 background: #fff5f5; 4039 color: #a44; 4040 } 4041 4042 4043 /* ========================================================= 4044 RELEASE SUBMIT 4045 ========================================================= */ 4046 4047 .admin-release-submit { 4048 display: flex; 4049 align-items: center; 4050 justify-content: space-between; 4051 4052 gap: 25px; 4053 4054 margin-top: 30px; 4055 padding: 25px 30px; 4056 4057 background: #171717; 4058 color: white; 4059 4060 border-radius: 9px; 4061 } 4062 4063 .admin-release-submit p { 4064 margin-top: 5px; 4065 4066 color: #aaa; 4067 4068 font-size: 11px; 4069 } 4070 4071 .admin-release-submit .admin-eyebrow { 4072 color: #888; 4073 } 4074 4075 .admin-release-submit .release-actions { 4076 margin: 0; 4077 padding: 0; 4078 4079 border: none; 4080 } 4081 4082 .admin-release-submit .admin-button.primary { 4083 border-color: white; 4084 4085 background: white; 4086 color: #171717; 4087 } 4088 4089 .admin-release-submit .admin-button.primary:hover { 4090 background: #ddd; 4091 } 4092 4093 .admin-release-submit .admin-button.secondary { 4094 border-color: #444; 4095 4096 background: transparent; 4097 color: white; 4098 } 4099 4100 .admin-release-submit .admin-button.secondary:hover { 4101 background: #292929; 4102 } 4103 4104 4105 /* ========================================================= 4106 RELEASE CREATOR RESPONSIVE 4107 ========================================================= */ 4108 4109 @media (max-width: 750px) { 4110 4111 .track-header { 4112 align-items: flex-start; 4113 4114 flex-direction: column; 4115 } 4116 4117 .admin-track-card { 4118 grid-template-columns: 4119 35px 4120 1fr; 4121 } 4122 4123 .admin-track-remove { 4124 position: absolute; 4125 4126 top: 15px; 4127 right: 15px; 4128 } 4129 4130 .admin-release-submit { 4131 align-items: flex-start; 4132 4133 flex-direction: column; 4134 } 4135 4136 .admin-release-submit .release-actions { 4137 width: 100%; 4138 } 4139 4140 }
Note:
See TracChangeset
for help on using the changeset viewer.
