Consolidate auth routes under /auth and rename /u to /user
Moves the login screen from /login to /auth, logout from /logout to /auth/logout (alongside the existing /auth/login and /auth/callback endpoints), and the public profile route from /u/[id] to /user/[id]. AUTH_REDIRECT_URI and AUTH_POST_LOGOUT_URI already point at /auth/callback and / respectively, so no Zitadel config change is needed for the auth move.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import {
|
||||
createApiClient,
|
||||
optionalUser,
|
||||
requireAccessTokenAction,
|
||||
unwrapOrError
|
||||
} from '$lib/server/api';
|
||||
|
||||
const PROFILE_PLAYLISTS_PAGE_SIZE = 50;
|
||||
|
||||
export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => {
|
||||
const { allowAnonymousListening } = await parent();
|
||||
const viewer = optionalUser(locals, url, allowAnonymousListening);
|
||||
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||
|
||||
const [profileRes, playlistsRes] = await Promise.all([
|
||||
api.GET('/users/{id}/profile', { params: { path: { id: params.id } } }),
|
||||
api.GET('/playlists/users/{userId}', {
|
||||
params: {
|
||||
path: { userId: params.id },
|
||||
query: { pageNumber: 1, pageSize: PROFILE_PLAYLISTS_PAGE_SIZE, onlyPublic: true }
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
const profile = unwrapOrError(profileRes, 'Usuario no encontrado.', 404);
|
||||
const playlistItems = playlistsRes.data?.items ?? [];
|
||||
|
||||
const tracksByPlaylist = await Promise.all(
|
||||
playlistItems.map((playlist) =>
|
||||
api
|
||||
.GET('/playlists/{playlistId}/tracks', {
|
||||
params: { path: { playlistId: playlist.id }, query: { pageNumber: 1, pageSize: 1 } }
|
||||
})
|
||||
.then((res) => Number(res.data?.totalItemCount ?? 0))
|
||||
)
|
||||
);
|
||||
|
||||
const playlists = playlistItems.map((playlist, i) => ({
|
||||
...playlist,
|
||||
trackCount: tracksByPlaylist[i]
|
||||
}));
|
||||
|
||||
const isOwnProfile = viewer !== null && viewer.sub === params.id;
|
||||
|
||||
const isFollowing =
|
||||
viewer && !isOwnProfile
|
||||
? await api
|
||||
.GET('/users/{id}/is-following', { params: { path: { id: params.id } } })
|
||||
.then((res) => res.data ?? false)
|
||||
.catch(() => false)
|
||||
: false;
|
||||
|
||||
return { profile, playlists, isOwnProfile, isFollowing, isAnonymous: viewer === null };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
follow: async ({ params, locals, fetch }) => {
|
||||
const accessToken = requireAccessTokenAction(locals);
|
||||
if (typeof accessToken !== 'string') return accessToken;
|
||||
|
||||
const api = createApiClient({ fetch, accessToken });
|
||||
await api.POST('/users/{id}/follow', { params: { path: { id: params.id } } });
|
||||
return { following: true };
|
||||
},
|
||||
|
||||
unfollow: async ({ params, locals, fetch }) => {
|
||||
const accessToken = requireAccessTokenAction(locals);
|
||||
if (typeof accessToken !== 'string') return accessToken;
|
||||
|
||||
const api = createApiClient({ fetch, accessToken });
|
||||
await api.DELETE('/users/{id}/follow', { params: { path: { id: params.id } } });
|
||||
return { following: false };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import MediaCard from '$lib/components/ui/media/MediaCard.svelte';
|
||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
||||
import Avatar from '$lib/components/ui/media/Avatar.svelte';
|
||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
||||
import { plural, playlistMeta } from '$lib/utils/format';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
const profile = $derived(data.profile);
|
||||
const playlists = $derived(data.playlists);
|
||||
const canFollow = $derived(!data.isOwnProfile && !data.isAnonymous);
|
||||
|
||||
let following = $state(untrack(() => data.isFollowing));
|
||||
let submitting = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'following' in form && typeof form.following === 'boolean')
|
||||
following = form.following;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{profile.name}</title>
|
||||
<meta name="description" content="Perfil de {profile.name} en Musify." />
|
||||
</svelte:head>
|
||||
|
||||
{#snippet followAction()}
|
||||
<form
|
||||
method="POST"
|
||||
action={following ? '?/unfollow' : '?/follow'}
|
||||
use:enhance={() => {
|
||||
submitting = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
submitting = false;
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant={following ? 'secondary' : 'primary'}
|
||||
disabled={submitting}
|
||||
>
|
||||
{following ? 'Dejar de seguir' : 'Seguir'}
|
||||
</Button>
|
||||
</form>
|
||||
{/snippet}
|
||||
|
||||
<Page>
|
||||
<PageHeader
|
||||
eyebrow="Perfil"
|
||||
title={profile.name}
|
||||
meta="{plural(
|
||||
Number(profile.followersCount),
|
||||
'seguidor',
|
||||
'seguidores'
|
||||
)} · {profile.followingCount} siguiendo"
|
||||
align="center"
|
||||
actions={canFollow ? followAction : undefined}
|
||||
>
|
||||
{#snippet cover()}
|
||||
<Avatar name={profile.name} src={profile.profilePictureUrl} size="hero" />
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
|
||||
<div>
|
||||
<SectionHeading title="Playlists públicas" subtitle="Creadas y compartidas por este usuario" />
|
||||
|
||||
{#if playlists.length > 0}
|
||||
<div class="mt-6 grid-cards-lg">
|
||||
{#each playlists as playlist, i (playlist.id)}
|
||||
<MediaCard
|
||||
href="/playlists/{playlist.id}"
|
||||
title={playlist.name}
|
||||
subtitle={playlistMeta(Number(playlist.trackCount))}
|
||||
trackIds={playlist.coverTrackIds}
|
||||
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
index={i}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<EmptyState
|
||||
icon={ListMusic}
|
||||
title="Sin playlists públicas"
|
||||
description="Este usuario todavía no ha compartido ninguna playlist pública."
|
||||
class="mt-6"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Page>
|
||||
Reference in New Issue
Block a user