From d4bb9d636ae1ed63faefa9c0598e7a2f526b6a43 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 16 Aug 2026 04:15:29 +0200 Subject: [PATCH] likes, season improvements --- src/lib/components/AppMenu.svelte | 2 +- src/lib/components/ItemPage.svelte | 10 +- src/lib/components/MenuItem.svelte | 4 +- src/lib/components/Review.svelte | 95 +++++++++-- src/lib/reviews.server.ts | 47 +++++- src/lib/tmdb.server.ts | 14 +- src/lib/types.ts | 1 + src/routes/[kind]/[id]/+page.server.ts | 4 +- .../[kind]/[id]/season/[season]/+page.svelte | 82 +++++----- src/routes/profile/[actor]/+layout.server.ts | 15 +- src/routes/profile/[actor]/+page.server.ts | 28 ++-- src/routes/profile/[actor]/+page.svelte | 150 ++++++++++++------ .../[actor]/review/[rkey]/+page.svelte | 29 ++-- 13 files changed, 330 insertions(+), 151 deletions(-) diff --git a/src/lib/components/AppMenu.svelte b/src/lib/components/AppMenu.svelte index cd3cc6e..b3c7329 100644 --- a/src/lib/components/AppMenu.svelte +++ b/src/lib/components/AppMenu.svelte @@ -39,7 +39,7 @@ -
+ {#if seasonPoster || data.trailer_url}
{#if seasonPoster} Poster for {data.season.name} of {data.show.title} {/if} + + {#if data.trailer_url} +
+ +
+ {/if}
+ {/if} -
-
- {#if seasonStatus}{seasonStatus}{/if} - {#if seasonStatus && seasonDateRange} - - {/if} - {#if seasonDateRange}{seasonDateRange}{/if} - {#if seasonDateRange}{/if} - {data.season.episodeCount} - {data.season.episodeCount === 1 ? 'episode' : 'episodes'} - {#if data.network} - - {data.network} - {/if} -
-

- {data.season.name} -

-

{data.show.title}

+
+
+ {#if seasonStatus}{seasonStatus}{/if} + {#if seasonStatus && seasonDateRange} + + {/if} + {#if seasonDateRange}{seasonDateRange}{/if} + {#if seasonDateRange}{/if} + {data.season.episodeCount} + {data.season.episodeCount === 1 ? 'episode' : 'episodes'} + {#if data.network} + + {data.network} + {/if}
+

+ {data.season.name} +

+

{data.show.title}

{#if data.season.overview} @@ -154,17 +164,15 @@

Episodes

{#if data.episodes.length > 0} -
+
{#each data.episodes as episode (episode.id)} {@const dayDifference = calendarDayDifference(episode.airDate, data.today)} {@const isUpcoming = dayDifference !== null && dayDifference >= 0}
-
+
{#if episode.still} -
+
{episodeCode(episode.seasonNumber, episode.episodeNumber)} @@ -220,11 +228,7 @@ {/each}
{:else} -
- Episode information has not been announced yet. -
+

Episode information has not been announced yet.

{/if} @@ -238,7 +242,7 @@ id: showId, season: String(data.previousSeason.seasonNumber) })} - class="block rounded-xl border border-white/10 bg-white/[0.05] p-3 transition-colors hover:bg-white/[0.1] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/70" + class="block py-2 transition-opacity hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/70" > Previous Next { const avatar = bskyProfile?.value && 'avatar' in bskyProfile.value ? bskyProfile.value.avatar : undefined; const popfeedBanner = popfeedProfile?.value?.banner; + const legacyPopfeedBannerUrl = + popfeedProfile?.value && 'bannerUrl' in popfeedProfile.value + ? popfeedProfile.value.bannerUrl + : undefined; const bskyBanner = bskyProfile?.value?.banner; - const banner = popfeedBanner ?? bskyBanner; - const bannerUrl = banner - ? getAtprotoCdnImageUrl({ did, blob: banner, preset: 'banner' }) - : undefined; + const bannerUrl = + (popfeedBanner + ? getAtprotoCdnImageUrl({ did, blob: popfeedBanner, preset: 'banner' }) + : undefined) ?? + legacyPopfeedBannerUrl ?? + (bskyBanner ? getAtprotoCdnImageUrl({ did, blob: bskyBanner, preset: 'banner' }) : undefined); return { profile: { did, handle: profileEntry.handle ?? did, displayName: popfeedProfile?.value?.displayName ?? bskyProfile?.value?.displayName, - description: popfeedProfile?.value?.description ?? bskyProfile?.value?.description, avatarUrl: avatar ? getAtprotoCdnImageUrl({ did, blob: avatar, preset: 'avatar' }) : undefined, diff --git a/src/routes/profile/[actor]/+page.server.ts b/src/routes/profile/[actor]/+page.server.ts index 62443e9..60509b8 100644 --- a/src/routes/profile/[actor]/+page.server.ts +++ b/src/routes/profile/[actor]/+page.server.ts @@ -1,14 +1,24 @@ import { error } from '@sveltejs/kit'; import { contrail } from '$lib/contrail'; import { getProfileMediaLists } from '$lib/lists.server'; -import { toReview } from '$lib/reviews.server'; +import { getViewerReviewLikes, toReview } from '$lib/reviews.server'; import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async ({ parent }) => { +export const load: PageServerLoad = async ({ locals, parent }) => { const { profile } = await parent(); - const response = await contrail.get('watch.atmo.review.listRecords', { - params: { actor: profile.did } - }); + const [response, viewerLikes, lists] = await Promise.all([ + contrail.get('watch.atmo.review.listRecords', { + params: { actor: profile.did, limit: 200, order: 'desc' } + }), + getViewerReviewLikes(locals.did).catch((cause) => { + console.error('Could not load viewer review likes from Contrail', cause); + return new Map(); + }), + getProfileMediaLists(profile).catch((cause) => { + console.error('Could not load profile lists from Contrail', cause); + return []; + }) + ]); if (!response.ok) error(502, 'Could not load reviews'); @@ -22,16 +32,12 @@ export const load: PageServerLoad = async ({ parent }) => { ...review.author, displayName: profile.displayName, avatarUrl: profile.avatarUrl - } + }, + viewerLikeUri: viewerLikes.get(review.uri) ?? null } ] : []; }); - const lists = await getProfileMediaLists(profile).catch((cause) => { - console.error('Could not load profile lists from Contrail', cause); - return []; - }); - return { reviews, lists }; }; diff --git a/src/routes/profile/[actor]/+page.svelte b/src/routes/profile/[actor]/+page.svelte index d993fd6..d917139 100644 --- a/src/routes/profile/[actor]/+page.svelte +++ b/src/routes/profile/[actor]/+page.svelte @@ -1,36 +1,24 @@ @@ -41,47 +29,109 @@
- + - {#if profileTabs.length > 0} -
-

@{data.profile.handle}'s activity

- + {#if writtenReviews.length > 0 || ratings.length > 0 || data.lists.length > 0} +
+ {#if writtenReviews.length > 0} +
+

Reviews

+
+ {#each displayedReviews as review (review.uri)} + + {/each} +
+ + {#if !showAllReviews && writtenReviews.length > 3} + + {/if} +
+ {/if} + + {#if ratings.length > 0} +
0 ? 'mt-10 border-t border-white/10 pt-6' : 'mt-6'}`} + > +

Ratings

+
+ {#each mobileRatings as review (review.uri)} + + {/each} +
+ + + {#if !showAllRatings && ratings.length > 6} + + {/if} +
+ {/if} - {#if selectedSection === 'reviews' && writtenReviews.length > 0} -
- {#each writtenReviews as review (review.uri)} - - {/each} -
- {:else if selectedSection === 'ratings' && ratings.length > 0} -
- {#each ratings as review (review.uri)} - - {/each} -
- {:else if selectedSection === 'lists' && data.lists.length > 0} -
- {#each data.lists as list (list.uri)} - - {/each} -
+ {#if data.lists.length > 0} +
0 || ratings.length > 0 ? 'mt-10 border-t border-white/10 pt-6' : 'mt-6'}`} + > +

Lists

+
+ {#each data.lists as list (list.uri)} + + {/each} +
+
{/if} -
+
{:else}

No reviews, ratings, or lists yet.

{/if} diff --git a/src/routes/profile/[actor]/review/[rkey]/+page.svelte b/src/routes/profile/[actor]/review/[rkey]/+page.svelte index a370285..fff517f 100644 --- a/src/routes/profile/[actor]/review/[rkey]/+page.svelte +++ b/src/routes/profile/[actor]/review/[rkey]/+page.svelte @@ -21,6 +21,8 @@ // svelte-ignore state_referenced_locally let viewerLikeUri = $state(data.viewerLikeUri); // svelte-ignore state_referenced_locally + let liked = $state(Boolean(data.viewerLikeUri)); + // svelte-ignore state_referenced_locally let likeCount = $state(data.likeCount); let spoilerRevealed = $state(false); @@ -63,18 +65,27 @@ } if (liking) return; + const previousLikeUri = viewerLikeUri; + const wasLiked = liked; + const previousLikeCount = likeCount; + liked = !wasLiked; + likeCount = wasLiked ? Math.max(0, likeCount - 1) : likeCount + 1; liking = true; + try { - if (viewerLikeUri) { - await unlikeReview({ reviewUri: data.review.uri, likeUri: viewerLikeUri }); + if (wasLiked) { + if (!previousLikeUri) throw new Error('Could not find your like.'); + await unlikeReview({ reviewUri: data.review.uri, likeUri: previousLikeUri }); viewerLikeUri = null; - likeCount = Math.max(0, likeCount - 1); } else { const result = await likeReview({ reviewUri: data.review.uri }); viewerLikeUri = result.uri; - if (result.created) likeCount += 1; + if (!result.created) likeCount = previousLikeCount; } } catch (cause) { + viewerLikeUri = previousLikeUri; + liked = wasLiked; + likeCount = previousLikeCount; interactionError = cause instanceof Error ? cause.message : 'Could not update like.'; } finally { liking = false; @@ -181,15 +192,15 @@ type="button" onclick={toggleLike} disabled={liking} - aria-pressed={Boolean(viewerLikeUri)} - aria-label={`${viewerLikeUri ? 'Unlike' : 'Like'} this review${likeCount > 0 ? `, ${likeCount} ${likeCount === 1 ? 'like' : 'likes'}` : ''}`} - class={`inline-flex items-center gap-1.5 text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent-400 disabled:cursor-wait disabled:opacity-60 ${ - viewerLikeUri ? 'text-accent-400 hover:text-accent-300' : 'hover:text-white' + aria-pressed={liked} + aria-label={`${liked ? 'Unlike' : 'Like'} this review${likeCount > 0 ? `, ${likeCount} ${likeCount === 1 ? 'like' : 'likes'}` : ''}`} + class={`inline-flex items-center gap-1.5 text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent-400 disabled:cursor-wait ${ + liked ? 'text-accent-400 hover:text-accent-300' : 'hover:text-white' }`} >