Web player general changes #22
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import Button from '../primitives/Button.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
following: boolean;
|
||||||
|
userId?: string;
|
||||||
|
invalidateAll?: boolean;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { following: initial, userId, invalidateAll = false, class: klass = '' }: Props = $props();
|
||||||
|
|
||||||
|
let following = $state(untrack(() => initial));
|
||||||
|
let submitting = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action={following ? '?/unfollow' : '?/follow'}
|
||||||
|
class={klass}
|
||||||
|
use:enhance={() => {
|
||||||
|
submitting = true;
|
||||||
|
return async ({ result, update }) => {
|
||||||
|
if (result.type === 'success' && typeof result.data?.following === 'boolean')
|
||||||
|
following = result.data.following;
|
||||||
|
await update({ reset: false, invalidateAll });
|
||||||
|
submitting = false;
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if userId}
|
||||||
|
<input type="hidden" name="userId" value={userId} />
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
variant={following ? 'secondary' : 'primary'}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
{following ? 'Dejar de seguir' : 'Seguir'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { fail, type RequestEvent } from '@sveltejs/kit';
|
||||||
|
import { createApiClient, requireAccessTokenAction } from '$lib/server/api';
|
||||||
|
|
||||||
|
async function setFollowing({ request, params, locals, fetch }: RequestEvent, follow: boolean) {
|
||||||
|
const accessToken = requireAccessTokenAction(locals);
|
||||||
|
if (typeof accessToken !== 'string') return accessToken;
|
||||||
|
|
||||||
|
const form = await request.formData();
|
||||||
|
const id = String(form.get('userId') ?? '') || String(params.id ?? '');
|
||||||
|
if (!id) return fail(400, { message: 'Falta el usuario.' });
|
||||||
|
|
||||||
|
const api = createApiClient({ fetch, accessToken });
|
||||||
|
const options = { params: { path: { id } } };
|
||||||
|
const { error } = follow
|
||||||
|
? await api.POST('/users/{id}/follow', options)
|
||||||
|
: await api.DELETE('/users/{id}/follow', options);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return fail(502, {
|
||||||
|
message: follow ? 'No se pudo seguir al usuario.' : 'No se pudo dejar de seguir al usuario.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { following: follow, userId: id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const followUserAction = (event: RequestEvent) => setFollowing(event, true);
|
||||||
|
export const unfollowUserAction = (event: RequestEvent) => setFollowing(event, false);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { createApiClient, unwrapOrError } from '$lib/server/api';
|
import { createApiClient, unwrapOrError } from '$lib/server/api';
|
||||||
import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions';
|
import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions';
|
||||||
|
import { followUserAction, unfollowUserAction } from '$lib/server/followActions';
|
||||||
import {
|
import {
|
||||||
EXPLORE_ALBUMS_PAGE_SIZE,
|
EXPLORE_ALBUMS_PAGE_SIZE,
|
||||||
EXPLORE_PAGE_SIZE as PAGE_SIZE,
|
EXPLORE_PAGE_SIZE as PAGE_SIZE,
|
||||||
@@ -45,16 +46,38 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
|||||||
|
|
||||||
const tracks = unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.');
|
const tracks = unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.');
|
||||||
|
|
||||||
|
const userItems = usersRes?.data?.items ?? [];
|
||||||
|
const followedIds = user
|
||||||
|
? await Promise.all(
|
||||||
|
userItems
|
||||||
|
.filter((u) => u.id !== user.sub)
|
||||||
|
.map((u) =>
|
||||||
|
api
|
||||||
|
.GET('/users/{id}/is-following', { params: { path: { id: u.id } } })
|
||||||
|
.then((res) => (res.data ? u.id : null))
|
||||||
|
.catch(() => null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
const followed = new Set(followedIds);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
tracks,
|
tracks,
|
||||||
albums: (albumsRes?.data?.items ?? []).map((item) => item.album),
|
albums: (albumsRes?.data?.items ?? []).map((item) => item.album),
|
||||||
users: usersRes?.data?.items ?? [],
|
users: userItems.map((u) => ({
|
||||||
|
...u,
|
||||||
|
isSelf: user?.sub === u.id,
|
||||||
|
isFollowing: followed.has(u.id)
|
||||||
|
})),
|
||||||
|
canFollow: !!user,
|
||||||
playlists: playlistsRes?.data?.items ?? []
|
playlists: playlistsRes?.data?.items ?? []
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
addTrack: addTrackAction,
|
addTrack: addTrackAction,
|
||||||
addAlbumToPlaylist: addAlbumToPlaylistAction
|
addAlbumToPlaylist: addAlbumToPlaylistAction,
|
||||||
|
follow: followUserAction,
|
||||||
|
unfollow: unfollowUserAction
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte';
|
import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte';
|
||||||
import Artwork from '$lib/components/ui/media/Artwork.svelte';
|
import Artwork from '$lib/components/ui/media/Artwork.svelte';
|
||||||
import Avatar from '$lib/components/ui/media/Avatar.svelte';
|
import Avatar from '$lib/components/ui/media/Avatar.svelte';
|
||||||
|
import FollowButton from '$lib/components/ui/media/FollowButton.svelte';
|
||||||
import PlayButton from '$lib/components/ui/media/PlayButton.svelte';
|
import PlayButton from '$lib/components/ui/media/PlayButton.svelte';
|
||||||
import EqBars from '$lib/components/ui/media/EqBars.svelte';
|
import EqBars from '$lib/components/ui/media/EqBars.svelte';
|
||||||
import ListRow from '$lib/components/ui/media/ListRow.svelte';
|
import ListRow from '$lib/components/ui/media/ListRow.svelte';
|
||||||
@@ -400,12 +401,15 @@
|
|||||||
<SectionHeading title="Usuarios" count={counts.Usuarios} />
|
<SectionHeading title="Usuarios" count={counts.Usuarios} />
|
||||||
<ul class="flex flex-col gap-1">
|
<ul class="flex flex-col gap-1">
|
||||||
{#each capped(sfilter, users) as u (u.id)}
|
{#each capped(sfilter, users) as u (u.id)}
|
||||||
<li>
|
<li class="flex items-center gap-3">
|
||||||
<ListRow title={u.name} href="/user/{u.id}" size="lg">
|
<ListRow title={u.name} href="/user/{u.id}" size="lg" class="min-w-0 flex-1">
|
||||||
{#snippet art()}
|
{#snippet art()}
|
||||||
<Avatar name={u.name} src={u.profilePictureUrl} size="md" />
|
<Avatar name={u.name} src={u.profilePictureUrl} size="md" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ListRow>
|
</ListRow>
|
||||||
|
{#if data.canFollow && !u.isSelf}
|
||||||
|
<FollowButton following={u.isFollowing} userId={u.id} class="shrink-0" />
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import {
|
import { createApiClient, optionalUser, unwrapOrError } from '$lib/server/api';
|
||||||
createApiClient,
|
import { followUserAction, unfollowUserAction } from '$lib/server/followActions';
|
||||||
optionalUser,
|
|
||||||
requireAccessTokenAction,
|
|
||||||
unwrapOrError
|
|
||||||
} from '$lib/server/api';
|
|
||||||
|
|
||||||
const PROFILE_PLAYLISTS_PAGE_SIZE = 50;
|
const PROFILE_PLAYLISTS_PAGE_SIZE = 50;
|
||||||
|
|
||||||
@@ -55,21 +51,6 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
follow: async ({ params, locals, fetch }) => {
|
follow: followUserAction,
|
||||||
const accessToken = requireAccessTokenAction(locals);
|
unfollow: unfollowUserAction
|
||||||
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 };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { untrack } from 'svelte';
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||||
import MediaCard from '$lib/components/ui/media/MediaCard.svelte';
|
import MediaCard from '$lib/components/ui/media/MediaCard.svelte';
|
||||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
import FollowButton from '$lib/components/ui/media/FollowButton.svelte';
|
||||||
import Avatar from '$lib/components/ui/media/Avatar.svelte';
|
import Avatar from '$lib/components/ui/media/Avatar.svelte';
|
||||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||||
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
||||||
import { plural, playlistMeta } from '$lib/utils/format';
|
import { plural, playlistMeta } from '$lib/utils/format';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const profile = $derived(data.profile);
|
const profile = $derived(data.profile);
|
||||||
const playlists = $derived(data.playlists);
|
const playlists = $derived(data.playlists);
|
||||||
const canFollow = $derived(!data.isOwnProfile && !data.isAnonymous);
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -32,26 +22,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#snippet followAction()}
|
{#snippet followAction()}
|
||||||
<form
|
<FollowButton following={data.isFollowing} invalidateAll />
|
||||||
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}
|
{/snippet}
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
|
|||||||
Reference in New Issue
Block a user