Checkpoint web-player design-system refactor and backend track genre tagging

Advances the ongoing token/componentization cleanup (anchored player dock,
border-based panels instead of floating shadows, corrected light-mode
accent/gradient tokens) alongside in-progress backend support for track
genre tags.
This commit is contained in:
2026-09-17 17:43:37 +02:00
parent 5ae9955bd0
commit c14db5a711
83 changed files with 1872 additions and 248 deletions
@@ -17,7 +17,8 @@ namespace Musify.Application.Tracks
long UserId,
string Title,
Guid PictureIntentId,
Guid AudioIntentId)
Guid AudioIntentId,
IReadOnlyCollection<Genre> Tags)
: ICommand<ErrorOr<TrackApplicationResponse>>;
public class CreateTrackCommandHandler(
@@ -40,6 +41,23 @@ namespace Musify.Application.Tracks
return Error.NotFound(description: $"User {request.UserId} not found");
}
var distinctTags = request.Tags.Distinct().ToList();
if (distinctTags.Count == 0)
{
logger.LogWarning("Track {Title} was submitted without any tags", request.Title);
return Error.Validation(description: "At least one tag is required.");
}
var tagConflicts = GenreCompatibility.FindConflicts(distinctTags);
if (tagConflicts.Count > 0)
{
var conflict = tagConflicts.First();
logger.LogWarning(
"Track {Title} was submitted with incompatible tags {First} and {Second}",
request.Title, conflict.First, conflict.Second);
return Error.Validation(description: $"Tags '{conflict.First}' and '{conflict.Second}' are not compatible.");
}
var pictureValidation = await uploadIntentValidator.ValidateAndLoadAsync(
uploadIntentConfiguration,
request.PictureIntentId, request.UserId, cancellationToken);
@@ -80,6 +98,10 @@ namespace Musify.Application.Tracks
}
};
trackEntity.Tags = distinctTags
.Select(tag => new TrackTag { TrackId = trackEntity.Id, Tag = tag })
.ToList();
await database.Tracks.AddAsync(trackEntity, cancellationToken);
await database.UserHasTracks.AddAsync(new UserHasTrack
@@ -25,6 +25,7 @@ namespace Musify.Application.Tracks
var tracksById = await database.Tracks
.Include(t => t.Owner)
.Include(t => t.Tags)
.Where(t => recentTrackIds.Contains(t.Id))
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count } )
.ToDictionaryAsync(t => t.Id, cancellationToken);
@@ -16,6 +16,7 @@ namespace Musify.Application.Tracks
{
var entity = await database.Tracks.AsNoTracking()
.Include(t => t.Owner)
.Include(t => t.Tags)
.Where(t => t.Id == request.TrackId)
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count })
.SingleOrDefaultAsync(cancellationToken);
@@ -19,6 +19,7 @@ namespace Musify.Application.Tracks
var tracksQuery = database.UserHasTracks
.AsNoTracking()
.Include(ut => ut.Track.Owner)
.Include(ut => ut.Track.Tags)
.AsQueryable();
if (!string.IsNullOrEmpty(request.Name))
@@ -27,6 +27,7 @@ namespace Musify.Application.Tracks
var tracksQuery = database.UserHasTracks
.AsNoTracking()
.Include(ut => ut.Track.Owner)
.Include(ut => ut.Track.Tags)
.Where(p => p.UserId == request.UserId);
if (!string.IsNullOrEmpty(request.Name))
@@ -14,7 +14,8 @@ namespace Musify.Application.Tracks.Responses
int ListensCount,
DateTime CreatedAt,
DateTime UpdatedAt,
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId)
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
IReadOnlyCollection<Genre> Tags)
{
public static TrackApplicationResponse FromEntity(Track track, int listensCount) =>
new(
@@ -26,6 +27,7 @@ namespace Musify.Application.Tracks.Responses
listensCount,
track.CreatedAt,
track.UpdatedAt,
track.OwnerUserId);
track.OwnerUserId,
track.Tags.Select(tag => tag.Tag).ToList());
}
}