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

added tv shows

xota1999 (Dec 9, 2024, 5:47 PM +0100) 75dd50e4 021c88bb

+323 -273
+77
src/lib/Components/ItemCard.svelte
··· 1 + <script lang="ts"> 2 + import { enhance } from '$app/forms'; 3 + import { watchedItems } from '$lib/state.svelte'; 4 + import { cn } from '$lib/utils'; 5 + const { 6 + item, 7 + showMark 8 + }: { 9 + item: { 10 + poster_path: string; 11 + original_title?: string; 12 + original_name?: string; 13 + movieId?: number; 14 + showId?: number; 15 + }; 16 + showMark?: boolean; 17 + } = $props(); 18 + </script> 19 + 20 + <div class="group relative"> 21 + <div 22 + class="pointer-events-none relative z-20 aspect-[2/3] h-auto min-h-44 w-full overflow-hidden rounded-md border border-base-800 bg-base-900/50 transition-opacity duration-75 group-hover:opacity-75 sm:min-h-64" 23 + > 24 + {#if item.poster_path} 25 + <img 26 + src="https://image.tmdb.org/t/p/w500{item.poster_path}" 27 + alt="movie poster for {item.original_title}" 28 + class="size-full object-cover object-center lg:size-full" 29 + /> 30 + {/if} 31 + 32 + {#if showMark} 33 + <form method="post" action="/?/mark" use:enhance> 34 + <input type="hidden" name="id" value={item.movieId ?? item.showId} /> 35 + <input type="hidden" name="kind" value={item.movieId ? 'movie' : 'tv'} /> 36 + 37 + <button 38 + class={cn( 39 + 'pointer-events-auto absolute bottom-2 right-2 z-20 rounded-full border border-base-50/20 bg-black/30 p-2 text-base-50 backdrop-blur-sm group-hover:block sm:hidden', 40 + watchedItems.hasWatched(item) 41 + ? 'border-green-500/20 bg-green-900/60 text-green-500 sm:block' 42 + : '' 43 + )} 44 + onclick={() => { 45 + if (watchedItems.hasWatched(item)) { 46 + watchedItems.removeWatched(item); 47 + } else { 48 + watchedItems.addWatched(item); 49 + } 50 + }} 51 + > 52 + <span class="sr-only">mark as watched</span> 53 + <svg 54 + xmlns="http://www.w3.org/2000/svg" 55 + fill="none" 56 + viewBox="0 0 24 24" 57 + stroke-width="2.5" 58 + stroke="currentColor" 59 + class="size-5" 60 + > 61 + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 62 + </svg> 63 + </button> 64 + </form> 65 + {/if} 66 + </div> 67 + <div class="mt-2 flex justify-between"> 68 + <h3 class="sm:text-md text-sm font-medium text-base-50"> 69 + <a href="/{item.movieId ? 'movie' : 'tv'}/{item.movieId ?? item.showId}"> 70 + <span aria-hidden="true" class="absolute inset-0"></span> 71 + <div class="line-clamp-2 max-w-full"> 72 + {item.original_title ?? item.original_name} 73 + </div> 74 + </a> 75 + </h3> 76 + </div> 77 + </div>
+25
src/lib/Components/ItemsGrid.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '../utils'; 3 + import ItemCard from './ItemCard.svelte'; 4 + 5 + const { 6 + items, 7 + class: className, 8 + showMark, 9 + }: { 10 + items: { poster_path: string; original_title: string; movieId?: number; showId?: number }[]; 11 + class?: string; 12 + showMark?: boolean; 13 + } = $props(); 14 + </script> 15 + 16 + <div 17 + class={cn( 18 + 'mt-6 grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 xl:gap-x-8', 19 + className 20 + )} 21 + > 22 + {#each items as item} 23 + <ItemCard {item} {showMark} /> 24 + {/each} 25 + </div>
+25
src/lib/Components/ItemsList.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '../utils'; 3 + import ItemCard from './ItemCard.svelte'; 4 + 5 + const { 6 + items, 7 + class: className, 8 + showMark, 9 + }: { 10 + items: { poster_path: string; original_title: string; movieId?: number; showId?: number }[]; 11 + class?: string; 12 + showMark?: boolean; 13 + } = $props(); 14 + </script> 15 + 16 + <div 17 + class={cn( 18 + 'flex gap-x-6 gap-y-10 overflow-x-auto', 19 + className 20 + )} 21 + > 22 + {#each items as item} 23 + <ItemCard {item} {showMark} /> 24 + {/each} 25 + </div>
-67
src/lib/Components/MovieCard.svelte
··· 1 - <script lang="ts"> 2 - import { enhance } from '$app/forms'; 3 - import { cn } from '$lib/utils'; 4 - const { 5 - movie, 6 - showMark, 7 - watchedMovies 8 - }: { 9 - movie: { poster_path: string; original_title: string; movieId: number }; 10 - showMark?: boolean; 11 - watchedMovies?: Set<number>; 12 - } = $props(); 13 - </script> 14 - 15 - <div class="group relative"> 16 - <div 17 - class="pointer-events-none relative z-20 aspect-[2/3] h-auto min-h-44 sm:min-h-64 w-full overflow-hidden rounded-md border border-base-800 bg-base-900/50 transition-opacity duration-75 group-hover:opacity-75" 18 - > 19 - {#if movie.poster_path} 20 - <img 21 - src="https://image.tmdb.org/t/p/w500{movie.poster_path}" 22 - alt="movie poster for {movie.original_title}" 23 - class="size-full object-cover object-center lg:size-full" 24 - /> 25 - {/if} 26 - 27 - {#if showMark} 28 - <form method="post" action="?/mark" use:enhance> 29 - <input type="hidden" name="id" value={movie.movieId} /> 30 - 31 - <button 32 - class={cn( 33 - 'pointer-events-auto absolute bottom-2 right-2 z-20 sm:hidden rounded-full border border-base-50/20 bg-black/30 p-2 text-base-50 backdrop-blur-sm group-hover:block', 34 - watchedMovies?.has(movie.movieId) 35 - ? 'sm:block border-green-500/20 bg-green-900/60 text-green-500' 36 - : '' 37 - )} 38 - onclick={() => { 39 - watchedMovies?.add(movie.movieId); 40 - }} 41 - > 42 - <span class="sr-only">mark as watched</span> 43 - <svg 44 - xmlns="http://www.w3.org/2000/svg" 45 - fill="none" 46 - viewBox="0 0 24 24" 47 - stroke-width="2.5" 48 - stroke="currentColor" 49 - class="size-5" 50 - > 51 - <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 52 - </svg> 53 - </button> 54 - </form> 55 - {/if} 56 - </div> 57 - <div class="mt-2 flex justify-between"> 58 - <h3 class="sm:text-md text-sm font-medium text-base-50"> 59 - <a href="/movie/{movie.movieId}"> 60 - <span aria-hidden="true" class="absolute inset-0"></span> 61 - <div class="line-clamp-2 max-w-full"> 62 - {movie.original_title} 63 - </div> 64 - </a> 65 - </h3> 66 - </div> 67 - </div>
-27
src/lib/Components/MovieGrid.svelte
··· 1 - <script lang="ts"> 2 - import { cn } from '../utils'; 3 - import MovieCard from './MovieCard.svelte'; 4 - 5 - const { 6 - movies, 7 - class: className, 8 - showMark, 9 - watchedMovies 10 - }: { 11 - movies: { poster_path: string; original_title: string; movieId: number }[]; 12 - class?: string; 13 - showMark?: boolean; 14 - watchedMovies?: Set<number>; 15 - } = $props(); 16 - </script> 17 - 18 - <div 19 - class={cn( 20 - 'mt-6 grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 xl:gap-x-8', 21 - className 22 - )} 23 - > 24 - {#each movies as movie} 25 - <MovieCard {movie} {showMark} {watchedMovies} /> 26 - {/each} 27 - </div>
-27
src/lib/Components/MovieList.svelte
··· 1 - <script lang="ts"> 2 - import { cn } from '../utils'; 3 - import MovieCard from './MovieCard.svelte'; 4 - 5 - const { 6 - movies, 7 - class: className, 8 - showMark, 9 - watchedMovies 10 - }: { 11 - movies: { poster_path: string; original_title: string; id: number }[]; 12 - class?: string; 13 - showMark?: boolean; 14 - watchedMovies?: Set<number>; 15 - } = $props(); 16 - </script> 17 - 18 - <div 19 - class={cn( 20 - 'flex gap-x-6 gap-y-10 overflow-x-auto', 21 - className 22 - )} 23 - > 24 - {#each movies as movie} 25 - <MovieCard {movie} {showMark} {watchedMovies} /> 26 - {/each} 27 - </div>
-2
src/lib/Components/VideoPlayer.svelte
··· 7 7 8 8 9 9 $effect(() => { 10 - console.log('id', id); 11 - 12 10 const player = new Plyr('.js-player', { 13 11 settings: ['captions', 'quality', 'loop', 'controls'] 14 12 });
+4 -3
src/lib/server/db/schema.ts
··· 15 15 expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull() 16 16 }); 17 17 18 - export const movies = sqliteTable('movies', { 18 + export const items = sqliteTable('items', { 19 19 id: text('id').primaryKey(), 20 - movieId: integer('movie_id').notNull(), 20 + movieId: integer('movie_id'), 21 + showId: integer('show_id'), 21 22 username: text('username') 22 23 .notNull() 23 24 .references(() => user.username), ··· 33 34 34 35 export type User = typeof user.$inferSelect; 35 36 36 - export type Movies = typeof movies.$inferSelect; 37 + export type Movies = typeof items.$inferSelect;
+35 -15
src/lib/server/movies.ts
··· 3 3 import * as table from '$lib/server/db/schema'; 4 4 import { eq, and } from 'drizzle-orm'; 5 5 6 - export async function searchMovie(query: string) { 7 - const apiUrl = `https://api.themoviedb.org/3/search/movie?query=${query}&include_adult=false&language=en-US&page=1`; 6 + export type Kind = 'movie' | 'tv'; 7 + 8 + export async function searchMulti(query: string) { 9 + const apiUrl = `https://api.themoviedb.org/3/search/multi?query=${query}&include_adult=false&language=en-US&page=1`; 8 10 const options = { 9 11 method: 'GET', 10 12 headers: { ··· 19 21 return data.results; 20 22 } 21 23 22 - export async function getDetails(id: number) { 23 - const url = `https://api.themoviedb.org/3/movie/${id}?language=en-US`; 24 + export async function getDetails(id: number, kind: Kind) { 25 + const url = `https://api.themoviedb.org/3/${kind}/${id}?language=en-US`; 24 26 const options = { 25 27 method: 'GET', 26 28 headers: { ··· 35 37 return data; 36 38 } 37 39 38 - export async function getTrailer(id: number): Promise<string | null> { 39 - const url = `https://api.themoviedb.org/3/movie/${id}/videos?language=en-US`; 40 + export async function getTrailer(id: number, kind: Kind): Promise<string | null> { 41 + const url = `https://api.themoviedb.org/3/${kind}/${id}/videos?language=en-US`; 40 42 const options = { 41 43 method: 'GET', 42 44 headers: { ··· 63 65 return trailer?.key ?? null; 64 66 } 65 67 66 - export async function getRecommendations(id: number) { 67 - const url = `https://api.themoviedb.org/3/movie/${id}/recommendations?language=en-US`; 68 + export async function getRecommendations(id: number, kind: Kind) { 69 + const url = `https://api.themoviedb.org/3/${kind}/${id}/recommendations?language=en-US`; 68 70 const options = { 69 71 method: 'GET', 70 72 headers: { ··· 79 81 return data.results; 80 82 } 81 83 82 - export async function checkWatched(id: number, username: string): Promise<boolean | number> { 84 + export async function checkWatched( 85 + id: number, 86 + username: string, 87 + kind: Kind 88 + ): Promise<boolean | number> { 83 89 const movie = await db 84 90 .select() 85 - .from(table.movies) 86 - .where(and(eq(table.movies.movieId, id), eq(table.movies.username, username))); 91 + .from(table.items) 92 + .where( 93 + and( 94 + eq(kind === 'movie' ? table.items.movieId : table.items.showId, id), 95 + eq(table.items.username, username) 96 + ) 97 + ); 87 98 88 99 if (movie.length > 0) return movie[0].watched; 89 100 ··· 92 103 93 104 export async function getWatchedMoviesIds(username: string) { 94 105 const movies = await db 95 - .select({ movieId: table.movies.movieId }) 96 - .from(table.movies) 97 - .where(and(eq(table.movies.username, username), eq(table.movies.watched, 1))); 106 + .select({ movieId: table.items.movieId }) 107 + .from(table.items) 108 + .where(and(eq(table.items.username, username), eq(table.items.watched, 1))); 109 + 110 + return new Set(movies.map((movie) => movie.movieId ?? 0)); 111 + } 98 112 99 - return new Set(movies.map((movie) => movie.movieId)); 113 + export async function getWatchedShowsIds(username: string) { 114 + const shows = await db 115 + .select({ showId: table.items.showId }) 116 + .from(table.items) 117 + .where(and(eq(table.items.username, username), eq(table.items.watched, 1))); 118 + 119 + return new Set(shows.map((show) => show.showId ?? 0)); 100 120 }
+24
src/lib/state.svelte.ts
··· 1 + export const watchedItems = $state({ 2 + watchedMovies: new Set<number>(), 3 + watchedShows: new Set<number>(), 4 + 5 + hasWatched: (item: { movieId?: number; showId?: number }) => { 6 + const id = item.movieId ?? item.showId; 7 + if (!id) return false; 8 + return item.movieId ? watchedItems.watchedMovies.has(id) : watchedItems.watchedShows.has(id); 9 + }, 10 + 11 + addWatched: (item: { movieId?: number; showId?: number }) => { 12 + const id = item.movieId ?? item.showId; 13 + if (!id) return; 14 + if (item.movieId) watchedItems.watchedMovies.add(id); 15 + else watchedItems.watchedShows.add(id); 16 + }, 17 + 18 + removeWatched: (item: { movieId?: number; showId?: number }) => { 19 + const id = item.movieId ?? item.showId; 20 + if (!id) return; 21 + if (item.movieId) watchedItems.watchedMovies.delete(id); 22 + else watchedItems.watchedShows.delete(id); 23 + } 24 + });
+3 -2
src/routes/+layout.server.ts
··· 1 - import { getWatchedMoviesIds } from '$lib/server/movies'; 1 + import { getWatchedMoviesIds, getWatchedShowsIds } from '$lib/server/movies'; 2 2 import type { LayoutServerLoad } from './$types'; 3 3 4 4 export const load: LayoutServerLoad = async (event) => { 5 5 if (event.locals.user) { 6 6 const watchedMovies = await getWatchedMoviesIds(event.locals.user.username); 7 + const watchedShows = await getWatchedShowsIds(event.locals.user.username); 7 8 8 - return { user: event.locals.user, watchedMovies }; 9 + return { user: event.locals.user, watchedMovies, watchedShows }; 9 10 } 10 11 11 12 return {};
+4
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import Logo from '$lib/Components/Logo.svelte'; 3 + import { watchedItems } from '$lib/state.svelte'; 3 4 import '../app.css'; 4 5 let { children, data } = $props(); 6 + 7 + watchedItems.watchedMovies = new Set(data.watchedMovies); 8 + watchedItems.watchedShows = new Set(data.watchedShows); 5 9 </script> 6 10 7 11 <header class="absolute inset-x-0 top-0 z-50">
+41
src/routes/+page.server.ts
··· 1 + import { db } from '$lib/server/db'; 2 + import { checkWatched, getDetails, type Kind } from '$lib/server/movies'; 3 + import type { Actions } from './$types'; 4 + import * as table from '$lib/server/db/schema'; 5 + import { eq, and } from 'drizzle-orm'; 6 + 7 + export const actions: Actions = { 8 + mark: async (event) => { 9 + const username = event.locals.user?.username as string; 10 + const formData = await event.request.formData(); 11 + const id = parseInt(formData.get('id') as string); 12 + const kind = formData.get('kind') as Kind; 13 + 14 + const result = await getDetails(id, kind); 15 + 16 + const watched = await checkWatched(id, username, kind); 17 + 18 + if (watched !== false) { 19 + await db 20 + .update(table.items) 21 + .set({ watched: watched !== 0 ? 0 : 1 }) 22 + .where( 23 + and( 24 + eq(kind === 'movie' ? table.items.movieId : table.items.showId, id), 25 + eq(table.items.username, username) 26 + ) 27 + ); 28 + } else { 29 + await db.insert(table.items).values({ 30 + username, 31 + id: crypto.randomUUID(), 32 + movieId: kind === 'movie' ? id : null, 33 + showId: kind === 'tv' ? id : null, 34 + watched: 1, 35 + originalTitle: result.original_title ?? result.original_name, 36 + posterPath: result.poster_path, 37 + timestamp: new Date() 38 + }); 39 + } 40 + } 41 + };
+41
src/routes/[kind]/[id]/+page.server.ts
··· 1 + import { getDetails, getRecommendations, getTrailer } 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 + const kind = event.params.kind; 8 + 9 + if (kind !== 'movie' && kind !== 'tv') { 10 + return error(404, 'Not found'); 11 + } 12 + 13 + if (id) { 14 + let result = await getDetails(id, kind); 15 + 16 + if (!result || result.success === false) { 17 + return error(404, 'Not found'); 18 + } 19 + 20 + result = { 21 + ...result, 22 + movieId: kind === 'movie' ? id : undefined, 23 + showId: kind === 'tv' ? id : undefined 24 + }; 25 + 26 + const trailer = await getTrailer(id, kind); 27 + 28 + const recommendations = (await getRecommendations(id, kind)).map((item) => { 29 + if (kind === 'movie') { 30 + return { ...item, movieId: item.id }; 31 + } else { 32 + return { ...item, showId: item.id }; 33 + } 34 + }); 35 + 36 + return { result, trailer, recommendations, kind }; 37 + } 38 + 39 + // not found 40 + return error(404, 'Not found'); 41 + }
-69
src/routes/movie/[id]/+page.server.ts
··· 1 - import { db } from '$lib/server/db'; 2 - import { checkWatched, getDetails, getRecommendations, getTrailer } from '$lib/server/movies'; 3 - import type { Actions } from './$types'; 4 - import * as table from '$lib/server/db/schema'; 5 - import { eq, and } from 'drizzle-orm'; 6 - import { error } from '@sveltejs/kit'; 7 - 8 - /** @type {import('./$types').PageServerLoad} */ 9 - export async function load(event) { 10 - const id = parseInt(event.params.id); 11 - 12 - if (id) { 13 - const result = await getDetails(id); 14 - 15 - if (!result || result.success === false) { 16 - return error(404, 'Not found'); 17 - } 18 - 19 - const trailer = await getTrailer(id); 20 - 21 - const recommendations = (await getRecommendations(id)).map((movie) => { 22 - return { ...movie, movieId: movie.id }; 23 - }); 24 - 25 - let watched = false; 26 - 27 - if (event.locals.user?.username) { 28 - watched = (await checkWatched(id, event.locals.user?.username)) as boolean; 29 - } 30 - 31 - return { result, watched, trailer, recommendations }; 32 - } 33 - 34 - // not found 35 - return error(404, 'Not found'); 36 - } 37 - 38 - export const actions: Actions = { 39 - mark: async (event) => { 40 - const username = event.locals.user?.username as string; 41 - const movieId = parseInt((await event.request.formData()).get('id') as string); 42 - 43 - const result = await getDetails(movieId); 44 - 45 - // check if user has watched this movie 46 - const movie = await db 47 - .select() 48 - .from(table.movies) 49 - .where(and(eq(table.movies.movieId, movieId), eq(table.movies.username, username))); 50 - const watched = await checkWatched(movieId, username); 51 - 52 - if (watched !== false) { 53 - await db 54 - .update(table.movies) 55 - .set({ watched: movie[0].watched !== 0 ? 0 : 1 }) 56 - .where(eq(table.movies.movieId, movieId)); 57 - } else { 58 - await db.insert(table.movies).values({ 59 - username, 60 - id: crypto.randomUUID(), 61 - movieId: movieId, 62 - watched: 1, 63 - originalTitle: result.original_title, 64 - posterPath: result.poster_path, 65 - timestamp: new Date() 66 - }); 67 - } 68 - } 69 - };
+19 -12
src/routes/movie/[id]/+page.svelte src/routes/[kind]/[id]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { type PageData } from './$types'; 3 3 import { enhance } from '$app/forms'; 4 + import { cn } from '$lib/utils'; 5 + import { watchedItems } from '$lib/state.svelte'; 6 + 4 7 import Container from '$lib/Components/Container.svelte'; 5 8 import VideoPlayer from '$lib/Components/VideoPlayer.svelte'; 6 - import { cn } from '$lib/utils'; 7 - import MovieList from '$lib/Components/MovieList.svelte'; 9 + import ItemsList from '$lib/Components/ItemsList.svelte'; 8 10 import Rating from '$lib/Components/Rating.svelte'; 9 11 10 12 let { data }: { data: PageData } = $props(); 11 - 12 - let hoveringWatchButton = $state(false); 13 13 </script> 14 14 15 15 <img ··· 28 28 /> 29 29 <div class="flex flex-col gap-4"> 30 30 <div class="max-w-xl text-2xl font-semibold text-white sm:text-4xl"> 31 - {data.result.original_title} 31 + {data.result.original_title ?? data.result.original_name} 32 32 </div> 33 33 {#if data.user} 34 34 <div class="flex gap-4"> 35 - <form method="post" action="?/mark" use:enhance> 35 + <form method="post" action="/?/mark" use:enhance> 36 36 <input type="hidden" name="id" value={data.result.id} /> 37 - 37 + <input type="hidden" name="kind" value={data.kind} /> 38 + 38 39 <button 39 - onmouseenter={() => (hoveringWatchButton = true)} 40 - onmouseleave={() => (hoveringWatchButton = false)} 41 40 class={cn( 42 41 'inline-flex items-center gap-2 rounded-md bg-white/10 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm transition-all duration-75 hover:bg-white/20 ', 43 - data.watched ? 'bg-green-500/10 text-green-400 hover:bg-green-500/20' : '' 42 + watchedItems.hasWatched(data.result) ? 'bg-green-500/10 text-green-400 hover:bg-green-500/20' : '' 44 43 )} 44 + 45 + onclick={() => { 46 + if (watchedItems.hasWatched(data.result)) { 47 + watchedItems.removeWatched(data.result); 48 + } else { 49 + watchedItems.addWatched(data.result); 50 + } 51 + }} 45 52 > 46 - {#if data.watched} 53 + {#if watchedItems.hasWatched(data.result)} 47 54 <svg 48 55 xmlns="http://www.w3.org/2000/svg" 49 56 fill="none" ··· 81 88 {#if data.recommendations.length > 0} 82 89 <div class="mb-2 mt-8 text-lg font-semibold">recommendations</div> 83 90 84 - <MovieList movies={data.recommendations} showMark={!!data.user} watchedMovies={data.watchedMovies} /> 91 + <ItemsList items={data.recommendations} showMark={!!data.user} /> 85 92 {/if} 86 93 </Container>
+12 -36
src/routes/search/+page.server.ts
··· 1 - import { db } from '$lib/server/db'; 2 - import { checkWatched, getDetails, searchMovie } from '$lib/server/movies'; 3 - import type { Actions } from './$types'; 4 - import * as table from '$lib/server/db/schema'; 5 - import { and, eq } from 'drizzle-orm'; 1 + import { searchMulti } from '$lib/server/movies'; 6 2 import { redirect } from '@sveltejs/kit'; 7 3 8 4 /** @type {import('./$types').PageServerLoad} */ 9 5 export async function load(event) { 10 - // redirect to / if not logged in 11 6 if (!event.locals.user) { 12 7 return redirect(302, '/login'); 13 8 } ··· 15 10 const query = event.url.searchParams.get('query'); 16 11 17 12 if (query) { 18 - const results = await searchMovie(query); 13 + const results = (await searchMulti(query)) 14 + .map((result) => { 15 + if (result.media_type === 'movie') { 16 + return { ...result, movieId: result.id }; 17 + } else if (result.media_type === 'tv') { 18 + return { ...result, showId: result.id }; 19 + } 20 + return null; 21 + }) 22 + .filter((result) => result !== null && result.poster_path !== null); 19 23 20 - return { results: results.map((result) => ({ ...result, movieId: result.id })), query }; 24 + return { results, query }; 21 25 } 22 26 23 27 return { results: [], query }; 24 28 } 25 - 26 - export const actions: Actions = { 27 - mark: async (event) => { 28 - const username = event.locals.user?.username as string; 29 - const movieId = parseInt((await event.request.formData()).get('id') as string); 30 - 31 - const result = await getDetails(movieId); 32 - 33 - const watched = await checkWatched(movieId, username); 34 - 35 - if (watched !== false) { 36 - await db 37 - .update(table.movies) 38 - .set({ watched: watched ? 0 : 1 }) 39 - .where(and(eq(table.movies.movieId, movieId), eq(table.movies.username, username))); 40 - } else { 41 - await db.insert(table.movies).values({ 42 - username, 43 - id: crypto.randomUUID(), 44 - movieId: movieId, 45 - watched: 1, 46 - originalTitle: result.original_title, 47 - posterPath: result.poster_path, 48 - timestamp: new Date() 49 - }); 50 - } 51 - } 52 - };
+3 -3
src/routes/search/+page.svelte
··· 1 1 <script lang="ts"> 2 - import MovieGrid from '$lib/Components/MovieGrid.svelte'; 2 + import ItemsGrid from '$lib/Components/ItemsGrid.svelte'; 3 3 import Container from '$lib/Components/Container.svelte'; 4 4 5 5 import { type PageData } from './$types'; ··· 8 8 9 9 <Container> 10 10 <div> 11 - <h1 class="my-8 text-4xl font-bold tracking-tight text-base-50">add a movie</h1> 11 + <h1 class="my-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 ··· 29 29 {#if data.results.length > 0} 30 30 <h2 class="mt-8 text-2xl font-bold tracking-tight text-base-50">results</h2> 31 31 32 - <MovieGrid movies={data.results} showMark={!!data.user} watchedMovies={data.watchedMovies} /> 32 + <ItemsGrid items={data.results} showMark={!!data.user} /> 33 33 {:else if data.query} 34 34 <p class="text-md mt-8 text-base-50">no results found</p> 35 35 {/if}
+4 -4
src/routes/user/[username]/+page.server.ts
··· 6 6 export async function load(event) { 7 7 const username = event.params.username; 8 8 9 - const movies = await db.select().from(table.movies).where(eq(table.movies.username, username)); 9 + const movies = await db.select().from(table.items).where(eq(table.items.username, username)); 10 10 11 11 // filter out movies that are not watched 12 - const watchedMovies = movies.filter((movie) => movie.watched === 1); 12 + const watchedItems = movies.filter((movie) => movie.watched === 1); 13 13 14 14 // sort movies by timestamp 15 - watchedMovies.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 15 + watchedItems.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 16 16 17 17 let isUser = false; 18 18 if (event.locals.user?.username === username) { 19 19 isUser = true; 20 20 } 21 21 22 - return { movies: watchedMovies, isUser, username }; 22 + return { items: watchedItems, isUser, username }; 23 23 }
+6 -6
src/routes/user/[username]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import Container from '$lib/Components/Container.svelte'; 3 - import MovieGrid from '$lib/Components/MovieGrid.svelte'; 3 + import ItemsGrid from '$lib/Components/ItemsGrid.svelte'; 4 4 5 5 let { data } = $props(); 6 6 </script> ··· 13 13 class="text-md rounded-md bg-white/10 px-3.5 py-2.5 font-semibold text-white shadow-sm hover:bg-white/20" 14 14 href="/search" 15 15 > 16 - add movie 16 + add movie or show 17 17 </a> 18 18 {/if} 19 19 20 - <MovieGrid 20 + <ItemsGrid 21 21 class="mt-8" 22 - movies={data.movies.map((movie) => ({ 22 + items={data.items.map((movie) => ({ 23 23 poster_path: movie.posterPath ?? '', 24 24 original_title: movie.originalTitle, 25 - id: movie.id, 26 - movieId: movie.movieId 25 + movieId: movie.movieId ?? undefined, 26 + showId: movie.showId ?? undefined, 27 27 }))} 28 28 /> 29 29 </Container>