[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.

add api, feed, recent reviews on detail page

Florian (Dec 16, 2024, 2:34 AM +0100) b13b761f cc78df32

+216 -124
+7 -8
src/lib/Components/ItemCard.svelte
··· 1 1 <script lang="ts"> 2 - import { enhance } from '$app/forms'; 3 2 import { rateMovieModal, watchedItems } from '$lib/state.svelte'; 4 3 import { cn } from '$lib/utils'; 5 - import { toast } from 'svelte-sonner'; 6 4 import Rating from './Rating.svelte'; 5 + 7 6 const { 8 7 item, 9 8 showMark 10 9 }: { 11 10 item: { 12 11 poster_path: string; 13 - original_title?: string; 14 - original_name?: string; 12 + title?: string; 13 + name?: string; 15 14 movieId?: number; 16 15 showId?: number; 17 16 rating?: number; ··· 27 26 {#if item.poster_path} 28 27 <img 29 28 src="https://image.tmdb.org/t/p/w500{item.poster_path}" 30 - alt="movie poster for {item.original_title ?? item.original_name}" 29 + alt="movie poster for {item.title ?? item.name}" 31 30 class="size-full object-cover object-center lg:size-full" 32 31 /> 33 32 {/if} ··· 45 44 class="absolute inset-0 z-10 bg-gradient-to-b from-transparent via-black/30 to-black/70" 46 45 ></div> 47 46 <div class="absolute bottom-2 left-0 right-0 z-10 flex justify-center"> 48 - <Rating rating={watchedItems.getRating(item).rating ?? 0} /> 47 + <Rating rating={watchedItems.getRating(item)?.rating ?? 0} /> 49 48 </div> 50 49 {:else} 51 50 <button ··· 60 59 movieId: item.movieId, 61 60 showId: item.showId, 62 61 kind: item.movieId ? 'movie' : 'tv', 63 - name: item.original_title ?? item.original_name, 62 + name: item.title ?? item.name, 64 63 currentRating: 0, 65 64 currentReview: '' 66 65 }; ··· 88 87 <a href="/{item.movieId ? 'movie' : 'tv'}/{item.movieId ?? item.showId}"> 89 88 <span aria-hidden="true" class="absolute inset-0"></span> 90 89 <div class="line-clamp-2 max-w-full"> 91 - {item.original_title ?? item.original_name} 90 + {item.title ?? item.name} 92 91 </div> 93 92 </a> 94 93 </h3>
+29 -40
src/lib/Components/ReviewCard.svelte
··· 1 1 <script lang="ts"> 2 + import type { MainRecord } from '$lib/db'; 2 3 import Rating from './Rating.svelte'; 3 4 import RelativeTime from './relative-time/RelativeTime.svelte'; 4 5 5 - let { 6 - review, 7 - item, 8 - user 9 - }: { 10 - review: { 11 - rating: number; 12 - ratingText?: string; 13 - updatedAt: string; 14 - movieId?: number; 15 - showId?: number; 16 - }; 17 - item: { 18 - original_title?: string; 19 - original_name?: string; 20 - kind: string; 21 - poster_path: string; 22 - }; 23 - user: { 24 - displayName?: string; 25 - handle: string; 26 - avatar: string; 27 - }; 28 - } = $props(); 6 + let { data }: { data: MainRecord } = $props(); 29 7 </script> 30 8 31 - <a href={item.movieId ? `/movie/${item.movieId}` : `/tv/${item.showId}`} class="flex w-full max-w-2xl flex-col gap-2 rounded-xl bg-white/5 border border-white/10 p-6 backdrop-blur-sm"> 32 - <div class="flex gap-4 items-center"> 9 + <a 10 + href={data.record.item.ref === 'tmdb:m' 11 + ? `/movie/${data.record.item.value}` 12 + : `/tv/${data.record.item.value}`} 13 + class="flex w-full max-w-2xl flex-col gap-2 rounded-xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm" 14 + > 15 + <div class="flex gap-4"> 33 16 <div 34 17 class="relative z-20 aspect-[2/3] h-44 w-auto shrink-0 overflow-hidden rounded-md border border-base-800 bg-base-900/50 transition-opacity duration-75 group-hover:opacity-75" 35 18 > 36 - {#if item.poster_path} 37 - <img 38 - src="https://image.tmdb.org/t/p/w500{item.poster_path}" 39 - alt="movie poster for {item.original_title ?? item.original_name}" 40 - class="size-full object-cover object-center lg:size-full" 41 - /> 19 + {#if data.record.metadata?.poster_path} 20 + <img 21 + src="https://image.tmdb.org/t/p/w500{data.record.metadata.poster_path}" 22 + alt="movie poster for {data.record.metadata.title}" 23 + class="size-full object-cover object-center lg:size-full" 24 + /> 42 25 {/if} 43 26 </div> 44 27 <div class="flex w-full flex-col justify-center gap-4"> 45 - <div class="flex w-full flex-row sm:items-center gap-4"> 28 + <div class="flex w-full flex-row gap-4 sm:items-center"> 46 29 <div class="flex items-center gap-2"> 47 - {#if user.avatar} 48 - <img src={user.avatar} alt="user avatar" class="size-5 rounded-full" /> 30 + {#if data.author.avatar} 31 + <img src={data.author.avatar} alt="user avatar" class="size-5 rounded-full" /> 49 32 {/if} 50 - <div class="text-md font-semibold">{user.displayName ?? user.handle}</div> 33 + <div class="text-md font-semibold">{data.author.displayName ?? data.author.handle}</div> 51 34 </div> 52 35 <div class="text-sm text-base-500"> 53 - <RelativeTime date={new Date(review.updatedAt)} locale="en-US" /> 36 + <RelativeTime date={new Date(data.updatedAt)} locale="en-US" /> 54 37 </div> 55 38 </div> 56 39 57 40 <div class="flex w-full flex-col gap-4"> 58 41 <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"> 59 - <div class="text-2xl font-bold">{item.original_title ?? item.original_name}</div> 60 - <Rating rating={review.rating} size="size-6" /> 42 + <div class="text-2xl font-bold">{data.record.metadata?.title}</div> 43 + {#if data.record.rating?.value} 44 + <Rating rating={data.record.rating?.value / 2} size="size-6" /> 45 + {/if} 61 46 </div> 62 47 63 - <div class="text-sm text-base-300">{@html review.ratingText?.replace('\n', '<br /><br />')}</div> 48 + {#if data.record.note?.value} 49 + <div class="text-sm text-base-300"> 50 + {@html data.record.note?.value?.replace('\n', '<br /><br />')} 51 + </div> 52 + {/if} 64 53 </div> 65 54 </div> 66 55 </div>
+7
src/lib/Components/Sidebar.svelte
··· 17 17 }, 18 18 { 19 19 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> 20 + <path stroke-linecap="round" stroke-linejoin="round" d="M12.75 3.03v.568c0 .334.148.65.405.864l1.068.89c.442.369.535 1.01.216 1.49l-.51.766a2.25 2.25 0 0 1-1.161.886l-.143.048a1.107 1.107 0 0 0-.57 1.664c.369.555.169 1.307-.427 1.605L9 13.125l.423 1.059a.956.956 0 0 1-1.652.928l-.679-.906a1.125 1.125 0 0 0-1.906.172L4.5 15.75l-.612.153M12.75 3.031a9 9 0 0 0-8.862 12.872M12.75 3.031a9 9 0 0 1 6.69 14.036m0 0-.177-.529A2.25 2.25 0 0 0 17.128 15H16.5l-.324-.324a1.453 1.453 0 0 0-2.328.377l-.036.073a1.586 1.586 0 0 1-.982.816l-.99.282c-.55.157-.894.702-.8 1.267l.073.438c.08.474.49.821.97.821.846 0 1.598.542 1.865 1.345l.215.643m5.276-3.67a9.012 9.012 0 0 1-5.276 3.67m0 0a9 9 0 0 1-10.275-4.835M15.75 9c0 .896-.393 1.7-1.016 2.25" /> 21 + </svg>`, 22 + label: 'Feed', 23 + href: '/feed' 24 + }, 25 + { 26 + icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> 20 27 <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /> 21 28 </svg>`, 22 29 label: 'Search',
+6
src/lib/bluesky.ts
··· 95 95 96 96 return items; 97 97 } 98 + 99 + export async function getReviewById(id: string) { 100 + // Implement the logic to fetch a single review by ID 101 + // This could involve querying a database or an external API 102 + // Return the review data in the expected format 103 + }
+75
src/lib/db.ts
··· 1 + export type MainRecord = { 2 + uri: string; 3 + cid: string; 4 + 5 + author: { 6 + did: string; 7 + handle: string; 8 + displayName?: string; 9 + avatar?: string; 10 + }; 11 + 12 + indexedAt: string; 13 + createdAt: string; 14 + updatedAt: string; 15 + 16 + record: { 17 + $type: string; 18 + item: { 19 + ref: string; 20 + value: string; 21 + }; 22 + note?: { 23 + value: string; 24 + createdAt: string; 25 + updatedAt: string; 26 + }; 27 + rating?: { 28 + value: number; 29 + createdAt: string; 30 + }; 31 + metadata?: { 32 + title: string; 33 + poster_path: string; 34 + backdrop_path: string; 35 + tagline: string; 36 + overview: string; 37 + genres: string[]; 38 + release_date?: string; 39 + }; 40 + crosspost?: { 41 + uri: string; 42 + }; 43 + }; 44 + }; 45 + 46 + export async function getRecentRecordOfUser({ did }: { did: string }): Promise<MainRecord[]> { 47 + const response = await fetch( 48 + `https://skywatched-jetstream.fly.dev/api/recent-records-by-user?did=${did}` 49 + ); 50 + const data = await response.json(); 51 + return data; 52 + } 53 + 54 + export async function getRecentRecords(): Promise<MainRecord[]> { 55 + const response = await fetch(`https://skywatched-jetstream.fly.dev/api/most-recent-records`); 56 + const data = await response.json(); 57 + return data; 58 + } 59 + 60 + export async function getRecentRecordsForItem({ 61 + ref, 62 + value 63 + }: { 64 + ref: string; 65 + value: string; 66 + }): Promise<MainRecord[]> { 67 + const response = await fetch( 68 + `https://skywatched-jetstream.fly.dev/api/recent-records-by-item?ref=${ref}&value=${value}` 69 + ); 70 + console.log( 71 + `https://skywatched-jetstream.fly.dev/api/recent-records-by-item?ref=${ref}&value=${value}` 72 + ); 73 + const data = await response.json(); 74 + return data; 75 + }
+38 -22
src/routes/[kind]/[id]/+page.server.ts
··· 1 + import { getRecentRecordsForItem } from '$lib/db.js'; 1 2 import { getDetails, getRecommendations, getTrailer, getWatchProviders } from '$lib/server/movies'; 2 3 import { error } from '@sveltejs/kit'; 3 4 ··· 10 11 return error(404, 'Not found'); 11 12 } 12 13 13 - if (id) { 14 - let result = await getDetails(id, kind); 14 + if (!id) { 15 + return error(404, 'Not found'); 16 + } 17 + const resultPromise = getDetails(id, kind); 15 18 16 - if (!result || result.success === false) { 17 - return error(404, 'Not found'); 18 - } 19 + const trailerPromise = getTrailer(id, kind); 20 + 21 + const recommendationsPromise = getRecommendations(id, kind); 19 22 20 - result = { 23 + const watchProvidersPromise = getWatchProviders(id, kind); 24 + 25 + const ratingsPromise = getRecentRecordsForItem({ 26 + ref: 'tmdb:' + (kind === 'movie' ? 'm' : 's'), 27 + value: id.toString() 28 + }); 29 + 30 + const [result, trailer, recommendations, watchProviders, ratings] = await Promise.all([ 31 + resultPromise, 32 + trailerPromise, 33 + recommendationsPromise, 34 + watchProvidersPromise, 35 + ratingsPromise 36 + ]); 37 + 38 + if (!result || result.success === false) { 39 + return error(404, 'Not found'); 40 + } 41 + 42 + return { 43 + result: { 21 44 ...result, 22 45 movieId: kind === 'movie' ? id : undefined, 23 46 showId: kind === 'tv' ? id : undefined 24 - }; 25 - 26 - const trailer = await getTrailer(id, kind); 27 - 28 - const recommendations = (await getRecommendations(id, kind)).map((item) => { 47 + }, 48 + trailer, 49 + // @ts-expect-error - TODO: fix this 50 + recommendations: recommendations.map((item) => { 29 51 if (kind === 'movie') { 30 52 return { ...item, movieId: item.id }; 31 53 } else { 32 54 return { ...item, showId: item.id }; 33 55 } 34 - }); 35 - 36 - const watchProviders = await getWatchProviders(id, kind); 37 - 38 - watchProviders.DE?.flatrate?.sort((a, b) => a.display_priority - b.display_priority); 39 - 40 - return { result, trailer, recommendations, kind, watchProviders }; 41 - } 42 - 43 - // not found 44 - return error(404, 'Not found'); 56 + }), 57 + kind, 58 + watchProviders, 59 + ratings 60 + }; 45 61 }
+9 -10
src/routes/[kind]/[id]/+page.svelte
··· 84 84 > 85 85 rate {data.kind === 'movie' ? 'movie' : 'show'} 86 86 </button> 87 - {:else} 88 - <!-- <div class="flex gap-2 text-lg font-semibold"> 89 - your rating: <Rating rating={watchedItems.getRating(data.result)?.rating ?? 0} /> 90 - </div> --> 91 - 92 - <ReviewCard 93 - item={data.result} 94 - review={watchedItems.getRating(data.result)} 95 - user={data.user} 96 - /> 97 87 {/if} 98 88 </div> 99 89 {/if} ··· 121 111 122 112 Trailer 123 113 </button> 114 + {/if} 115 + 116 + {#if data.ratings.length > 0} 117 + <div class="mb-2 mt-8 text-lg font-semibold">recent ratings</div> 118 + <div class="flex flex-col gap-2"> 119 + {#each data.ratings as rating} 120 + <ReviewCard data={rating} /> 121 + {/each} 122 + </div> 124 123 {/if} 125 124 126 125 {#if data.recommendations.length > 0}
+7
src/routes/feed/+page.server.ts
··· 1 + import { getRecentRecords } from '$lib/db'; 2 + 3 + /** @type {import('./$types').PageServerLoad} */ 4 + export async function load() { 5 + const feed = await getRecentRecords(); 6 + return { feed }; 7 + }
+28
src/routes/feed/+page.svelte
··· 1 + <script lang="ts"> 2 + import Container from '$lib/Components/Container.svelte'; 3 + import { onMount } from 'svelte'; 4 + import { type PageData } from './$types'; 5 + import ReviewCard from '$lib/Components/ReviewCard.svelte'; 6 + 7 + let { data }: { data: PageData } = $props(); 8 + 9 + onMount(async () => { 10 + console.log(data); 11 + }); 12 + </script> 13 + 14 + <Container class="relative z-10 py-16"> 15 + <div class="flex flex-col gap-4 px-4"> 16 + <div class="flex flex-col gap-4"> 17 + <h1 class="text-2xl font-bold">Feed</h1> 18 + </div> 19 + </div> 20 + 21 + {#if data.feed.length > 0} 22 + <div class="mx-auto max-w-2xl pt-16 flex flex-col gap-4"> 23 + {#each data.feed as record} 24 + <ReviewCard data={record} /> 25 + {/each} 26 + </div> 27 + {/if} 28 + </Container>
+2 -2
src/routes/search/+page.svelte
··· 7 7 </script> 8 8 9 9 <Container> 10 - <div class="px-4"> 11 - <h1 class="py-8 text-4xl font-bold tracking-tight text-base-50">search for a movie or show</h1> 10 + <div class="px-4 py-16"> 11 + <h1 class="pb-8 text-4xl font-bold tracking-tight text-base-50">search for a movie or show</h1> 12 12 13 13 <form class="relative mt-6 flex items-center" method="GET"> 14 14 <input
+5 -3
src/routes/user/[handle]/+page.server.ts
··· 1 1 import { getProfile, resolveHandle } from '$lib/bluesky.js'; 2 + import { getRecentRecordOfUser } from '$lib/db'; 2 3 3 4 /** @type {import('./$types').PageServerLoad} */ 4 5 export async function load(event) { ··· 6 7 7 8 const did = await resolveHandle({ handle: handle }); 8 9 9 - const profile = await getProfile({ did }); 10 + const profilePromise = getProfile({ did }); 11 + const itemsPromise = getRecentRecordOfUser({ did }); 10 12 11 - const isUser = event.locals.user?.did === did; 13 + const [profile, items] = await Promise.all([profilePromise, itemsPromise]); 12 14 13 - return { isUser, username: event.params.handle, profile }; 15 + return { isUser: event.locals.user?.did === did, username: event.params.handle, profile, items }; 14 16 }
+3 -39
src/routes/user/[handle]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import Container from '$lib/Components/Container.svelte'; 3 - import ItemsGrid from '$lib/Components/ItemsGrid.svelte'; 4 3 import Profile from '$lib/Components/Profile.svelte'; 5 4 import ReviewCard from '$lib/Components/ReviewCard.svelte'; 6 - import { onMount } from 'svelte'; 7 5 8 6 let { data } = $props(); 9 - 10 - let items: { 11 - showId?: number; 12 - movieId?: number; 13 - poster_path?: string; 14 - original_title?: string; 15 - original_name?: string; 16 - rating?: number; 17 - ratingText?: string; 18 - updatedAt: string; 19 - }[] = $state([]); 20 - 21 - let loading = $state(true); 22 - 23 - onMount(async () => { 24 - const response = await fetch(`/api/getRatedWithDetails?did=${data.profile.did}`); 25 - const itemsData = await response.json(); 26 - items = itemsData.items.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); 27 - loading = false; 28 - console.log(items); 29 - }); 30 7 </script> 31 8 32 9 <Container> 33 10 <Profile profile={data.profile} /> 34 11 35 - {#if items.length > 0} 36 - <!-- <ItemsGrid 37 - class="px-4 py-8" 38 - items={items.map((movie) => ({ 39 - poster_path: movie.poster_path ?? '', 40 - original_title: movie.original_title ?? movie.original_name ?? '', 41 - movieId: movie.movieId ?? undefined, 42 - showId: movie.showId ?? undefined, 43 - rating: movie.rating ?? undefined, 44 - ratingText: movie.ratingText ?? undefined 45 - }))} 46 - /> --> 12 + {#if data.items.length > 0} 47 13 <div class="flex flex-col gap-8 w-full items-center py-8 px-4"> 48 - {#each items as item} 49 - <ReviewCard item={item} user={data.profile} review={item} /> 14 + {#each data.items as item} 15 + <ReviewCard data={item} /> 50 16 {/each} 51 17 </div> 52 - {:else if loading} 53 - <p class="text-center text-base-500 py-8">Loading reviews...</p> 54 18 {:else} 55 19 <p class="text-center text-base-500 py-8">No movies or tv shows rated yet.</p> 56 20 {/if}