[READ-ONLY] Mirror of https://github.com/flo-bit/skywatched. review movies and tv shows, based on at proto skywatched.app
0

Configure Feed

Select the types of activity you want to include in your feed.

cast added

xota1999 (Dec 17, 2024, 9:51 PM +0100) 5d0fb3a1 97ff7e04

+202 -13
+1
src/lib/Components/ItemCard.svelte
··· 28 28 src="https://image.tmdb.org/t/p/w342{item.poster_path}" 29 29 alt="movie poster for {item.title ?? item.name}" 30 30 class="size-full object-cover object-center lg:size-full" 31 + loading="lazy" 31 32 /> 32 33 {/if} 33 34
+48
src/lib/server/movies.ts
··· 179 179 ]) 180 180 ); 181 181 } 182 + 183 + export async function getCast(id: number, kind: Kind) { 184 + const url = `https://api.themoviedb.org/3/${kind}/${id}/credits?language=en-US`; 185 + const options = { 186 + method: 'GET', 187 + headers: { 188 + accept: 'application/json', 189 + Authorization: `Bearer ${env.TMDB_API_KEY}` 190 + } 191 + }; 192 + 193 + const response = await fetch(url, options); 194 + const data = await response.json(); 195 + 196 + return data.cast; 197 + } 198 + 199 + export async function getPersonDetails(personId: number) { 200 + const url = `https://api.themoviedb.org/3/person/${personId}?language=en-US`; 201 + const options = { 202 + method: 'GET', 203 + headers: { 204 + accept: 'application/json', 205 + Authorization: `Bearer ${env.TMDB_API_KEY}` 206 + } 207 + }; 208 + 209 + const response = await fetch(url, options); 210 + const data = await response.json(); 211 + 212 + return data; 213 + } 214 + 215 + export async function getCombinedCredits(personId: number) { 216 + const url = `https://api.themoviedb.org/3/person/${personId}/combined_credits?language=en-US`; 217 + const options = { 218 + method: 'GET', 219 + headers: { 220 + accept: 'application/json', 221 + Authorization: `Bearer ${env.TMDB_API_KEY}` 222 + } 223 + }; 224 + 225 + const response = await fetch(url, options); 226 + const data = await response.json(); 227 + 228 + return data; 229 + }
+14 -4
src/routes/[kind]/[id]/+page.server.ts
··· 1 1 import { getRecentRecordsForItem } from '$lib/db.js'; 2 - import { getDetails, getRecommendations, getTrailer, getWatchProviders } from '$lib/server/movies'; 2 + import { 3 + getCast, 4 + getDetails, 5 + getRecommendations, 6 + getTrailer, 7 + getWatchProviders 8 + } from '$lib/server/movies'; 3 9 import { error } from '@sveltejs/kit'; 4 10 5 11 /** @type {import('./$types').PageServerLoad} */ ··· 27 33 value: id.toString() 28 34 }); 29 35 30 - const [result, trailer, recommendations, watchProviders, ratings] = await Promise.all([ 36 + const castPromise = getCast(id, kind); 37 + 38 + const [result, trailer, recommendations, watchProviders, ratings, cast] = await Promise.all([ 31 39 resultPromise, 32 40 trailerPromise, 33 41 recommendationsPromise, 34 42 watchProvidersPromise, 35 - ratingsPromise 43 + ratingsPromise, 44 + castPromise 36 45 ]); 37 46 38 47 if (!result || result.success === false) { ··· 56 65 }), 57 66 kind, 58 67 watchProviders, 59 - ratings 68 + ratings, 69 + cast 60 70 }; 61 71 }
+33 -9
src/routes/[kind]/[id]/+page.svelte
··· 6 6 import Container from '$lib/Components/Container.svelte'; 7 7 import ItemsList from '$lib/Components/ItemsList.svelte'; 8 8 import ReviewList from '$lib/Components/ReviewList.svelte'; 9 + import Avatar from '$lib/Components/Avatar.svelte'; 9 10 10 11 let { data }: { data: PageData } = $props(); 11 12 </script> ··· 67 68 /> 68 69 <div class="fixed inset-0 h-full w-full bg-black/50"></div> 69 70 70 - <Container class="relative z-10"> 71 + <Container class="relative z-10 pt-4 pb-8"> 71 72 <div class="flex gap-4 px-4 pt-8"> 72 73 <img 73 74 src="https://image.tmdb.org/t/p/w500{data.result.poster_path}" ··· 114 115 </div> 115 116 </div> 116 117 117 - <div class="px-4 pb-8 pt-4 text-sm text-white"> 118 + <div class="px-4 pt-4 text-sm text-white"> 118 119 <div class="mb-4 flex gap-2 sm:hidden"> 119 120 {@render buttons()} 120 121 </div> ··· 122 123 <div class="mb-2 text-lg font-semibold">overview</div> 123 124 {data.result.overview} 124 125 </div> 126 + </div> 125 127 126 - {#if data.recommendations.length > 0} 127 - <div class="mb-2 mt-8 text-lg font-semibold">recommendations</div> 128 + {#if data.ratings.length > 0} 129 + <div class="mt-8 px-4 text-lg font-semibold">reviews</div> 130 + 131 + <ReviewList reviews={data.ratings} showMovieDetails={false} class="" /> 132 + {/if} 133 + 134 + {#if data.recommendations.length > 0} 135 + <div class="px-4 pt-4 text-sm text-white"> 136 + <div class="mb-2 text-lg font-semibold">recommendations</div> 128 137 129 138 <ItemsList items={data.recommendations} showMark={!!data.user} /> 130 - {/if} 131 - </div> 139 + </div> 140 + {/if} 132 141 133 - {#if data.ratings.length > 0} 134 - <div class="mb-2 mt-8 px-4 text-lg font-semibold">recent reviews</div> 142 + {#if data.cast.length > 0} 143 + <div class="px-4 pb-8 pt-4 text-sm text-white"> 144 + <div class="mb-2 text-lg font-semibold">cast</div> 135 145 136 - <ReviewList reviews={data.ratings} showMovieDetails={false} class="pb-8" /> 146 + <div class={cn('flex gap-x-6 overflow-x-auto')}> 147 + {#each data.cast as castMember} 148 + <a href={`/cast/${castMember.id}`} class="flex flex-col gap-1 items-center"> 149 + <Avatar 150 + src={castMember.profile_path 151 + ? 'https://image.tmdb.org/t/p/w500' + castMember.profile_path 152 + : undefined} 153 + size="size-32" 154 + /> 155 + <div class="text-xs font-medium text-center">{castMember.name}</div> 156 + <div class="text-xs text-base-400 text-center">{castMember.character}</div> 157 + </a> 158 + {/each} 159 + </div> 160 + </div> 137 161 {/if} 138 162 </Container>
+39
src/routes/cast/[id]/+page.server.ts
··· 1 + import { getCombinedCredits, getPersonDetails } from '$lib/server/movies'; 2 + import { error } from '@sveltejs/kit'; 3 + 4 + /** @type {import('./$types').PageServerLoad} */ 5 + export async function load(event) { 6 + const id = parseInt(event.params.id); 7 + 8 + if (!id) { 9 + return error(404, 'Not found'); 10 + } 11 + 12 + const combinedCredits = await getCombinedCredits(id); 13 + const personDetails = await getPersonDetails(id); 14 + 15 + const creditsSet = new Set<string>(); 16 + 17 + combinedCredits.cast = combinedCredits.cast 18 + .filter((item: { id: string; poster_path: string }) => { 19 + if (creditsSet.has(item.id)) { 20 + return false; 21 + } 22 + creditsSet.add(item.id); 23 + return item.poster_path; 24 + }) 25 + .sort((a: { order: number }, b: { order: number }) => b.order - a.order) 26 + .map((item: { id: string; media_type: string }) => { 27 + return { 28 + ...item, 29 + movieId: item.media_type === 'movie' ? item.id : undefined, 30 + showId: item.media_type === 'tv' ? item.id : undefined 31 + }; 32 + }); 33 + 34 + console.log(combinedCredits.cast.length); 35 + return { 36 + combinedCredits: combinedCredits.cast, 37 + personDetails 38 + }; 39 + }
+67
src/routes/cast/[id]/+page.svelte
··· 1 + <script lang="ts"> 2 + import Avatar from '$lib/Components/Avatar.svelte'; 3 + import Container from '$lib/Components/Container.svelte'; 4 + import ItemsGrid from '$lib/Components/ItemsGrid.svelte'; 5 + import { onMount } from 'svelte'; 6 + 7 + let { data } = $props(); 8 + 9 + function calculateAge(birthday: string) { 10 + // set birthday time to 00:00:00 11 + const birthdayDate = new Date(birthday); 12 + birthdayDate.setHours(0, 0, 0, 0); 13 + 14 + // set current time to 00:00:00 15 + const currentDate = new Date(); 16 + currentDate.setHours(0, 0, 0, 0); 17 + 18 + const ageDifMs = currentDate.getTime() - birthdayDate.getTime(); 19 + const ageDate = new Date(ageDifMs); 20 + return Math.abs(ageDate.getUTCFullYear() - 1970); 21 + } 22 + 23 + onMount(() => { 24 + console.log(data.combinedCredits.length); 25 + }); 26 + 27 + let showFullBiography = $state(false); 28 + </script> 29 + 30 + <Container class="relative z-10 pb-8 pt-4"> 31 + <div class="flex items-center gap-4 px-4 pt-8"> 32 + <Avatar 33 + src={data.personDetails.profile_path 34 + ? 'https://image.tmdb.org/t/p/w500' + data.personDetails.profile_path 35 + : undefined} 36 + size="size-44" 37 + /> 38 + <div class="flex flex-col gap-2"> 39 + <div class="max-w-xl text-2xl font-semibold text-white sm:text-4xl"> 40 + {data.personDetails.name} 41 + </div> 42 + {#if data.personDetails.birthday} 43 + <div class="text-sm text-base-300"> 44 + {calculateAge(data.personDetails.birthday)} years old 45 + </div> 46 + {/if} 47 + </div> 48 + </div> 49 + <div class="px-4 pt-4 text-sm text-white"> 50 + <div class="mb-8 max-w-2xl text-sm text-white"> 51 + <div class="mb-2 text-lg font-semibold">overview</div> 52 + <div class={showFullBiography ? '' : 'line-clamp-4'}> 53 + {data.personDetails.biography} 54 + </div> 55 + {#if !showFullBiography} 56 + <button class="text-accent-400 font-semibold mt-1" onclick={() => (showFullBiography = true)}> 57 + show more 58 + </button> 59 + {/if} 60 + </div> 61 + 62 + <div class="mb-4 max-w-2xl text-sm text-white"> 63 + <div class="mb-2 text-lg font-semibold">appearing in</div> 64 + <ItemsGrid items={data.combinedCredits} /> 65 + </div> 66 + </div> 67 + </Container>