Changeset fa0fbaf


Ignore:
Timestamp:
09/01/26 06:11:34 (4 weeks ago)
Author:
mmilevski <markomilevski3@…>
Branches:
main
Children:
d2eccb3
Parents:
08aefc6
Message:

Added major improvements.

Files:
13 added
5 deleted
48 edited

Legend:

Unmodified
Added
Removed
  • KernelRecordsMVC.Web/Controllers/AccountController.cs

    r08aefc6 rfa0fbaf  
    1 using Microsoft.AspNetCore.Mvc;
    2 using KernelRecordsMVC.Data;
     1using System.Security.Claims;
     2using KernelRecordsMVC.Application.ViewModels;
     3using KernelRecordsMVC.Infrastructure.Data;
    34using KernelRecordsMVC.Models;
    4 using KernelRecordsMVC.Application.ViewModels;
    5 using Microsoft.AspNetCore.Http;
    6 
    7 namespace KernelRecordsMVC.Controllers;
     5using Microsoft.AspNetCore.Authentication;
     6using Microsoft.AspNetCore.Mvc;
     7using Microsoft.EntityFrameworkCore;
     8
     9namespace KernelRecordsMVC.Web.Controllers;
    810
    911public class AccountController : Controller
    … …  
    1618    }
    1719
    18     // ==============================
     20
     21    // =========================================================
    1922    // REGISTER - GET
    20     // ==============================
     23    // =========================================================
    2124
    2225    [HttpGet]
    … …  
    2730
    2831
    29     // ==============================
     32    // =========================================================
    3033    // REGISTER - POST
    31     // ==============================
     34    // =========================================================
    3235
    3336    [HttpPost]
    … …  
    6164            Username = model.Username,
    6265
    63             // We'll replace this with proper password hashing
    64             // in the security cleanup section.
     66            // TODO:
     67            // Replace with password hashing later.
    6568            Password = model.Password,
    6669
    … …  
    7477        _context.SaveChanges();
    7578
     79
    7680        // Every normal registered user is a Consumer.
    7781        var consumer = new Consumer
    … …  
    8993
    9094
    91     // ==============================
     95    // =========================================================
    9296    // LOGIN - GET
    93     // ==============================
     97    // =========================================================
    9498
    9599    [HttpGet]
    … …  
    100104
    101105
    102     // ==============================
     106    // =========================================================
    103107    // LOGIN - POST
    104     // ==============================
     108    // =========================================================
    105109
    106110    [HttpPost]
    107111    [ValidateAntiForgeryToken]
    108     public IActionResult Login(LoginViewModel model)
     112    public async Task<IActionResult> Login(
     113        LoginViewModel model)
    109114    {
    110115        if (!ModelState.IsValid)
    … …  
    125130        }
    126131
     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
    127206        HttpContext.Session.SetInt32(
    128207            "UserId",
    … …  
    133212            user.Username);
    134213
    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);
    139217
    140218        if (admin != null)
    141219        {
    142             HttpContext.Session.SetString(
    143                 "Role",
    144                 "Admin");
    145 
    146220            HttpContext.Session.SetString(
    147221                "AdminType",
    148222                admin.Type.ToString());
    149223        }
    150         else
    151         {
    152             HttpContext.Session.SetString(
    153                 "Role",
    154                 "Consumer");
    155         }
     224
    156225
    157226        return RedirectToAction(
    … …  
    161230
    162231
    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    // =========================================================
    164414    // 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
    169423        HttpContext.Session.Clear();
    170424
  • KernelRecordsMVC.Web/Controllers/AdminController.cs

    r08aefc6 rfa0fbaf  
    1 using Microsoft.AspNetCore.Mvc;
     1using KernelRecordsMVC.Application.ViewModels;
     2using KernelRecordsMVC.Domain.Enums;
     3using KernelRecordsMVC.Infrastructure.Data;
     4using KernelRecordsMVC.Models;
     5using Microsoft.AspNetCore.Mvc;
    26using 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
     8namespace KernelRecordsMVC.Web.Controllers;
    109
    1110public class AdminController : Controller
    … …  
    1918
    2019
    21     // ==========================================
     20    // =========================================================
    2221    // ADMIN DASHBOARD
    23     // ==========================================
     22    // =========================================================
    2423
    2524    [HttpGet]
    … …  
    2928            return Forbid();
    3029
     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
    3147        return View();
    3248    }
    3349
    3450
    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    // =========================================================
    3976
    4077    [HttpGet]
    … …  
    4481            return Forbid();
    4582
    46         ViewBag.Releases = _context.Releases
    47             .OrderBy(x => x.Title)
    48             .ToList();
    49 
    50         return View(new CreateProductViewModel());
    51     }
    52 
    53 
    54     // ==========================================
    55     // UC008
    56     // NEW PRODUCT - POST
    57     // ==========================================
     83
     84        LoadReleases();
     85
     86
     87        return View(
     88            new CreateProductViewModel());
     89    }
     90
     91
     92    // =========================================================
     93    // CREATE PRODUCT - POST
     94    // =========================================================
    5895
    5996    [HttpPost]
    … …  
    65102            return Forbid();
    66103
     104
    67105        if (!ModelState.IsValid)
    68106        {
    69107            LoadReleases();
     108
    70109            return View(model);
    71110        }
    72111
    73112
    74         // Make sure the release actually exists.
    75113        var release = _context.Releases
    76114            .FirstOrDefault(x =>
    77115                x.ReleaseId == model.ReleaseId);
    78116
     117
    79118        if (release == null)
    80119        {
    81120            ModelState.AddModelError(
    82                 "ReleaseId",
     121                nameof(model.ReleaseId),
    83122                "The selected release does not exist.");
    84123
     124
    85125            LoadReleases();
     126
     127
    86128            return View(model);
    87129        }
    88130
    89131
    90         // Don't allow duplicate format for
    91         // 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
    95137
    96138        if (alreadyExists)
    97139        {
    98140            ModelState.AddModelError(
    99                 "Format",
     141                nameof(model.Format),
    100142                "This release already has a product in this format.");
    101143
     144
    102145            LoadReleases();
     146
     147
    103148            return View(model);
    104149        }
    105150
    106151
    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    // =========================================================
    147946
    148947    private bool IsAdmin()
    149948    {
    150949        return HttpContext.Session
    151             .GetString("Role") == "Admin";
     950            .GetString("Role") ==
     951            "Admin";
    152952    }
    153953
    … …  
    155955    private bool IsProductManager()
    156956    {
    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
    162965
    163966        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    // =========================================================
    168977
    169978    private long? GetCurrentUserId()
    170979    {
    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    // =========================================================
    1801024
    1811025    private long GetNextProductId()
    1821026    {
    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
    1861033
    1871034        return (maxId ?? 0) + 1;
    … …  
    1891036
    1901037
    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    // =========================================================
    1981092
    1991093    private void CreateModification(
    … …  
    2021096        decimal? discount = null)
    2031097    {
    204         var adminId = GetCurrentUserId();
     1098        var adminId =
     1099            GetCurrentUserId();
     1100
    2051101
    2061102        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);
    2261131
    2271132        _context.SaveChanges();
    … …  
    2341139                    modification.ModificationId,
    2351140
    236                 ProductId = productId
     1141                ProductId =
     1142                    productId
    2371143            };
    2381144
    … …  
    2431149        _context.SaveChanges();
    2441150    }
    245 
    246 
    247     private long GetNextModificationId()
    248     {
    249         var maxId = _context.Modifications
    250             .Select(x => (long?)x.ModificationId)
    251             .Max();
    252 
    253         return (maxId ?? 0) + 1;
    254     }
    255     // ==========================================
    256 // UC009
    257 // MODIFY PRODUCT - GET
    258 // ==========================================
    259 
    260     [HttpGet]
    261     public IActionResult EditProduct(long id)
    262     {
    263         if (!IsProductManager())
    264             return Forbid();
    265 
    266         var product = _context.Products
    267             .Include(p => p.Release)
    268             .FirstOrDefault(p => p.ProductId == id);
    269 
    270         if (product == null)
    271             return NotFound();
    272 
    273         var model = new EditProductViewModel
    274         {
    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.Stock
    282         };
    283 
    284         return View(model);
    285     }
    286     // ==========================================
    287 // UC009
    288 // MODIFY PRODUCT - POST
    289 // ==========================================
    290 
    291 [HttpPost]
    292 [ValidateAntiForgeryToken]
    293 public IActionResult EditProduct(
    294     EditProductViewModel model)
    295 {
    296     if (!IsProductManager())
    297         return Forbid();
    298 
    299     var product = _context.Products
    300         .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     // DISCOUNT
    319     // ==========================================
    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     else
    342     {
    343         // ==========================================
    344         // NORMAL UPDATE
    345         // ==========================================
    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 MODIFICATION
    361     // ==========================================
    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));
    3731151}
    374 [HttpGet]
    375 public IActionResult Products()
    376 {
    377     if (!IsProductManager())
    378         return Forbid();
    379 
    380     var products = _context.Products
    381         .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  
    11using System.Diagnostics;
     2using KernelRecordsMVC.Models;
    23using Microsoft.AspNetCore.Mvc;
    3 using KernelRecordsMVC.Models;
    4 using Microsoft.Extensions.Logging;
    54
    6 namespace KernelRecordsMVC.Controllers;
     5namespace KernelRecordsMVC.Web.Controllers;
    76
    87public class HomeController : Controller
  • KernelRecordsMVC.Web/Controllers/OrderController.cs

    r08aefc6 rfa0fbaf  
    1 using Microsoft.AspNetCore.Http;
    2 
    3 namespace KernelRecordsMVC.Controllers;
    4 
     1using KernelRecordsMVC.Application.ViewModels;
     2using KernelRecordsMVC.Domain.Enums;
     3using KernelRecordsMVC.Infrastructure.Data;
     4using KernelRecordsMVC.Models;
    55using Microsoft.AspNetCore.Mvc;
    66using Microsoft.EntityFrameworkCore;
    7 using KernelRecordsMVC.Data;
    8 using KernelRecordsMVC.Models;
    9 using KernelRecordsMVC.Domain.Enums;
    10 using KernelRecordsMVC.Application.ViewModels;
     7
     8namespace KernelRecordsMVC.Web.Controllers;
    119
    1210public class OrderController : Controller
    … …  
    6765            order = new Order
    6866            {
    69                 OrderId = GetNextOrderId(),
    7067                UserId = userId.Value,
    71 
    72                 // The actual payment method will be
    73                 // selected during checkout.
    7468                PaymentMethod = PaymentMethodType.CARD,
    75 
    7669                PurchaseDate = DateTime.Today,
    77 
    7870                PointsEarned = 0,
    79 
    8071                PointsUsed = null,
    81 
    8272                Status = OrderStatusType.PENDING
    8373            };
    … …  
    113103        else
    114104        {
     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
    115130            var orderProduct = new OrderProduct
    116131            {
    … …  
    186201
    187202                    Stock =
    188                         op.Product.Stock
     203                        op.Product.Stock,
     204                   
     205                    CoverPhoto = op.Product.Release.CoverPhoto
    189206                })
    190207                .ToList();
    … …  
    294311    private long? GetUserId()
    295312    {
    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;
    301317
    302318        return null;
    303319    }
    304 
    305 
    306     // ==========================================
    307     // ORDER ID
    308     // ==========================================
    309 
    310     private long GetNextOrderId()
    311     {
    312         var maxId = _context.Orders
    313             .Select(x => (long?)x.OrderId)
    314             .Max();
    315 
    316         return (maxId ?? 0) + 1;
    317     }
    318320}
  • KernelRecordsMVC.Web/Controllers/ReleaseController.cs

    r08aefc6 rfa0fbaf  
    1 using Microsoft.AspNetCore.Mvc;
     1using KernelRecordsMVC.Domain.Enums;
     2using KernelRecordsMVC.Infrastructure.Data;
     3using Microsoft.AspNetCore.Mvc;
    24using Microsoft.EntityFrameworkCore;
    3 using KernelRecordsMVC.Data;
    45
    5 namespace KernelRecordsMVC.Controllers;
     6namespace KernelRecordsMVC.Web.Controllers;
    67
    78public class ReleaseController : Controller
    … …  
    1415    }
    1516
    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)
    1827    {
    1928        var query = _context.Releases
    2029            .Include(r => r.Products)
    2130            .Include(r => r.ReleaseArtists)
    22             .ThenInclude(ra => ra.Artist)
     31                .ThenInclude(ra => ra.Artist)
     32            .Include(r => r.Album)
     33            .Include(r => r.SingleRelease)
    2334            .AsQueryable();
    2435
     36        // SEARCH
    2537        if (!string.IsNullOrWhiteSpace(search))
    2638        {
     39            search = search.Trim();
     40
    2741            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())));
    2947        }
    3048
     49        // GENRE
    3150        if (!string.IsNullOrWhiteSpace(genre))
    3251        {
    … …  
    3554        }
    3655
    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();
    40112
    41113        ViewBag.Search = search;
    42114        ViewBag.Genre = genre;
     115        ViewBag.Format = format;
     116        ViewBag.Type = type;
     117        ViewBag.Sort = sort;
    43118
    44119        ViewBag.Genres = _context.Releases
    … …  
    48123            .ToList();
    49124
     125        ViewBag.Formats = Enum
     126            .GetValues<ProductFormat>()
     127            .Select(x => x.ToString())
     128            .ToList();
     129
    50130        return View(releases);
    51131    }
    52132
    53133
    54     // Release details
     134    // =========================================================
     135    // DETAILS
     136    // =========================================================
     137
    55138    public IActionResult Details(long id)
    56139    {
    57140        var release = _context.Releases
    58141            .Include(r => r.Products)
     142
    59143            .Include(r => r.ReleaseArtists)
    60             .ThenInclude(ra => ra.Artist)
     144                .ThenInclude(ra => ra.Artist)
     145
    61146            .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
    64152            .Include(r => r.SingleRelease)
    65             .ThenInclude(s => s!.SingleFeatures)
    66             .ThenInclude(x => x.Song)
     153
    67154            .FirstOrDefault(r => r.ReleaseId == id);
    68155
  • KernelRecordsMVC.Web/KernelRecordsMVC.Web.csproj

    r08aefc6 rfa0fbaf  
    55        <ImplicitUsings>enable</ImplicitUsings>
    66        <Nullable>enable</Nullable>
    7     </PropertyGroup>
     7      <UserSecretsId>7996030f-d5d7-4616-a943-1e4992329169</UserSecretsId>
     8  </PropertyGroup>
    89
    910    <ItemGroup>
    … …  
    3738    <ItemGroup>
    3839      <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
     40      <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" />
    3941    </ItemGroup>
    4042
  • KernelRecordsMVC.Web/Program.cs

    r08aefc6 rfa0fbaf  
    11using Microsoft.EntityFrameworkCore;
    2 using KernelRecordsMVC.Data;
     2using Npgsql;
     3using Npgsql.NameTranslation;
     4using KernelRecordsMVC.Domain.Enums;
     5using KernelRecordsMVC.Infrastructure.Data;
    36
    47var builder = WebApplication.CreateBuilder(args);
    58
     9// ==========================================
     10// AUTHENTICATION
     11// ==========================================
     12
     13builder.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
     23builder.Services.AddAuthorization();
     24
    625builder.Services.AddControllersWithViews();
    726
     27// ==========================================
     28// DATABASE
     29// ==========================================
     30
     31var dataSourceBuilder = new NpgsqlDataSourceBuilder(
     32    builder.Configuration.GetConnectionString("KernelRecords"));
     33
     34var nameTranslator = new NpgsqlNullNameTranslator();
     35
     36dataSourceBuilder.MapEnum<AdminType>(
     37    "project.admin_type",
     38    nameTranslator);
     39
     40dataSourceBuilder.MapEnum<ProductFormat>(
     41    "project.product_format",
     42    nameTranslator);
     43
     44dataSourceBuilder.MapEnum<PaymentMethodType>(
     45    "project.payment_method_type",
     46    nameTranslator);
     47
     48dataSourceBuilder.MapEnum<OrderStatusType>(
     49    "project.order_status_type",
     50    nameTranslator);
     51
     52dataSourceBuilder.MapEnum<ModificationType>(
     53    "project.modification_type",
     54    nameTranslator);
     55
     56dataSourceBuilder.MapEnum<ArtistReleaseType>(
     57    "project.artist_release_type",
     58    nameTranslator);
     59
     60var dataSource = dataSourceBuilder.Build();
     61
    862builder.Services.AddDbContext<KernelRecordsContext>(options =>
    9     options.UseNpgsql(
    10         builder.Configuration.GetConnectionString("KernelRecords")));
     63    options.UseNpgsql(dataSource));
    1164
    12 builder.Services.AddSession();
     65// ==========================================
     66// SESSION
     67// ==========================================
     68
     69builder.Services.AddSession(options =>
     70{
     71    options.IdleTimeout = TimeSpan.FromHours(2);
     72    options.Cookie.HttpOnly = true;
     73    options.Cookie.IsEssential = true;
     74});
    1375
    1476var app = builder.Build();
     77
     78// ==========================================
     79// MIDDLEWARE
     80// ==========================================
    1581
    1682if (!app.Environment.IsDevelopment())
    … …  
    2187
    2288app.UseHttpsRedirection();
     89
    2390app.UseStaticFiles();
    2491
    … …  
    2693
    2794app.UseSession();
     95
     96app.UseAuthentication();
    2897
    2998app.UseAuthorization();
  • KernelRecordsMVC.Web/Views/Admin/CreateProduct.cshtml

    r08aefc6 rfa0fbaf  
    11@model KernelRecordsMVC.Application.ViewModels.CreateProductViewModel
    22
    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
    1634    </div>
    1735
    1836
    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>
    49275
    50276    </div>
    51277
    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
    164280
    165281@section Scripts
    166282{
    167     <partial name="_ValidationScriptsPartial"/>
     283    <partial name="_ValidationScriptsPartial" />
    168284}
  • KernelRecordsMVC.Web/Views/Admin/EditProduct.cshtml

    r08aefc6 rfa0fbaf  
    11@model KernelRecordsMVC.Application.ViewModels.EditProductViewModel
    22
    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>
    2033
    2134    </div>
    2235
     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
    23354</div>
    24355
    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 Type
    47 
    48         </label>
    49 
    50         <select asp-for="ModificationType"
    51                 class="form-select">
    52 
    53             <option value="UPDATE">
    54                 Update Product
    55             </option>
    56 
    57             <option value="DISCOUNT">
    58                 Apply Discount
    59             </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             Price
    78 
    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 Percentage
    108 
    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             Description
    145 
    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             Stock
    168 
    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 Changes
    189 
    190         </button>
    191 
    192         <a asp-action="Index"
    193            class="btn btn-secondary">
    194 
    195             Cancel
    196 
    197         </a>
    198 
    199     </div>
    200 
    201 </form>
    202356
    203357@section Scripts
    204358{
    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>
    206449}
  • KernelRecordsMVC.Web/Views/Admin/Index.cshtml

    r08aefc6 rfa0fbaf  
    33}
    44
    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
    1149    </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>
    23167
    24168                <p>
    25                     Create and modify products available
    26                     in the music store.
     169                    Create music releases and manage their physical formats.
    27170                </p>
    28171
    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>
    48355
    49356</div>
  • KernelRecordsMVC.Web/Views/Admin/Products.cshtml

    r08aefc6 rfa0fbaf  
    11@model IEnumerable<KernelRecordsMVC.Models.Product>
    22
    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>
    1242
    1343    </div>
    1444
    1545
    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>
    22224
    23225</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.Title
    69                     </strong>
    70 
    71                 </td>
    72 
    73 
    74                 <td>
    75 
    76                     <span class="badge bg-secondary">
    77 
    78                         @product.Format
    79 
    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.Stock
    99 
    100                         </span>
    101                     }
    102                     else
    103                     {
    104                         <span class="text-danger">
    105                             Out of stock
    106                         </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                         Modify
    119 
    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}
    24
    3     <h1 class="display-3">
    4         Music Store
    5     </h1>
     5<div class="home-page">
    66
    7     <p class="lead mt-3">
    8         CDs · Vinyl · Cassettes
    9     </p>
     7    <!-- =====================================================
     8         HERO
     9         ===================================================== -->
    1010
    11     <p class="text-muted">
    12         Discover your next favorite release.
    13     </p>
     11    <section class="home-hero">
    1412
    15     <a asp-controller="Release"
    16        asp-action="Index"
    17        class="btn btn-primary btn-lg mt-3">
     13        <div class="home-hero-content">
    1814
    19         Browse Releases
     15            <span class="home-eyebrow">
     16                KERNEL RECORDS
     17            </span>
    2018
    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>
    22177
    23178</div>
  • KernelRecordsMVC.Web/Views/Order/Cart.cshtml

    r08aefc6 rfa0fbaf  
    11@model KernelRecordsMVC.Application.ViewModels.CartViewModel
    22
    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";
    255}
    266
    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>
    3724
    3825        <a asp-controller="Release"
    3926           asp-action="Index"
    40            class="btn btn-primary">
    41 
    42             Browse Releases
     27           class="store-button secondary">
     28
     29            Continue Shopping
    4330
    4431        </a>
    … …  
    4633    </div>
    4734
    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
    102205                    </span>
    103206
    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                             Update
    138 
    139                         </button>
    140 
    141                     </form>
    142 
    143                 </td>
    144 
    145 
    146                 <td>
    147 
    148207                    <strong>
    149 
    150                         $@item.Total.ToString("0.00")
    151 
     208                        @Model.Items.Sum(x => x.Quantity)
    152209                    </strong>
    153210
    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    }
    185238
    186239</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 Checkout
    216 
    217                 </a>
    218 
    219             </div>
    220 
    221         </div>
    222 
    223     </div>
    224 
    225 </div>
  • KernelRecordsMVC.Web/Views/Release/Details.cshtml

    r08aefc6 rfa0fbaf  
    11@model KernelRecordsMVC.Models.Release
    22
    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>
    10123
    11124    </div>
    12125
    13126
    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    }
    47386
    48387</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.Format
    68                 </h4>
    69 
    70                 <h5>
    71                     $@product.Price
    72                 </h5>
    73 
    74                 <p>
    75                     @product.ProductDescription
    76                 </p>
    77 
    78 
    79                 @if (product.Stock > 0)
    80                 {
    81                     <p class="text-success">
    82                         In stock: @product.Stock
    83                     </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 Cart
    91 
    92                     </a>
    93                 }
    94                 else
    95                 {
    96                     <p class="text-danger">
    97                         Out of stock
    98                     </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.AlbumSongs
    120         .OrderBy(x => x.SongId))
    121     {
    122         <li>
    123 
    124             @albumSong.Song.SongName
    125 
    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.Duration
    146     </p>
    147 }
  • KernelRecordsMVC.Web/Views/Release/Index.cshtml

    r08aefc6 rfa0fbaf  
    11@model IEnumerable<KernelRecordsMVC.Models.Release>
    22
    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
    1029    </div>
    1130
     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
    12344</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 genres
    39             </option>
    40 
    41             @foreach (var genre in ViewBag.Genres)
    42             {
    43                 <option value="@genre"
    44                         selected="@(ViewBag.Genre == genre)">
    45 
    46                     @genre
    47 
    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             Search
    62 
    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.Title
    92                 </h4>
    93 
    94                 <p class="text-muted mb-1">
    95                     @release.Genre
    96                 </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.ReleaseArtists
    110                             .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.Format
    126 
    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 Release
    138 
    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
    25<html lang="en">
    36<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
    1016</head>
     17
    1118<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
    29436            </div>
     437
    30438        </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
    32466</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
    37621</div>
    38622
    39 <footer class="border-top footer text-muted">
    40     <div class="container">
    41         &copy; 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
    43639</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",
     679required: false)
     680
    48681</body>
    49682</html>
  • KernelRecordsMVC.Web/appsettings.json

    r08aefc6 rfa0fbaf  
    11{
     2  "ConnectionStrings": {
     3    "KernelRecords": ""
     4  },
     5
    26  "Logging": {
    37    "LogLevel": {
    … …  
    610    }
    711  },
     12
    813  "AllowedHosts": "*"
    914}
  • KernelRecordsMVC.Web/bin/Debug/net8.0/KernelRecordsMVC.Web.deps.json

    r08aefc6 rfa0fbaf  
    1111          "KernelRecordsMVC.Application": "1.0.0",
    1212          "KernelRecordsMVC.Infrastructure": "1.0.0",
    13           "Microsoft.EntityFrameworkCore": "8.0.8"
     13          "Microsoft.EntityFrameworkCore": "8.0.8",
     14          "Npgsql.EntityFrameworkCore.PostgreSQL": "8.0.8"
    1415        },
    1516        "runtime": {
  • KernelRecordsMVC.Web/bin/Debug/net8.0/appsettings.json

    r08aefc6 rfa0fbaf  
    11{
     2  "ConnectionStrings": {
     3    "KernelRecords": ""
     4  },
     5
    26  "Logging": {
    37    "LogLevel": {
    … …  
    610    }
    711  },
     12
    813  "AllowedHosts": "*"
    914}
  • KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.AssemblyInfo.cs

    r08aefc6 rfa0fbaf  
    1111using System.Reflection;
    1212
     13[assembly: Microsoft.Extensions.Configuration.UserSecrets.UserSecretsIdAttribute("7996030f-d5d7-4616-a943-1e4992329169")]
    1314[assembly: System.Reflection.AssemblyCompanyAttribute("KernelRecordsMVC.Web")]
    1415[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
    1516[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")]
    1718[assembly: System.Reflection.AssemblyProductAttribute("KernelRecordsMVC.Web")]
    1819[assembly: System.Reflection.AssemblyTitleAttribute("KernelRecordsMVC.Web")]
  • KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.AssemblyInfoInputs.cache

    r08aefc6 rfa0fbaf  
    1 a4f4eb88e517c7eefc9f4de02b54f54cf2b8d15155e7524b493c4ff6d5ef0bd5
     1c8c33cb9ccc0e5c5e6227ad0d596fbb462f0c80224c10ca755e42b9afff049e9
  • KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.GeneratedMSBuildEditorConfig.editorconfig

    r08aefc6 rfa0fbaf  
    6565build_metadata.AdditionalFiles.CssScope =
    6666
     67[C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Account/Profile.cshtml]
     68build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWNjb3VudFxQcm9maWxlLmNzaHRtbA==
     69build_metadata.AdditionalFiles.CssScope =
     70
    6771[C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Account/Register.cshtml]
    6872build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWNjb3VudFxSZWdpc3Rlci5jc2h0bWw=
    … …  
    7175[C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Admin/CreateProduct.cshtml]
    7276build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWRtaW5cQ3JlYXRlUHJvZHVjdC5jc2h0bWw=
     77build_metadata.AdditionalFiles.CssScope =
     78
     79[C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Admin/CreateRelease.cshtml]
     80build_metadata.AdditionalFiles.TargetPath = Vmlld3NcQWRtaW5cQ3JlYXRlUmVsZWFzZS5jc2h0bWw=
    7381build_metadata.AdditionalFiles.CssScope =
    7482
    … …  
    96104build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUmVsZWFzZVxJbmRleC5jc2h0bWw=
    97105build_metadata.AdditionalFiles.CssScope =
     106
     107[C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Sale/Index.cshtml]
     108build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2FsZVxJbmRleC5jc2h0bWw=
     109build_metadata.AdditionalFiles.CssScope =
     110
     111[C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/TopSellers/Index.cshtml]
     112build_metadata.AdditionalFiles.TargetPath = Vmlld3NcVG9wU2VsbGVyc1xJbmRleC5jc2h0bWw=
     113build_metadata.AdditionalFiles.CssScope =
     114
     115[C:/Users/Marko/RiderProjects/KernelRecordsMVC/KernelRecordsMVC.Web/Views/Wishlist/Index.cshtml]
     116build_metadata.AdditionalFiles.TargetPath = Vmlld3NcV2lzaGxpc3RcSW5kZXguY3NodG1s
     117build_metadata.AdditionalFiles.CssScope =
  • KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.csproj.CoreCompileInputs.cache

    r08aefc6 rfa0fbaf  
    1 9f4bc3ac55251cbca4306c0fb37573f798c006e8b97657e25caacf03d029d22e
     121c22170dbd5df356be1a77b3f77b619add1a8fb50970ec121bfff2f0bc30d5a
  • KernelRecordsMVC.Web/obj/Debug/net8.0/KernelRecordsMVC.Web.csproj.FileListAbsolute.txt

    r08aefc6 rfa0fbaf  
    2626C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\KernelRecordsMVC.Web.RazorAssemblyInfo.cache
    2727C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\KernelRecordsMVC.Web.RazorAssemblyInfo.cs
     28C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\KernelRecordsMVC.Web.sourcelink.json
    2829C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\staticwebassets.build.json
    2930C:\Users\Marko\RiderProjects\KernelRecordsMVC\KernelRecordsMVC.Web\obj\Debug\net8.0\staticwebassets.development.json
  • KernelRecordsMVC.Web/obj/KernelRecordsMVC.Web.csproj.nuget.dgspec.json

    r08aefc6 rfa0fbaf  
    6969            }
    7070          },
    71           "runtimeIdentifierGraphPath": "C:\\Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"
     71          "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json"
    7272        }
    7373      }
    … …  
    133133            }
    134134          },
    135           "runtimeIdentifierGraphPath": "C:\\Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"
     135          "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json"
    136136        }
    137137      }
    … …  
    208208            }
    209209          },
    210           "runtimeIdentifierGraphPath": "C:\\Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"
     210          "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json"
    211211        }
    212212      }
    … …  
    261261              "target": "Package",
    262262              "version": "[8.0.8, )"
     263            },
     264            "Npgsql.EntityFrameworkCore.PostgreSQL": {
     265              "target": "Package",
     266              "version": "[8.0.8, )"
    263267            }
    264268          },
    … …  
    282286            }
    283287          },
    284           "runtimeIdentifierGraphPath": "C:\\Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"
     288          "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json"
    285289        }
    286290      }
  • KernelRecordsMVC.Web/obj/KernelRecordsMVC.Web.csproj.nuget.g.props

    r08aefc6 rfa0fbaf  
    88    <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Marko\.nuget\packages\</NuGetPackageFolders>
    99    <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
    10     <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
     10    <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.11.2</NuGetToolVersion>
    1111  </PropertyGroup>
    1212  <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
  • KernelRecordsMVC.Web/obj/project.assets.json

    r08aefc6 rfa0fbaf  
    772772      "KernelRecordsMVC.Application >= 1.0.0",
    773773      "KernelRecordsMVC.Infrastructure >= 1.0.0",
    774       "Microsoft.EntityFrameworkCore >= 8.0.8"
     774      "Microsoft.EntityFrameworkCore >= 8.0.8",
     775      "Npgsql.EntityFrameworkCore.PostgreSQL >= 8.0.8"
    775776    ]
    776777  },
    … …  
    827828            "target": "Package",
    828829            "version": "[8.0.8, )"
     830          },
     831          "Npgsql.EntityFrameworkCore.PostgreSQL": {
     832            "target": "Package",
     833            "version": "[8.0.8, )"
    829834          }
    830835        },
    … …  
    848853          }
    849854        },
    850         "runtimeIdentifierGraphPath": "C:\\Users\\Marko\\.dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"
     855        "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.424/PortableRuntimeIdentifierGraph.json"
    851856      }
    852857    }
  • KernelRecordsMVC.Web/obj/project.nuget.cache

    r08aefc6 rfa0fbaf  
    11{
    22  "version": 2,
    3   "dgSpecHash": "OEK7FBkFq/o=",
     3  "dgSpecHash": "iFoBIOYqVS4=",
    44  "success": true,
    55  "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 17878899583857593
     117882300160457909
  • KernelRecordsMVC.Web/obj/rider.project.restore.info

    r08aefc6 rfa0fbaf  
    1 17878899596274790
     117882300160457909
  • 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
    112html {
     13  font-size: 16px;
     14  scroll-behavior: smooth;
     15}
     16
     17body {
     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
     34a {
     35  color: inherit;
     36  text-decoration: none;
     37}
     38
     39button,
     40input,
     41select,
     42textarea {
     43  font: inherit;
     44}
     45
     46button {
     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
    2916  font-size: 14px;
    3917}
    4918
    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 {
    161800  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.