Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
26 changed files with 1317 additions and 88 deletions
Showing only changes of commit e728b333a9 - Show all commits
@@ -2,5 +2,5 @@ namespace Musify.Api.DataTransferObjects.PlayLists;
public record CreatePlayListRequest( public record CreatePlayListRequest(
string Name, string Name,
string Description, string? Description,
Guid? PictureIntentId); Guid? PictureIntentId);
@@ -181,12 +181,8 @@ public static class PlayListEndpoints
CreatePlayListRequest request, CreatePlayListRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var description = string.IsNullOrWhiteSpace(request.Description)
? "No description was provided."
: request.Description;
var result = await mediator.Send( var result = await mediator.Send(
new CreatePlayListCommand(currentUser.RequiredId, request.Name, description, request.PictureIntentId), new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId),
cancellationToken); cancellationToken);
return result.ToCreatedResult(playList => $"/playlists/{playList.Id}"); return result.ToCreatedResult(playList => $"/playlists/{playList.Id}");
@@ -16,7 +16,7 @@ namespace Musify.Application.PlayLists
public record CreatePlayListCommand( public record CreatePlayListCommand(
long UserId, long UserId,
string Name, string Name,
string Description, string? Description,
Guid? PictureIntentId) Guid? PictureIntentId)
: ICommand<ErrorOr<PlayListApplicationResponse>>; : ICommand<ErrorOr<PlayListApplicationResponse>>;
@@ -58,7 +58,7 @@ namespace Musify.Application.PlayLists
UserId = request.UserId, UserId = request.UserId,
Name = request.Name, Name = request.Name,
NormalizedName = request.Name.Trim().ToUpperInvariant(), NormalizedName = request.Name.Trim().ToUpperInvariant(),
Description = request.Description, Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
Pictures = pictureIntent == null ? null Pictures = pictureIntent == null ? null
: new PlayListPictures { OriginalName = pictureIntent.ObjectName } : new PlayListPictures { OriginalName = pictureIntent.ObjectName }
}; };
+1 -2
View File
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Musify.Domain.Abstractions; using Musify.Domain.Abstractions;
@@ -24,7 +24,6 @@ namespace Musify.Domain.Entities
[MaxLength(50)] [MaxLength(50)]
public required string NormalizedName { get; set; } public required string NormalizedName { get; set; }
[Required]
[MaxLength(256)] [MaxLength(256)]
public string? Description { get; set; } public string? Description { get; set; }
@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakePlayListDescriptionOptional : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "PlayLists",
type: "character varying(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
migrationBuilder.Sql(
"""
UPDATE "PlayLists" SET "Description" = NULL
WHERE "Description" = 'No description was provided.';
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "PlayLists",
type: "character varying(256)",
maxLength: 256,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true);
}
}
}
@@ -409,7 +409,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
@@ -22,7 +22,7 @@
<header <header
class="sticky top-0 z-40 flex h-14 items-center justify-between border-b border-line bg-bg/85 page-x backdrop-blur-md md:hidden" class="sticky top-0 z-40 flex h-14 items-center justify-between border-b border-line bg-bg/85 page-x backdrop-blur-md md:hidden"
> >
<a href="/" class="text-lg font-extrabold tracking-wide">Musify</a> <a href="/" class="text-lg font-semibold tracking-wide">Musify</a>
<div class="relative" bind:this={menuRef}> <div class="relative" bind:this={menuRef}>
<button <button
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import MediaCard from './MediaCard.svelte';
import PlaylistArt from './PlaylistArt.svelte'; import PlaylistArt from './PlaylistArt.svelte';
import Cover from './Cover.svelte'; import Cover from './Cover.svelte';
import { hueFor } from '$lib/theme/color'; import { hueFor } from '$lib/theme/color';
@@ -45,30 +46,25 @@
); );
</script> </script>
<a <MediaCard
href={href ?? `/albums/${id}`} href={href ?? `/albums/${id}`}
class="group animate-enter min-w-0 text-left" {title}
style="animation-delay:{index * 45}ms" subtitle={meta}
{index}
oncontextmenu={onContextMenu} oncontextmenu={onContextMenu}
> >
{#if coverSrc} {#snippet art(artClass)}
<Cover {#if coverSrc}
trackId={id} <Cover
src={coverSrc} trackId={id}
hue={hueFor(id)} src={coverSrc}
size="large" hue={hueFor(id)}
alt={title} size="large"
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg" alt={title}
/> class={artClass}
{:else} />
<PlaylistArt {:else}
{trackIds} <PlaylistArt {trackIds} hue={hueFor(id)} class={artClass} />
hue={hueFor(id)} {/if}
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg" {/snippet}
/> </MediaCard>
{/if}
<div class="mt-2.5 truncate text-fg">{title}</div>
{#if meta}
<div class="mt-0.5 truncate text-sm text-fg-3">{meta}</div>
{/if}
</a>
@@ -20,7 +20,7 @@
</span> </span>
{/if} {/if}
{#if title} {#if title}
<h3 class="mt-5 text-2xl font-bold tracking-tight text-fg">{title}</h3> <h3 class="mt-5 text-2xl font-semibold tracking-tight text-fg">{title}</h3>
{/if} {/if}
{#if description} {#if description}
<p class="mt-2 text-fg-2">{description}</p> <p class="mt-2 text-fg-2">{description}</p>
@@ -0,0 +1,32 @@
<script lang="ts" module>
export const mediaCardArt =
'aspect-square w-full rounded-art shadow-art transition duration-300 ease-out group-hover:-translate-y-1 group-hover:shadow-art-lg';
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
href: string;
title: string;
subtitle?: string | null;
index?: number;
oncontextmenu?: (event: MouseEvent) => void;
art: Snippet<[string]>;
}
let { href, title, subtitle, index = 0, oncontextmenu, art }: Props = $props();
</script>
<a
{href}
class="group animate-enter min-w-0 text-left"
style="animation-delay:{index * 45}ms"
{oncontextmenu}
>
{@render art(mediaCardArt)}
<div class="mt-2.5 truncate text-fg transition-colors group-hover:text-accent-soft">{title}</div>
{#if subtitle}
<div class="mt-0.5 truncate text-sm text-fg-3">{subtitle}</div>
{/if}
</a>
@@ -11,7 +11,7 @@
let { let {
as = 'div', as = 'div',
min = '200px', min = '165px',
minMobile = '150px', minMobile = '150px',
class: klass = '', class: klass = '',
children children
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import MediaCard from './MediaCard.svelte';
import MixArt from './MixArt.svelte'; import MixArt from './MixArt.svelte';
import { hueFor } from '$lib/theme/color'; import { hueFor } from '$lib/theme/color';
import type { Mix } from '$lib/mixes'; import type { Mix } from '$lib/mixes';
@@ -13,19 +14,13 @@
const count = $derived(Number(mix.itemCount)); const count = $derived(Number(mix.itemCount));
</script> </script>
<a <MediaCard
href="/mixes/{mix.id}" href="/mixes/{mix.id}"
class="group animate-enter min-w-0 text-left" title={mix.title}
style="animation-delay:{index * 45}ms" subtitle="{count} {count === 1 ? 'canción' : 'canciones'}"
{index}
> >
<MixArt {#snippet art(artClass)}
items={mix.items} <MixArt items={mix.items} hue={hueFor(mix.id)} class={artClass} />
hue={hueFor(mix.id)} {/snippet}
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg" </MediaCard>
/>
<div class="mt-2.5 truncate text-fg">{mix.title}</div>
<div class="mt-0.5 truncate text-sm text-fg-3">
{count}
{count === 1 ? 'canción' : 'canciones'}
</div>
</a>
@@ -82,7 +82,7 @@
> >
{#if title} {#if title}
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-lg font-bold tracking-tight text-fg">{title}</h2> <h2 class="text-lg font-semibold tracking-tight text-fg">{title}</h2>
<IconButton label="Cerrar" onclick={onClose}> <IconButton label="Cerrar" onclick={onClose}>
<X class="h-4 w-4" /> <X class="h-4 w-4" />
</IconButton> </IconButton>
@@ -13,7 +13,7 @@
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between {klass}"> <div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between {klass}">
<div class="min-w-0"> <div class="min-w-0">
<h1 class="text-2xl font-bold tracking-tight text-fg sm:text-4xl">{title}</h1> <h1 class="text-2xl font-semibold tracking-tight text-fg sm:text-4xl">{title}</h1>
{#if subtitle} {#if subtitle}
<p class="mt-2 text-sm text-fg-2 sm:text-base">{subtitle}</p> <p class="mt-2 text-sm text-fg-2 sm:text-base">{subtitle}</p>
{/if} {/if}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import MediaCard from './MediaCard.svelte';
import PlaylistArt from './PlaylistArt.svelte'; import PlaylistArt from './PlaylistArt.svelte';
import { hueFor } from '$lib/theme/color'; import { hueFor } from '$lib/theme/color';
@@ -14,20 +15,8 @@
let { id, name, description, trackIds, updatedAt, index = 0 }: Props = $props(); let { id, name, description, trackIds, updatedAt, index = 0 }: Props = $props();
</script> </script>
<a <MediaCard href="/playlists/{id}" title={name} subtitle={description} {index}>
href="/playlists/{id}" {#snippet art(artClass)}
class="group animate-enter min-w-0 text-left" <PlaylistArt playlistId={id} {trackIds} hue={hueFor(id)} version={updatedAt} class={artClass} />
style="animation-delay:{index * 45}ms" {/snippet}
> </MediaCard>
<PlaylistArt
playlistId={id}
{trackIds}
hue={hueFor(id)}
version={updatedAt}
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
/>
<div class="mt-2.5 truncate text-fg">{name}</div>
{#if description}
<div class="mt-0.5 truncate text-sm text-fg-3">{description}</div>
{/if}
</a>
@@ -11,10 +11,12 @@
</script> </script>
{#if actions} {#if actions}
<div class="mb-4 flex items-center justify-between gap-4 sm:mb-6 {klass}"> <div class="mb-3 flex items-baseline justify-between gap-4 sm:mb-4 {klass}">
<h2 class="text-xl font-bold text-fg sm:text-2xl">{title}</h2> <h2 class="text-lg font-semibold tracking-tight text-fg sm:text-xl">{title}</h2>
{@render actions()} {@render actions()}
</div> </div>
{:else} {:else}
<h2 class="mb-4 text-xl font-bold text-fg sm:mb-6 sm:text-2xl {klass}">{title}</h2> <h2 class="mb-3 text-lg font-semibold tracking-tight text-fg sm:mb-4 sm:text-xl {klass}">
{title}
</h2>
{/if} {/if}
@@ -77,7 +77,7 @@
</Modal> </Modal>
<Modal open={confirmingDelete} onClose={() => (confirmingDelete = false)} maxWidth="max-w-sm"> <Modal open={confirmingDelete} onClose={() => (confirmingDelete = false)} maxWidth="max-w-sm">
<h2 class="text-lg font-bold tracking-tight text-fg"> <h2 class="text-lg font-semibold tracking-tight text-fg">
¿Eliminar «{album.title}»? ¿Eliminar «{album.title}»?
</h2> </h2>
<p class="mt-2 text-base text-fg-3"> <p class="mt-2 text-base text-fg-3">
@@ -64,7 +64,7 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm tracking-[0.14em] text-fg-3 uppercase">Álbum</p> <p class="text-sm tracking-[0.14em] text-fg-3 uppercase">Álbum</p>
<h1 <h1
class="mt-1.5 text-3xl font-bold tracking-tight break-words text-fg sm:text-5xl md:text-7xl" class="mt-1.5 text-3xl font-semibold tracking-tight break-words text-fg sm:text-5xl md:text-7xl"
> >
{title} {title}
</h1> </h1>
@@ -103,7 +103,7 @@
{album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'} · YouTube Music {album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'} · YouTube Music
</p> </p>
<h1 <h1
class="mt-1.5 text-3xl font-bold tracking-tight break-words text-fg sm:text-5xl md:text-7xl" class="mt-1.5 text-3xl font-semibold tracking-tight break-words text-fg sm:text-5xl md:text-7xl"
> >
{album.title} {album.title}
</h1> </h1>
@@ -85,7 +85,7 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm tracking-[0.14em] text-fg-3 uppercase">Mezcla</p> <p class="text-sm tracking-[0.14em] text-fg-3 uppercase">Mezcla</p>
<h1 <h1
class="mt-1.5 text-3xl font-bold tracking-tight break-words text-fg sm:text-5xl md:text-7xl" class="mt-1.5 text-3xl font-semibold tracking-tight break-words text-fg sm:text-5xl md:text-7xl"
> >
{mix.title} {mix.title}
</h1> </h1>
@@ -79,7 +79,7 @@
</Modal> </Modal>
<Modal open={confirmingDelete} onClose={() => (confirmingDelete = false)} maxWidth="max-w-sm"> <Modal open={confirmingDelete} onClose={() => (confirmingDelete = false)} maxWidth="max-w-sm">
<h2 class="text-lg font-bold tracking-tight text-fg"> <h2 class="text-lg font-semibold tracking-tight text-fg">
¿Eliminar «{playlist.name}»? ¿Eliminar «{playlist.name}»?
</h2> </h2>
<p class="mt-2 text-base text-fg-3"> <p class="mt-2 text-base text-fg-3">
@@ -46,7 +46,7 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm tracking-[0.14em] text-fg-3 uppercase">Lista</p> <p class="text-sm tracking-[0.14em] text-fg-3 uppercase">Lista</p>
<h1 <h1
class="mt-1.5 text-3xl font-bold tracking-tight break-words text-fg sm:text-5xl md:text-7xl" class="mt-1.5 text-3xl font-semibold tracking-tight break-words text-fg sm:text-5xl md:text-7xl"
> >
{name} {name}
</h1> </h1>
@@ -141,7 +141,7 @@
> >
<Check class="h-8 w-8" strokeWidth={2.5} /> <Check class="h-8 w-8" strokeWidth={2.5} />
</span> </span>
<h2 class="mt-6 text-2xl font-bold tracking-tight text-fg">¡Subida!</h2> <h2 class="mt-6 text-2xl font-semibold tracking-tight text-fg">¡Subida!</h2>
<p class="mt-3 text-fg-2"> <p class="mt-3 text-fg-2">
«{publishedTitle}» se está procesando. Aparecerá lista para reproducir en tu biblioteca en «{publishedTitle}» se está procesando. Aparecerá lista para reproducir en tu biblioteca en
unos momentos. unos momentos.
+6 -5
View File
@@ -178,14 +178,14 @@
></div> ></div>
<div class="animate-enter relative flex flex-col gap-2 sm:gap-2.5"> <div class="animate-enter relative flex flex-col gap-2 sm:gap-2.5">
<p class="text-sm tracking-[0.14em] text-fg-2 uppercase sm:text-base">{data.greeting}</p> <p class="text-sm tracking-[0.14em] text-fg-2 uppercase sm:text-base">{data.greeting}</p>
<h1 class="max-w-160 text-3xl leading-[1.05] font-extrabold text-fg sm:text-5xl"> <h1 class="max-w-160 text-3xl leading-[1.05] font-semibold text-fg sm:text-5xl">
Tu música. Sin límites. Tu música. Sin límites.
</h1> </h1>
</div> </div>
</section> </section>
{#if recentlyPlayed.length > 0} {#if recentlyPlayed.length > 0}
<section class="page-x pt-5 pb-3 sm:pt-6"> <section class="page-x pt-10 sm:pt-14">
<SectionHeading title="Volver a escuchar" /> <SectionHeading title="Volver a escuchar" />
<MediaGrid min="280px" minMobile="220px"> <MediaGrid min="280px" minMobile="220px">
{#each recentlyPlayed as track, i (track.id)} {#each recentlyPlayed as track, i (track.id)}
@@ -228,7 +228,7 @@
{/if} {/if}
{#if mixes.length > 0 || playlists.length > 0} {#if mixes.length > 0 || playlists.length > 0}
<section class="grid gap-x-8 gap-y-5 page-x pt-5 pb-3 sm:pt-6 lg:grid-cols-2"> <section class="grid gap-x-8 gap-y-10 page-x pt-10 sm:pt-14 lg:grid-cols-2">
{#if mixes.length > 0} {#if mixes.length > 0}
<div class="min-w-0"> <div class="min-w-0">
<SectionHeading title="Hechas para ti" /> <SectionHeading title="Hechas para ti" />
@@ -269,7 +269,7 @@
{/if} {/if}
{#if albums.length > 0} {#if albums.length > 0}
<section class="page-x pt-5 pb-3 sm:pt-6"> <section class="page-x pt-10 sm:pt-14">
<SectionHeading title="Álbumes escuchados recientemente"> <SectionHeading title="Álbumes escuchados recientemente">
{#snippet actions()} {#snippet actions()}
<a href="/albums" class="text-sm text-fg-3 transition hover:text-accent-soft">Ver todos</a> <a href="/albums" class="text-sm text-fg-3 transition hover:text-accent-soft">Ver todos</a>
@@ -290,7 +290,7 @@
</section> </section>
{/if} {/if}
<section class="page-x pt-5 pb-10 sm:pt-6"> <section class="page-x pt-10 pb-16 sm:pt-14">
<SectionHeading title="Descubrir" /> <SectionHeading title="Descubrir" />
{#if latest.length > 0} {#if latest.length > 0}
<MediaGrid as="ul"> <MediaGrid as="ul">
@@ -299,6 +299,7 @@
id={targetId(item)} id={targetId(item)}
title={targetTitle(item)} title={targetTitle(item)}
artist={targetArtist(item)} artist={targetArtist(item)}
coverShape="round"
coverSrc={targetCoverSrc(item)} coverSrc={targetCoverSrc(item)}
hue={hueFor(targetId(item))} hue={hueFor(targetId(item))}
explicit={targetExplicit(item)} explicit={targetExplicit(item)}
+2 -2
View File
@@ -13,10 +13,10 @@
<div class="grid min-h-screen place-items-center bg-bg px-6 text-fg"> <div class="grid min-h-screen place-items-center bg-bg px-6 text-fg">
<div class="flex w-full max-w-110 flex-col items-center gap-9 text-center"> <div class="flex w-full max-w-110 flex-col items-center gap-9 text-center">
<div class="text-3xl font-extrabold tracking-tight">Musify</div> <div class="text-3xl font-semibold tracking-tight">Musify</div>
<div> <div>
<h1 class="text-4xl font-extrabold tracking-tight">Tu música. Sin límites.</h1> <h1 class="text-4xl font-semibold tracking-tight">Tu música. Sin límites.</h1>
<p class="mt-3 text-base text-fg-2">Inicia sesión para continuar.</p> <p class="mt-3 text-base text-fg-2">Inicia sesión para continuar.</p>
</div> </div>