From 72cbc18a4837f477149da440463e86e5509d8195 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 23 Sep 2026 22:34:56 +0200 Subject: [PATCH] Add follow button next to each user in search results Extract FollowButton and shared follow/unfollow actions, reused by the user profile page. --- .../components/ui/media/FollowButton.svelte | 44 +++++++++++++++++++ web-player/src/lib/server/followActions.ts | 28 ++++++++++++ .../src/routes/(app)/explore/+page.server.ts | 27 +++++++++++- .../src/routes/(app)/explore/+page.svelte | 8 +++- .../routes/(app)/user/[id]/+page.server.ts | 27 ++---------- .../src/routes/(app)/user/[id]/+page.svelte | 35 ++------------- 6 files changed, 110 insertions(+), 59 deletions(-) create mode 100644 web-player/src/lib/components/ui/media/FollowButton.svelte create mode 100644 web-player/src/lib/server/followActions.ts diff --git a/web-player/src/lib/components/ui/media/FollowButton.svelte b/web-player/src/lib/components/ui/media/FollowButton.svelte new file mode 100644 index 0000000..e477343 --- /dev/null +++ b/web-player/src/lib/components/ui/media/FollowButton.svelte @@ -0,0 +1,44 @@ + + +
{ + 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} + + {/if} + +
diff --git a/web-player/src/lib/server/followActions.ts b/web-player/src/lib/server/followActions.ts new file mode 100644 index 0000000..9a7e8df --- /dev/null +++ b/web-player/src/lib/server/followActions.ts @@ -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); diff --git a/web-player/src/routes/(app)/explore/+page.server.ts b/web-player/src/routes/(app)/explore/+page.server.ts index 51f7f37..227e316 100644 --- a/web-player/src/routes/(app)/explore/+page.server.ts +++ b/web-player/src/routes/(app)/explore/+page.server.ts @@ -1,6 +1,7 @@ import type { PageServerLoad, Actions } from './$types'; import { createApiClient, unwrapOrError } from '$lib/server/api'; import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions'; +import { followUserAction, unfollowUserAction } from '$lib/server/followActions'; import { EXPLORE_ALBUMS_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 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 { query, tracks, 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 ?? [] }; }; export const actions: Actions = { addTrack: addTrackAction, - addAlbumToPlaylist: addAlbumToPlaylistAction + addAlbumToPlaylist: addAlbumToPlaylistAction, + follow: followUserAction, + unfollow: unfollowUserAction }; diff --git a/web-player/src/routes/(app)/explore/+page.svelte b/web-player/src/routes/(app)/explore/+page.svelte index 4d90242..28de335 100644 --- a/web-player/src/routes/(app)/explore/+page.svelte +++ b/web-player/src/routes/(app)/explore/+page.svelte @@ -10,6 +10,7 @@ import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte'; import Artwork from '$lib/components/ui/media/Artwork.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 EqBars from '$lib/components/ui/media/EqBars.svelte'; import ListRow from '$lib/components/ui/media/ListRow.svelte'; @@ -400,12 +401,15 @@ diff --git a/web-player/src/routes/(app)/user/[id]/+page.server.ts b/web-player/src/routes/(app)/user/[id]/+page.server.ts index eff5b2c..a936bef 100644 --- a/web-player/src/routes/(app)/user/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/user/[id]/+page.server.ts @@ -1,10 +1,6 @@ import type { PageServerLoad, Actions } from './$types'; -import { - createApiClient, - optionalUser, - requireAccessTokenAction, - unwrapOrError -} from '$lib/server/api'; +import { createApiClient, optionalUser, unwrapOrError } from '$lib/server/api'; +import { followUserAction, unfollowUserAction } from '$lib/server/followActions'; const PROFILE_PLAYLISTS_PAGE_SIZE = 50; @@ -55,21 +51,6 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }; 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 }; - } + follow: followUserAction, + unfollow: unfollowUserAction }; diff --git a/web-player/src/routes/(app)/user/[id]/+page.svelte b/web-player/src/routes/(app)/user/[id]/+page.svelte index 1c57924..5de8dd2 100644 --- a/web-player/src/routes/(app)/user/[id]/+page.svelte +++ b/web-player/src/routes/(app)/user/[id]/+page.svelte @@ -1,29 +1,19 @@ @@ -32,26 +22,7 @@ {#snippet followAction()} -
{ - submitting = true; - return async ({ update }) => { - await update(); - submitting = false; - }; - }} - > - -
+ {/snippet}