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

Merge pull request #108 from gree45/main

follow and game impleentatio

authored by

Florian and committed by
GitHub
(Feb 12, 2025, 2:06 AM +0100) cd9840bb df9b26ad

+1019 -215
+2
.env.example
··· 7 7 # API key for https://www.themoviedb.org/ 8 8 TMDB_API_KEY="" 9 9 10 + DATABASE_AUTH_TOKEN="" 11 + 10 12 # A random key for encrypting the session 11 13 # on unix systems you can generate one with `openssl rand -base64 32` 12 14 NYX_PASSWORD=""
+4 -1
Dockerfile
··· 18 18 # set env variables 19 19 ENV DATABASE_URL=file:local.db 20 20 ENV DATABASE_AUTH_TOKEN=a 21 + ENV TMDB_API_KEY=a 22 + ENV BACKEND_URL=a 23 + ENV NYX_PASSWORD=a 21 24 22 25 # Install packages needed to build node modules 23 26 RUN apt-get update -qq && \ ··· 46 49 COPY --from=build /app/package.json /app 47 50 48 51 # Start the server by default, this can be overwritten at runtime 49 - EXPOSE 3000 52 + EXPOSE 8080 50 53 CMD [ "npm", "run", "start" ]
+1 -1
package.json
··· 67 67 "svelte-sonner": "^0.3.28", 68 68 "tailwind-merge": "^2.5.4" 69 69 } 70 - } 70 + }
+2 -2
src/lib/Components/Items/BlueskyPost/Avatar.svelte
··· 13 13 {#snippet avatar()} 14 14 <div 15 15 class={[ 16 - 'border-base-400/50 bg-base-100 dark:border-base-700 dark:bg-base-800 overflow-hidden rounded-full border', 16 + 'overflow-hidden rounded-full border border-base-400/50 bg-base-100 dark:border-base-700 dark:bg-base-800', 17 17 size, 18 18 className 19 19 ]} ··· 22 22 <img loading="lazy" class="h-full w-full object-cover" {src} {alt} /> 23 23 {:else} 24 24 <svg 25 - class="text-base-300 dark:text-base-600 size-full" 25 + class="size-full text-base-300 dark:text-base-600" 26 26 fill="currentColor" 27 27 viewBox="0 0 24 24" 28 28 >
+10 -10
src/lib/Components/Items/BlueskyPost/Comment.svelte
··· 13 13 </script> 14 14 15 15 {#snippet top(expand: boolean)} 16 - <div class="text-base-600 dark:text-base-500 -ml-6 flex items-center text-sm"> 16 + <div class="-ml-6 flex items-center text-sm text-base-600 dark:text-base-500"> 17 17 <div class="relative size-6"> 18 18 <Avatar 19 19 src={comment.post.author.avatar} ··· 43 43 <a 44 44 target="_blank" 45 45 rel="noopener noreferrer nofollow" 46 - class="dark:text-base-100 hover:text-base-500 dark:hover:text-base-300 ml-2" 46 + class="ml-2 hover:text-base-500 dark:text-base-100 dark:hover:text-base-300" 47 47 href={`https://bsky.app/profile/${comment.post.author.did}`} 48 48 > 49 49 {comment.post.author.displayName || comment.post.author.handle} 50 50 </a> 51 51 52 - <div class="text-base-400 ml-2 text-xs"> 52 + <div class="ml-2 text-xs text-base-400"> 53 53 <RelativeTime date={new Date(comment.post.record.createdAt)} locale="en" /> 54 54 </div> 55 55 </div> ··· 57 57 58 58 <div class="relative pl-3"> 59 59 <button 60 - class="group absolute top-0 -left-1.5 flex h-full w-3 cursor-pointer items-center" 60 + class="group absolute -left-1.5 top-0 flex h-full w-3 cursor-pointer items-center" 61 61 onclick={() => (expanded = !expanded)} 62 62 > 63 63 <div 64 - class="bg-base-200 dark:bg-base-800 group-hover:bg-base-300 dark:group-hover:bg-base-700 mx-auto h-full w-0.5" 64 + class="mx-auto h-full w-0.5 bg-base-200 group-hover:bg-base-300 dark:bg-base-800 dark:group-hover:bg-base-700" 65 65 ></div> 66 66 <span class="sr-only">collapse comment</span> 67 67 </button> ··· 77 77 <Embed post={comment.post} /> 78 78 {/if} 79 79 80 - <div class="text-base-500 dark:text-base-400 mt-2 flex gap-8"> 80 + <div class="mt-2 flex gap-8 text-base-500 dark:text-base-400"> 81 81 <a 82 82 href={atUriToPostUri(comment.post.uri)} 83 83 target="_blank" ··· 90 90 viewBox="0 0 24 24" 91 91 stroke-width="1.5" 92 92 stroke="currentColor" 93 - class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 93 + class="-m-1.5 size-7 rounded-full p-1.5 transition-all duration-100 group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400" 94 94 > 95 95 <path 96 96 stroke-linecap="round" ··· 114 114 viewBox="0 0 24 24" 115 115 stroke-width="1.5" 116 116 stroke="currentColor" 117 - class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 117 + class="-m-1.5 size-7 rounded-full p-1.5 transition-all duration-100 group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400" 118 118 > 119 119 <path 120 120 stroke-linecap="round" ··· 137 137 viewBox="0 0 24 24" 138 138 stroke-width="1.5" 139 139 stroke="currentColor" 140 - class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 140 + class="-m-1.5 size-7 rounded-full p-1.5 transition-all duration-100 group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400" 141 141 > 142 142 <path 143 143 stroke-linecap="round" ··· 155 155 href={atUriToPostUri(comment.post.uri)} 156 156 target="_blank" 157 157 rel="noopener noreferrer nofollow" 158 - class="text-base-500 dark:text-base-400 dark:hover:text-base-300 hover:text-base-600 text-sm font-medium" 158 + class="text-sm font-medium text-base-500 hover:text-base-600 dark:text-base-400 dark:hover:text-base-300" 159 159 >View more replies on bluesky</a 160 160 > 161 161 {/if}
+30 -30
src/lib/Components/Items/BlueskyPost/Comments.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from "svelte"; 3 - import { getUserPosts, getComments, getCommentCount } from "./utils"; 4 - import Comment from "./Comment.svelte"; 2 + import { onMount } from 'svelte'; 3 + import { getUserPosts, getComments, getCommentCount } from './utils'; 4 + import Comment from './Comment.svelte'; 5 5 6 - let { uri, user, comments, url } = $props(); 6 + let { uri, user, comments, url } = $props(); 7 7 8 - let postUri = $state(uri); 8 + let postUri = $state(uri); 9 9 10 - onMount(async () => { 11 - if (!uri && user) { 12 - let posts = await getUserPosts(user); 10 + onMount(async () => { 11 + if (!uri && user) { 12 + let posts = await getUserPosts(user); 13 13 14 - // @ts-expect-error: weird type fuckery 15 - const post = posts.find((post) => post.post.embed?.external?.uri === url); 14 + // @ts-expect-error: weird type fuckery 15 + const post = posts.find((post) => post.post.embed?.external?.uri === url); 16 16 17 - if (post) { 18 - postUri = post.post.uri; 19 - comments = await getComments(post.post.uri); 20 - } 21 - } else if (uri) { 22 - comments = await getComments(uri); 23 - } 24 - }); 17 + if (post) { 18 + postUri = post.post.uri; 19 + comments = await getComments(post.post.uri); 20 + } 21 + } else if (uri) { 22 + comments = await getComments(uri); 23 + } 24 + }); 25 25 </script> 26 26 27 27 <div class="not-prose mt-2"> 28 - {#if comments.length > 0} 29 - <div class="text-sm text-base-950 dark:text-base-100 font-semibold"> 30 - {getCommentCount(comments)} comments, sorted by newest first 31 - </div> 32 - {/if} 28 + {#if comments.length > 0} 29 + <div class="text-sm font-semibold text-base-950 dark:text-base-100"> 30 + {getCommentCount(comments)} comments, sorted by newest first 31 + </div> 32 + {/if} 33 33 34 - {#if comments.length > 0} 35 - <div class="pt-4"> 36 - {#each comments as comment} 37 - <Comment {comment} /> 38 - {/each} 39 - </div> 40 - {/if} 34 + {#if comments.length > 0} 35 + <div class="pt-4"> 36 + {#each comments as comment} 37 + <Comment {comment} /> 38 + {/each} 39 + </div> 40 + {/if} 41 41 </div>
+3 -3
src/lib/Components/Items/BlueskyPost/Likes.svelte
··· 37 37 38 38 {#if postUri} 39 39 <div class="not-prose flex flex-col gap-4"> 40 - <div class="text-base-950 dark:text-base-100 text-sm font-semibold"> 40 + <div class="text-sm font-semibold text-base-950 dark:text-base-100"> 41 41 {postLikesCount} like{postLikesCount === 1 ? '' : 's'} 42 42 </div> 43 43 ··· 46 46 <a 47 47 href={`https://bsky.app/profile/${user.actor.handle}`} 48 48 class={[ 49 - 'ring-base-50 dark:ring-base-900 bg-base-950 relative inline-block size-12 overflow-hidden rounded-full ring-2', 49 + 'relative inline-block size-12 overflow-hidden rounded-full bg-base-950 ring-2 ring-base-50 dark:ring-base-900', 50 50 index === 0 ? '-ml-2' : '' 51 51 ]} 52 52 target="_blank" ··· 62 62 63 63 {#if postLikesData.length < postLikesCount} 64 64 <div 65 - class="text-accent-700 dark:text-accent-300 bg-accent-100 dark:bg-accent-950 ring-base-50 dark:ring-base-900 z-10 mb-4 flex size-12 items-center justify-center rounded-full text-sm font-semibold ring-2" 65 + class="z-10 mb-4 flex size-12 items-center justify-center rounded-full bg-accent-100 text-sm font-semibold text-accent-700 ring-2 ring-base-50 dark:bg-accent-950 dark:text-accent-300 dark:ring-base-900" 66 66 > 67 67 +{postLikesCount - postLikesData.length} 68 68 </div>
+4 -4
src/lib/Components/Items/BlueskyPost/embeds/External.svelte
··· 7 7 </script> 8 8 9 9 <article 10 - class="bg-base-900 ring-base-800 relative isolate flex aspect-16/9 h-64 flex-col justify-end rounded-2xl p-4 ring-1" 10 + class="aspect-16/9 relative isolate flex h-64 flex-col justify-end rounded-2xl bg-base-900 p-4 ring-1 ring-base-800" 11 11 > 12 12 {#if data.thumb} 13 13 <img ··· 17 17 /> 18 18 {/if} 19 19 <div 20 - class="from-base-950/90 via-base-950/40 absolute inset-0 -z-10 rounded-xl bg-linear-to-t" 20 + class="bg-linear-to-t absolute inset-0 -z-10 rounded-xl from-base-950/90 via-base-950/40" 21 21 ></div> 22 22 23 23 <div 24 - class="inset-shadow-md inset-ring-base-950/50 inset-shadow-base-950/40 ring-base-800 absolute inset-0 -z-10 h-full w-full rounded-2xl inset-ring-1 ring-1" 24 + class="inset-shadow-md inset-ring-base-950/50 inset-shadow-base-950/40 inset-ring-1 absolute inset-0 -z-10 h-full w-full rounded-2xl ring-1 ring-base-800" 25 25 ></div> 26 26 27 - <div class="text-base-300 flex flex-wrap items-center gap-y-1 overflow-hidden text-sm/6"> 27 + <div class="flex flex-wrap items-center gap-y-1 overflow-hidden text-sm/6 text-base-300"> 28 28 <div>{domain}</div> 29 29 </div> 30 30 <h3 class="mt-1 text-lg/6 font-semibold text-white">
+2 -2
src/lib/Components/Items/BlueskyPost/embeds/Images.svelte
··· 3 3 </script> 4 4 5 5 {#each data as { fullsize: string; alt: string; thumb: string; aspectRatio: { width: number; height: number } }[] as image} 6 - <div class="ring-base-800 relative h-fit w-fit rounded-2xl ring-1"> 6 + <div class="relative h-fit w-fit rounded-2xl ring-1 ring-base-800"> 7 7 <img 8 8 loading="lazy" 9 9 src={image.thumb} ··· 14 14 /> 15 15 16 16 <div 17 - class="inset-shadow-md inset-shadow-base-950/30 inset-ring-base-950/10 absolute inset-0 h-full w-full rounded-2xl inset-ring-2" 17 + class="inset-shadow-md inset-shadow-base-950/30 inset-ring-base-950/10 inset-ring-2 absolute inset-0 h-full w-full rounded-2xl" 18 18 ></div> 19 19 </div> 20 20 {/each}
+30 -21
src/lib/Components/Items/BlueskyPost/embeds/Video.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from "svelte"; 3 - import Hls from "hls.js"; 2 + import { onMount } from 'svelte'; 3 + import Hls from 'hls.js'; 4 4 5 - const { data } = $props(); 5 + const { data } = $props(); 6 6 7 - import Plyr from "plyr"; 7 + import Plyr from 'plyr'; 8 8 9 - onMount(() => { 10 - if (Hls.isSupported()) { 11 - var hls = new Hls(); 12 - hls.loadSource(data.playlist); 13 - hls.attachMedia(element); 14 - } 9 + onMount(() => { 10 + if (Hls.isSupported()) { 11 + var hls = new Hls(); 12 + hls.loadSource(data.playlist); 13 + hls.attachMedia(element); 14 + } 15 15 16 - new Plyr(element, { 17 - controls: ["play-large", "play", "progress", "current-time", "mute", "volume", "fullscreen", "settings"], 18 - hideControls: true 19 - }); 20 - }); 16 + new Plyr(element, { 17 + controls: [ 18 + 'play-large', 19 + 'play', 20 + 'progress', 21 + 'current-time', 22 + 'mute', 23 + 'volume', 24 + 'fullscreen', 25 + 'settings' 26 + ], 27 + hideControls: true 28 + }); 29 + }); 21 30 22 - let element: HTMLMediaElement; 31 + let element: HTMLMediaElement; 23 32 </script> 24 33 25 34 <svelte:head> 26 - <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" /> 35 + <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" /> 27 36 </svelte:head> 28 37 29 - <div class="max-w-full w-full aspect-video rounded-2xl overflow-hidden border border-base-700"> 30 - <video bind:this={element}> 31 - <track kind="captions" /> 32 - </video> 38 + <div class="aspect-video w-full max-w-full overflow-hidden rounded-2xl border border-base-700"> 39 + <video bind:this={element}> 40 + <track kind="captions" /> 41 + </video> 33 42 </div> 34 43 35 44 <style>
+9 -9
src/lib/Components/Items/BlueskyPost/relative-time/RelativeTime.svelte
··· 1 1 <script lang="ts"> 2 - import { onDestroy } from 'svelte' 3 - import { register, unregister } from './state' 2 + import { onDestroy } from 'svelte'; 3 + import { register, unregister } from './state'; 4 4 5 - export let date: Date | number 6 - export let locale: string 7 - export let live = true 5 + export let date: Date | number; 6 + export let locale: string; 7 + export let live = true; 8 8 9 - let instance = new Object() 10 - let text = '' 9 + let instance = new Object(); 10 + let text = ''; 11 11 12 - register(instance, date, locale, live, value => ({ text } = value)) 12 + register(instance, date, locale, live, (value) => ({ text } = value)); 13 13 14 - onDestroy(() => unregister(instance)) 14 + onDestroy(() => unregister(instance)); 15 15 </script> 16 16 17 17 <span class={$$props.class}>{text}</span>
+15 -15
src/lib/Components/Items/BlueskyPost/relative-time/action.ts
··· 1 - import type { Callback } from './render' 2 - import { register, unregister } from './state' 1 + import type { Callback } from './render'; 2 + import { register, unregister } from './state'; 3 3 4 4 export interface Options { 5 - date: Date | number 6 - locale?: string 7 - live?: boolean 5 + date: Date | number; 6 + locale?: string; 7 + live?: boolean; 8 8 } 9 9 10 10 export function relativeTime(node: HTMLElement, options: Options) { 11 - const callback: Callback = ({ text }) => (node.textContent = text) 11 + const callback: Callback = ({ text }) => (node.textContent = text); 12 12 13 13 function init(options: Options) { 14 - const date = options.date 15 - const locale = options.locale || navigator.language 16 - const live = (options.live = true) 14 + const date = options.date; 15 + const locale = options.locale || navigator.language; 16 + const live = (options.live = true); 17 17 18 - register(node, date, locale, live, callback) 18 + register(node, date, locale, live, callback); 19 19 } 20 20 21 - init(options) 21 + init(options); 22 22 23 23 return { 24 24 update(options: Options) { 25 - init(options) 25 + init(options); 26 26 }, 27 27 destroy() { 28 - unregister(node) 29 - }, 30 - } 28 + unregister(node); 29 + } 30 + }; 31 31 }
+5 -5
src/lib/Components/Items/BlueskyPost/relative-time/formatter.ts
··· 1 1 // keep a cache of formatter per locale, to avoid re-creating them (GC) 2 - const formatters = new Map<string, Intl.RelativeTimeFormat>() 2 + const formatters = new Map<string, Intl.RelativeTimeFormat>(); 3 3 4 4 // get the Intl.RelativeTimeFormat formatter for the given locale 5 5 export function getFormatter(locale: string) { 6 6 if (formatters.has(locale)) { 7 - return formatters.get(locale)! 7 + return formatters.get(locale)!; 8 8 } 9 - const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' }) 10 - formatters.set(locale, formatter) 11 - return formatter 9 + const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' }); 10 + formatters.set(locale, formatter); 11 + return formatter; 12 12 }
+4 -4
src/lib/Components/Items/BlueskyPost/relative-time/index.ts
··· 1 - export * from './action' 2 - export type { Callback } from './render' 3 - export { register, unregister } from './state' 4 - export { default as default } from './RelativeTime.svelte' 1 + export * from './action'; 2 + export type { Callback } from './render'; 3 + export { register, unregister } from './state'; 4 + export { default as default } from './RelativeTime.svelte';
+35 -22
src/lib/Components/Items/BlueskyPost/relative-time/render.ts
··· 1 - export type Callback = (result: { seconds: number; count: number; units: Intl.RelativeTimeFormatUnit; text: string }) => void 1 + export type Callback = (result: { 2 + seconds: number; 3 + count: number; 4 + units: Intl.RelativeTimeFormatUnit; 5 + text: string; 6 + }) => void; 2 7 3 8 export interface RenderState { 4 - date: Date | number 5 - callback: Callback 6 - formatter: Intl.RelativeTimeFormat 9 + date: Date | number; 10 + callback: Callback; 11 + formatter: Intl.RelativeTimeFormat; 7 12 } 8 13 9 14 // Array reprsenting one minute, hour, day, week, month, etc in seconds 10 - const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity] 15 + const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; 11 16 12 17 // Array equivalent to the above but in the string representation of the units 13 - const formatUnits: Intl.RelativeTimeFormatUnit[] = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'] 18 + const formatUnits: Intl.RelativeTimeFormatUnit[] = [ 19 + 'seconds', 20 + 'minutes', 21 + 'hours', 22 + 'days', 23 + 'weeks', 24 + 'months', 25 + 'years' 26 + ]; 14 27 15 28 // function to render relative time into 16 29 export function render(state: RenderState, now: number) { 17 - const { date, callback, formatter } = state 30 + const { date, callback, formatter } = state; 18 31 19 32 // Allow dates or times to be passed 20 - const timeMs = typeof date === 'number' ? date : date.getTime() 33 + const timeMs = typeof date === 'number' ? date : date.getTime(); 21 34 22 35 // Get the amount of seconds between the given date and now 23 - const delta = timeMs - now 24 - const seconds = Math.round(delta / 1000) 36 + const delta = timeMs - now; 37 + const seconds = Math.round(delta / 1000); 25 38 26 39 // Grab the ideal cutoff unit 27 - const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(seconds)) 40 + const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(seconds)); 28 41 29 42 // units 30 - const units = formatUnits[unitIndex] 43 + const units = formatUnits[unitIndex]; 31 44 32 45 // Get the divisor to divide from the seconds. E.g. if our unit is 'day' our divisor 33 46 // is one day in seconds, so we can divide our seconds by this to get the # of days 34 - const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1 47 + const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; 35 48 36 49 // count of units 37 - const count = Math.round(seconds / divisor) 50 + const count = Math.round(seconds / divisor); 38 51 39 52 // Intl.RelativeTimeFormat do its magic 40 - callback({ seconds: seconds, count, units, text: formatter.format(count, units) }) 53 + callback({ seconds: seconds, count, units, text: formatter.format(count, units) }); 41 54 42 55 // calculate time to next update, taking account rounding (e.g. it goes from 2 minutes to 1 minute at 90 seconds) 43 56 // and also the changeover from units (59 seconds shouldn't show as 1 minute, so at 61 seconds we schedule the next 44 57 // update for 1 second time) 45 58 46 - const divisorMs = divisor * 1000 59 + const divisorMs = divisor * 1000; 47 60 48 - let updateIn 61 + let updateIn; 49 62 50 63 if (unitIndex) { 51 - updateIn = divisorMs / 2 - (Math.abs(delta) % divisorMs) 64 + updateIn = divisorMs / 2 - (Math.abs(delta) % divisorMs); 52 65 if (updateIn < 0) { 53 - updateIn += divisorMs 66 + updateIn += divisorMs; 54 67 } 55 68 } else { 56 - updateIn = divisorMs - (Math.abs(delta) % divisorMs) 69 + updateIn = divisorMs - (Math.abs(delta) % divisorMs); 57 70 } 58 71 59 - const updateAt = now + updateIn 72 + const updateAt = now + updateIn; 60 73 61 - return updateAt 74 + return updateAt; 62 75 }
+24 -18
src/lib/Components/Items/BlueskyPost/relative-time/state.ts
··· 1 - import { getFormatter } from './formatter' 2 - import { render } from './render' 3 - import type { Callback, RenderState } from './render' 1 + import { getFormatter } from './formatter'; 2 + import { render } from './render'; 3 + import type { Callback, RenderState } from './render'; 4 4 5 5 interface UpdateState extends RenderState { 6 - update: number 6 + update: number; 7 7 } 8 8 9 9 // keep track of each instance 10 - const instances = new Map<Object, UpdateState>() 10 + const instances = new Map<Object, UpdateState>(); 11 11 12 12 // we use a single timer for efficiency and to keep updates in sync 13 - let updateInterval: number | NodeJS.Timeout 13 + let updateInterval: number | NodeJS.Timeout; 14 14 15 15 // register or update instance 16 - export function register(instance: Object, date: Date | number, locale: string, live: boolean, callback: Callback) { 16 + export function register( 17 + instance: Object, 18 + date: Date | number, 19 + locale: string, 20 + live: boolean, 21 + callback: Callback 22 + ) { 17 23 // get the formatter for the given locale, we do this here so we don't keep having to look it up on each tick 18 - const formatter = getFormatter(locale) 24 + const formatter = getFormatter(locale); 19 25 20 26 // create state to render 21 - const state = { date, callback, formatter } 27 + const state = { date, callback, formatter }; 22 28 23 29 // initial render is immediate, so works for SSR 24 - const update = render(state, Date.now()) 30 + const update = render(state, Date.now()); 25 31 26 32 // if it's to update live, we keep a track and schedule the next update 27 33 if (live) { 28 - instances.set(instance, { ...state, update }) 34 + instances.set(instance, { ...state, update }); 29 35 } else { 30 - instances.delete(instance) 36 + instances.delete(instance); 31 37 } 32 38 33 39 // start the clock ticking if there are any live instances ··· 35 41 updateInterval = 36 42 updateInterval || 37 43 setInterval(() => { 38 - const now = Date.now() 44 + const now = Date.now(); 39 45 for (const state of instances.values()) { 40 46 if (state.update <= now) { 41 - state.update = render(state, now) 47 + state.update = render(state, now); 42 48 } 43 49 } 44 - }, 1000) 50 + }, 1000); 45 51 } 46 52 } 47 53 48 54 export function unregister(instance: Object) { 49 - instances.delete(instance) 55 + instances.delete(instance); 50 56 if (instances.size === 0) { 51 - clearInterval(updateInterval) 52 - updateInterval = 0 57 + clearInterval(updateInterval); 58 + updateInterval = 0; 53 59 } 54 60 }
+1 -1
src/lib/Components/Items/BlueskyPost/types.ts
··· 5 5 AppBskyEmbedExternal, 6 6 AppBskyEmbedImages, 7 7 AppBskyEmbedRecordWithMedia, 8 - AppBskyEmbedVideo, 8 + AppBskyEmbedVideo 9 9 } from '@atproto/api'; 10 10 11 11 export interface Post extends AppBskyFeedDefs.PostView {
+1 -5
src/lib/Components/Items/ItemCard.svelte
··· 70 70 </div> 71 71 <div class="mt-2 flex justify-between"> 72 72 <h3 class="sm:text-md text-sm font-medium text-base-50"> 73 - <a 74 - href="/{item.media_type}/{item.id}-{nameToId( 75 - item.title ?? '' 76 - )}" 77 - > 73 + <a href="/{item.media_type}/{item.id}-{nameToId(item.title ?? '')}"> 78 74 <span aria-hidden="true" class="absolute inset-0"></span> 79 75 <div class="line-clamp-2 max-w-full"> 80 76 {item.title}
+8 -5
src/lib/Components/Items/ReviewCard.svelte
··· 8 8 9 9 import { cn, nameToId } from '$lib/utils'; 10 10 11 - let { data, showMovieDetails = true, bigText = false }: { data: MainRecord; showMovieDetails?: boolean; bigText?: boolean } = 12 - $props(); 11 + let { 12 + data, 13 + showMovieDetails = true, 14 + bigText = false 15 + }: { data: MainRecord; showMovieDetails?: boolean; bigText?: boolean } = $props(); 13 16 14 17 let isLiked = $state(false); 15 18 </script> ··· 54 57 {data.author.displayName || data.author.handle} 55 58 </div> 56 59 </div> 57 - <div class="shrink-0 pr-2 text-sm text-base-400 font-normal"> 60 + <div class="shrink-0 pr-2 text-sm font-normal text-base-400"> 58 61 <RelativeTime date={new Date(data.updatedAt)} locale="en-US" /> 59 62 </div> 60 63 </a> ··· 95 98 </div> 96 99 97 100 {#if data.record.note?.value} 98 - <div class={cn("mt-4 text-sm text-base-300", bigText ? 'text-xl text-base-100' : '')}> 101 + <div class={cn('mt-4 text-sm text-base-300', bigText ? 'text-xl text-base-100' : '')}> 99 102 {@html data.record.note?.value?.replace('\n', '<br /><br />')} 100 103 </div> 101 104 {/if} ··· 176 179 {(data.record.likes ?? 0) + (isLiked ? 1 : 0)} 177 180 </button> 178 181 179 - <OptionButton data={data} /> 182 + <OptionButton {data} /> 180 183 </div> 181 184 182 185 <!-- <a href={`/review/${encodeURIComponent(data.uri)}`}>
+8
src/lib/Components/Layout/Sidebar.svelte
··· 5 5 import { rateMovieModal, showSidebar } from '$lib/state/modals.svelte'; 6 6 import { fade } from 'svelte/transition'; 7 7 import { onNavigate } from '$app/navigation'; 8 + //import { get2PopularMovies } from '$lib/server/movies'; 8 9 9 10 const menu = [ 10 11 { ··· 41 42 showSidebar.value = false; 42 43 rateMovieModal.showEmpty(); 43 44 } 45 + }, 46 + { 47 + 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"> 48 + <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" /> 49 + </svg>`, 50 + label: 'Following', 51 + href: '/following' 44 52 }, 45 53 { 46 54 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">
+2 -2
src/lib/Components/Modals/OptionMenu.svelte
··· 38 38 39 39 {#if $open} 40 40 <div 41 - class="bg-base-900 border-base-800 shadow-xl shadow-base-950 divide-base-800 z-20 flex flex-col items-start divide-y overflow-hidden rounded-xl border" 41 + class="z-20 flex flex-col items-start divide-y divide-base-800 overflow-hidden rounded-xl border border-base-800 bg-base-900 shadow-xl shadow-base-950" 42 42 use:melt={$menu} 43 43 transition:fly={{ duration: 150, y: -10 }} 44 44 > ··· 192 192 193 193 <style> 194 194 .item { 195 - @apply text-base-300 hover:bg-accent-950/30 hover:text-accent-400 inline-flex w-full items-center gap-2 px-3 py-2 text-sm; 195 + @apply inline-flex w-full items-center gap-2 px-3 py-2 text-sm text-base-300 hover:bg-accent-950/30 hover:text-accent-400; 196 196 } 197 197 </style>
+9 -9
src/lib/Components/Modals/RateMovieModal.svelte
··· 87 87 {#if rateMovieModal.showModal} 88 88 <div class="relative z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true"> 89 89 <div 90 - class="bg-base-950/90 fixed inset-0 backdrop-blur-sm transition-opacity" 90 + class="fixed inset-0 bg-base-950/90 backdrop-blur-sm transition-opacity" 91 91 onclick={() => (rateMovieModal.showModal = false)} 92 92 aria-hidden="true" 93 93 ></div> 94 94 <div class="pointer-events-none fixed inset-0 z-50 h-[100dvh] w-screen overflow-y-auto"> 95 95 <div class="flex h-[100dvh] items-end justify-center p-4 text-center sm:items-center sm:p-0"> 96 96 <div 97 - class="border-base-800 bg-base-900 pointer-events-auto relative w-full transform overflow-hidden rounded-lg border px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:max-w-sm sm:p-6" 97 + class="pointer-events-auto relative w-full transform overflow-hidden rounded-lg border border-base-800 bg-base-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:max-w-sm sm:p-6" 98 98 > 99 99 <button 100 - class="bg-base-800/50 hover:bg-base-800/80 absolute right-2 top-2 rounded-full p-1" 100 + class="absolute right-2 top-2 rounded-full bg-base-800/50 p-1 hover:bg-base-800/80" 101 101 onclick={() => (rateMovieModal.showModal = false)} 102 102 > 103 103 <svg ··· 115 115 </button> 116 116 117 117 <div> 118 - <h3 class="text-md text-base-50 mb-4 font-semibold" id="modal-title"> 118 + <h3 class="text-md mb-4 font-semibold text-base-50" id="modal-title"> 119 119 {#if rateMovieModal.selectedItem?.editUri} 120 120 Edit review 121 121 {:else} ··· 126 126 {#if rateMovieModal.selectedItem?.title} 127 127 <div class="relative flex items-center gap-4"> 128 128 <div 129 - class="border-base-800 bg-base-900/50 relative z-20 aspect-[2/3] h-32 w-auto shrink-0 overflow-hidden rounded-md border" 129 + class="relative z-20 aspect-[2/3] h-32 w-auto shrink-0 overflow-hidden rounded-md border border-base-800 bg-base-900/50" 130 130 > 131 131 {#if rateMovieModal.selectedItem?.poster_path} 132 132 <img ··· 137 137 {/if} 138 138 </div> 139 139 <h3 140 - class="text-base-50 mb-4 flex flex-col gap-2 text-xl font-semibold" 140 + class="mb-4 flex flex-col gap-2 text-xl font-semibold text-base-50" 141 141 id="modal-title" 142 142 > 143 143 {rateMovieModal.selectedItem.title} ··· 158 158 {/if} 159 159 160 160 <div class="mt-4"> 161 - <label for="comment" class="text-base-50 block text-xs font-medium">review</label> 161 + <label for="comment" class="block text-xs font-medium text-base-50">review</label> 162 162 <div class="mt-2"> 163 163 <textarea 164 164 rows="4" ··· 166 166 id="comment" 167 167 bind:value={review} 168 168 placeholder="write a review" 169 - class="outline-nonse border-base-800 bg-base-950 text-base-50 placeholder:text-base-400 focus:outline-accent-400 block w-full rounded-lg border px-3 py-1.5 text-base -outline-offset-1 focus:outline focus:outline-2 focus:-outline-offset-2 sm:text-sm/6" 169 + class="outline-nonse block w-full rounded-lg border border-base-800 bg-base-950 px-3 py-1.5 text-base text-base-50 -outline-offset-1 placeholder:text-base-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-accent-400 sm:text-sm/6" 170 170 ></textarea> 171 171 </div> 172 172 </div> ··· 182 182 }} 183 183 type="button" 184 184 disabled={sending || !rateMovieModal.selectedItem?.title} 185 - class="border-accent-900 bg-accent-950/80 text-accent-300 hover:bg-accent-950 focus-visible:outline-accent-600 inline-flex w-full justify-center rounded-md border px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50" 185 + class="inline-flex w-full justify-center rounded-md border border-accent-900 bg-accent-950/80 px-3 py-2 text-sm font-semibold text-accent-300 shadow-sm hover:bg-accent-950 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-600 disabled:cursor-not-allowed disabled:opacity-50" 186 186 >{sending 187 187 ? 'Sending...' 188 188 : rateMovieModal.selectedItem?.editUri
+1 -1
src/lib/Components/UI/SearchCombobox.svelte
··· 111 111 rateMovieModal.selectedItem = { 112 112 ...item, 113 113 currentRating: watchedItems.getRating(item)?.rating ?? undefined, 114 - currentReview: watchedItems.getRating(item)?.ratingText ?? undefined, 114 + currentReview: watchedItems.getRating(item)?.ratingText ?? undefined 115 115 }; 116 116 rateMovieModal.showModal = true; 117 117 }}
-8
src/lib/Components/User/Profile.svelte
··· 44 44 @{profile.handle} 45 45 </div> 46 46 </div> 47 - <div class="mt-6 hidden flex-row justify-stretch space-x-4 space-y-0"> 48 - <button 49 - type="button" 50 - class="inline-flex justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-base-900 shadow-sm ring-1 ring-inset ring-base-300 hover:bg-base-50" 51 - > 52 - <span>Call</span> 53 - </button> 54 - </div> 55 47 </div> 56 48 </div> 57 49 </div>
+8
src/lib/bluesky.ts
··· 26 26 const { data } = await agent.app.bsky.actor.getProfile({ actor: did }); 27 27 return data; 28 28 } 29 + export async function getFollows({ did, agent = undefined }: { did: string; agent?: AgentType }) { 30 + if (!agent) { 31 + agent = new AtpBaseClient({ service: 'https://api.bsky.app' }); 32 + } 33 + 34 + const { data } = await agent.app.bsky.graph.getFollows({ actor: did }); 35 + return data; 36 + }
+5
src/lib/db.ts
··· 81 81 const data = await response.json(); 82 82 return data; 83 83 } 84 + export async function getAuthorDids(): Promise<string[]> { 85 + const response = await fetch(`${env.BACKEND_URL}/api/author-dids`); 86 + const data = await response.json(); 87 + return data; 88 + }
+1 -1
src/lib/server/client.ts
··· 3 3 import { SessionStore, StateStore } from './storage'; 4 4 import { dev } from '$app/environment'; 5 5 6 - const publicUrl = 'https://skywatched.app'; 6 + const publicUrl = 'https://skywatchedgoogle2-791157493831.us-central1.run.app'; 7 7 const port = 5173; 8 8 const url = dev ? `http://[::1]:${port}` : publicUrl; 9 9
+16
src/lib/server/movies.ts
··· 163 163 164 164 return data; 165 165 } 166 + export async function getPopularMovies() { 167 + const i = Math.floor(Math.random() * 6); 168 + const url = `https://api.themoviedb.org/3/movie/popular?language=en-US&page=` + i; 169 + const options = { 170 + method: 'GET', 171 + headers: { 172 + accept: 'application/json', 173 + Authorization: `Bearer ${env.TMDB_API_KEY}` 174 + } 175 + }; 176 + 177 + const response = await fetch(url, options); 178 + const data = await response.json(); 179 + 180 + return data; 181 + }
+2 -2
src/routes/+page.svelte
··· 46 46 showLoginModal.toggle(); 47 47 }} 48 48 type="button" 49 - class="mt-8 inline-flex w-fit items-center gap-x-1.5 rounded-md border border-accent-500/30 bg-accent-700/20 px-3 py-2 text-sm font-semibold text-accent-400 shadow-sm transition-all duration-100 hover:bg-accent-700/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-600" 49 + class="mt-8 inline-flex w-fit items-center gap-x-1.5 rounded-xl border border-accent-500/30 bg-accent-700/20 px-3 py-2 text-sm font-semibold text-accent-400 shadow-sm transition-all duration-100 hover:bg-accent-700/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-600" 50 50 > 51 51 Sign in with Bluesky 52 52 </button> ··· 56 56 rateMovieModal.showEmpty(); 57 57 }} 58 58 type="button" 59 - class="mt-8 inline-flex w-fit items-center gap-x-1.5 rounded-md border border-accent-500/30 bg-accent-700/20 px-3 py-2 text-sm font-semibold text-accent-400 shadow-sm transition-all duration-100 hover:bg-accent-700/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-600" 59 + class="mt-8 inline-flex w-fit items-center gap-x-1.5 rounded-xl border border-accent-500/30 bg-accent-700/20 px-3 py-2 text-sm font-semibold text-accent-400 shadow-sm transition-all duration-100 hover:bg-accent-700/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-600" 60 60 > 61 61 New review 62 62 </button>
+6 -21
src/routes/[kind=item]/[id]/+page.svelte
··· 17 17 <svelte:head> 18 18 <title>{data.item.title} | skywatched</title> 19 19 20 - <meta 21 - name="description" 22 - content={`Rate and review "${data.item.title}" on skywatched`} 23 - /> 20 + <meta name="description" content={`Rate and review "${data.item.title}" on skywatched`} /> 24 21 25 22 <meta property="og:url" content={$page.url.href} /> 26 23 <meta property="og:type" content="website" /> 27 - <meta 28 - property="og:title" 29 - content="{data.item.title} | skywatched.app" 30 - /> 31 - <meta 32 - property="og:description" 33 - content={`Rate and review "${data.item.title}" on skywatched`} 34 - /> 24 + <meta property="og:title" content="{data.item.title} | skywatched.app" /> 25 + <meta property="og:description" content={`Rate and review "${data.item.title}" on skywatched`} /> 35 26 <meta property="og:image" content="{$page.url.href}/og.png" /> 36 27 37 28 <meta name="twitter:card" content="summary_large_image" /> 38 29 <meta property="twitter:domain" content="skywatched.app" /> 39 30 <meta property="twitter:url" content={$page.url.href} /> 40 - <meta 41 - name="twitter:title" 42 - content="{data.item.title} | skywatched.app" 43 - /> 44 - <meta 45 - name="twitter:description" 46 - content={`Rate and review "${data.item.title}" on skywatched`} 47 - /> 31 + <meta name="twitter:title" content="{data.item.title} | skywatched.app" /> 32 + <meta name="twitter:description" content={`Rate and review "${data.item.title}" on skywatched`} /> 48 33 <meta name="twitter:image" content="{$page.url.href}/og.png" /> 49 34 </svelte:head> 50 35 ··· 59 44 )} 60 45 onclick={() => { 61 46 rateMovieModal.show({ 62 - ...data.item, 47 + ...data.item 63 48 }); 64 49 }} 65 50 >
+39
src/routes/api/follow/+server.ts
··· 1 + import { error, json, type RequestHandler } from '@sveltejs/kit'; 2 + import { AtpBaseClient } from '@atproto/api'; 3 + import { TID } from '@atproto/common'; 4 + 5 + export const POST: RequestHandler = async ({ request, locals }) => { 6 + const user = locals.user; 7 + const agent = locals.agent; 8 + if (!user || !agent || agent instanceof AtpBaseClient) { 9 + return error(401, 'Unauthorized API call'); 10 + } 11 + const body = await request.json(); 12 + 13 + const did = body.did; 14 + 15 + const did2 = user.did; 16 + 17 + const rkey = TID.nextStr(); 18 + 19 + const record: { 20 + repo: string; 21 + collection: string; 22 + rkey: string; 23 + record: { 24 + subject: string; 25 + createdAt: string; 26 + }; 27 + } = { 28 + repo: did2, 29 + collection: 'app.bsky.graph.follow', 30 + rkey, 31 + record: { 32 + subject: did, 33 + createdAt: new Date().toISOString() 34 + } 35 + }; 36 + await agent.com.atproto.repo.createRecord(record); 37 + 38 + return json({ status: 'liked' }); 39 + };
+14
src/routes/following/+page.server.ts
··· 1 + import { getFollows } from '$lib/bluesky.js'; 2 + import { getRecentRecords } from '$lib/db'; 3 + 4 + /** @type {import('./$types').PageServerLoad} */ 5 + export async function load(event) { 6 + const feed = await getRecentRecords(); 7 + const did = event.locals.user.did; 8 + const follows = await getFollows({ did }); 9 + const filt = feed.filter((review) => 10 + follows.follows.some((person) => person.did == review.author.did) 11 + ); 12 + 13 + return { truefeed: filt }; 14 + }
+26
src/routes/following/+page.svelte
··· 1 + <script lang="ts"> 2 + import Container from '$lib/Components/Layout/Container.svelte'; 3 + import { type PageData } from './$types'; 4 + import ReviewList from '$lib/Components/Items/ReviewList.svelte'; 5 + import BaseHeadTags from '$lib/Components/Layout/BaseHeadTags.svelte'; 6 + 7 + let { data }: { data: PageData } = $props(); 8 + </script> 9 + 10 + <svelte:head> 11 + <title>feed | skywatched</title> 12 + </svelte:head> 13 + 14 + <BaseHeadTags /> 15 + 16 + <Container class="relative z-10 w-full py-8 sm:py-16"> 17 + <div class="mx-auto w-full max-w-2xl"> 18 + <div class="flex flex-col gap-4 px-4"> 19 + <h1 class="pb-4 text-2xl font-bold">Recent reviews</h1> 20 + </div> 21 + 22 + {#if data.truefeed.length > 0} 23 + <ReviewList reviews={data.truefeed} /> 24 + {/if} 25 + </div> 26 + </Container>
+174
src/routes/game/[kind]/[id]/[kind2]/[id2]/[counter]/+page.server.ts
··· 1 + import { getRecentRecordsForItem } from '$lib/db.js'; 2 + import { 3 + getCast, 4 + getDetails, 5 + getRecommendations, 6 + getTrailer, 7 + getWatchProviders, 8 + getPersonDetails, 9 + getCombinedCredits 10 + } from '$lib/server/movies'; 11 + import { error, redirect } from '@sveltejs/kit'; 12 + 13 + /** @type {import('./$types').PageServerLoad} */ 14 + export async function load(event) { 15 + const id = parseInt(event.params.id.split('-')[0]); 16 + const id2 = parseInt(event.params.id2.split('-')[0]); 17 + const kind = event.params.kind; 18 + const kind2 = event.params.kind2; 19 + const ids = id; 20 + const ids2 = id2; 21 + const couter = event.params.counter; 22 + if (kind == kind2 && id == id2) { 23 + redirect(302, '/win/' + couter); 24 + } 25 + 26 + if (kind !== 'movie' && kind !== 'tv' && kind !== 'cast') { 27 + return error(404, 'Not found'); 28 + } 29 + if (kind2 !== 'movie' && kind2 !== 'tv' && kind2 !== 'cast') { 30 + return error(404, 'Not found'); 31 + } 32 + 33 + if (!id) { 34 + return error(404, 'Not found'); 35 + } 36 + if (!id2) { 37 + return error(404, 'Not found'); 38 + } 39 + var temp; 40 + var temp2; 41 + var casting; 42 + var casting2; 43 + var person; 44 + var person2; 45 + var credits; 46 + var credits2; 47 + if (kind === 'cast') { 48 + temp = getDetails(68730, 'movie'); 49 + casting = getCast(68730, 'movie'); 50 + person = getPersonDetails(id); 51 + person2 = getPersonDetails(id2); 52 + credits = id; 53 + credits2 = id2; 54 + } else { 55 + temp = getDetails(id, kind); 56 + casting = getCast(id, kind); 57 + person = getPersonDetails(37625); 58 + person2 = getPersonDetails(37625); 59 + credits = 37625; 60 + credits2 = 37625; 61 + } 62 + if (kind2 === 'cast') { 63 + temp2 = getDetails(68730, 'movie'); 64 + casting2 = getCast(68730, 'movie'); 65 + person2 = getPersonDetails(id2); 66 + credits2 = id2; 67 + } else { 68 + temp2 = getDetails(id2, kind2); 69 + casting2 = getCast(id2, kind2); 70 + person2 = getPersonDetails(37625); 71 + credits2 = 37625; 72 + } 73 + const resultPromise = temp; 74 + const personPromise = person; 75 + const person2Promise = person2; 76 + //if(kind==="cast"){ 77 + // const resultPromise = getDetails(68730,"movie") 78 + //} 79 + //else{ 80 + // const resultPromise = getDetails(id, kind); 81 + //} 82 + const resultPromise2 = temp2; 83 + 84 + const castPromise = casting; 85 + const castPromise2 = casting2; 86 + const creditsPromise = credits; 87 + const creditsPromise2 = credits2; 88 + const combinedCredits = await getCombinedCredits(credits); 89 + const creditsSet = new Set<string>(); 90 + const combinedCredits2 = await getCombinedCredits(credits2); 91 + const creditsSet2 = new Set<string>(); 92 + combinedCredits.cast = combinedCredits.cast 93 + .filter((item: { id: string; poster_path: string }) => { 94 + if (creditsSet.has(item.id)) { 95 + return false; 96 + } 97 + creditsSet.add(item.id); 98 + return item.poster_path; 99 + }) 100 + .sort((a: { order: number }, b: { order: number }) => b.order - a.order) 101 + .map((item: { id: string; media_type: string }) => { 102 + return { 103 + ...item, 104 + movieId: item.media_type === 'movie' ? item.id : undefined, 105 + showId: item.media_type === 'tv' ? item.id : undefined 106 + }; 107 + }); 108 + combinedCredits2.cast = combinedCredits2.cast 109 + .filter((item: { id: string; poster_path: string }) => { 110 + if (creditsSet2.has(item.id)) { 111 + return false; 112 + } 113 + creditsSet2.add(item.id); 114 + return item.poster_path; 115 + }) 116 + .sort((a: { order: number }, b: { order: number }) => b.order - a.order) 117 + .map((item: { id: string; media_type: string }) => { 118 + return { 119 + ...item, 120 + movieId: item.media_type === 'movie' ? item.id : undefined, 121 + showId: item.media_type === 'tv' ? item.id : undefined 122 + }; 123 + }); 124 + 125 + const [ 126 + result, 127 + result2, 128 + cast, 129 + cast2, 130 + personDetails, 131 + person2Details, 132 + creditDetails, 133 + creditDetails2 134 + ] = await Promise.all([ 135 + resultPromise, 136 + resultPromise2, 137 + castPromise, 138 + castPromise2, 139 + personPromise, 140 + person2Promise, 141 + creditsPromise, 142 + creditsPromise2 143 + ]); 144 + 145 + if (!result || result.success === false) { 146 + return error(404, 'Not found'); 147 + } 148 + 149 + return { 150 + result: { 151 + ...result, 152 + movieId: kind === 'movie' ? id : undefined, 153 + showId: kind === 'tv' ? id : undefined 154 + }, 155 + result2: { 156 + ...result2, 157 + movieId: kind2 === 'movie' ? id2 : undefined, 158 + showId: kind2 === 'tv' ? id2 : undefined 159 + }, 160 + kind, 161 + kind2, 162 + cast, 163 + cast2, 164 + couter, 165 + personDetails, 166 + person2Details, 167 + creditDetails, 168 + creditDetails2, 169 + ids, 170 + ids2, 171 + combinedCredits: combinedCredits.cast, 172 + combinedCredits2: combinedCredits2.cast 173 + }; 174 + }
+206
src/routes/game/[kind]/[id]/[kind2]/[id2]/[counter]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { type PageData } from './$types'; 3 + import { cn, nameToId } from '$lib/utils'; 4 + 5 + import Container from '$lib/Components/Layout/Container.svelte'; 6 + import Avatar from '$lib/Components/User/Avatar.svelte'; 7 + import { page } from '$app/stores'; 8 + import { watchedItems } from '$lib/state/user.svelte'; 9 + import { rateMovieModal } from '$lib/state/modals.svelte'; 10 + 11 + let { data }: { data: PageData } = $props(); 12 + </script> 13 + 14 + <svelte:head> 15 + <title>{data.result.title ?? data.result.name ?? ''} | skywatched</title> 16 + 17 + <meta 18 + name="description" 19 + content={`Rate and review "${data.result.title ?? data.result.name ?? ''}" on skywatched`} 20 + /> 21 + 22 + <meta property="og:url" content={$page.url.href} /> 23 + <meta property="og:type" content="website" /> 24 + <meta 25 + property="og:title" 26 + content="{data.result.title ?? data.result.name ?? ''} | skywatched.app" 27 + /> 28 + <meta 29 + property="og:description" 30 + content={`Rate and review "${data.result.title ?? data.result.name ?? ''}" on skywatched`} 31 + /> 32 + <meta property="og:image" content="{$page.url.href}/og.png" /> 33 + 34 + <meta name="twitter:card" content="summary_large_image" /> 35 + <meta property="twitter:domain" content="skywatched.app" /> 36 + <meta property="twitter:url" content={$page.url.href} /> 37 + <meta 38 + name="twitter:title" 39 + content="{data.result.title ?? data.result.name ?? ''} | skywatched.app" 40 + /> 41 + <meta 42 + name="twitter:description" 43 + content={`Rate and review "${data.result.title ?? data.result.name ?? ''}" on skywatched`} 44 + /> 45 + <meta name="twitter:image" content="{$page.url.href}/og.png" /> 46 + </svelte:head> 47 + 48 + {#snippet buttons()} 49 + {#if data.user && !watchedItems.hasRated(data.result)} 50 + <button 51 + class={cn( 52 + '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 ', 53 + watchedItems.hasRated(data.result) 54 + ? 'bg-green-500/10 text-green-400 hover:bg-green-500/20' 55 + : '' 56 + )} 57 + onclick={() => { 58 + rateMovieModal.show({ 59 + movieId: data.result.movieId, 60 + showId: data.result.showId, 61 + kind: data.result.movieId ? 'movie' : 'tv', 62 + name: data.result.original_title ?? data.result.original_name, 63 + posterPath: data.result.poster_path, 64 + currentRating: watchedItems.getRating(data.result)?.rating ?? 0, 65 + currentReview: watchedItems.getRating(data.result)?.ratingText ?? '' 66 + }); 67 + }} 68 + > 69 + rate {data.kind === 'movie' ? 'movie' : 'show'} 70 + </button> 71 + {/if} 72 + {/snippet} 73 + 74 + <div class="fixed inset-0 h-full w-full bg-black/50"></div> 75 + 76 + <Container class="relative z-10 pb-8 pt-4"> 77 + <div class="flex justify-center gap-4 px-4 pt-8"> 78 + <div class="flex-column"> 79 + {#if data.kind === 'cast'} 80 + <div class="text-center text-sm font-medium">Cast1</div> 81 + <Avatar 82 + src={data.personDetails.profile_path 83 + ? 'https://image.tmdb.org/t/p/w500' + data.personDetails.profile_path 84 + : undefined} 85 + size="size-44" 86 + /> 87 + {:else} 88 + <div class="text-center text-sm font-medium">Movie1</div> 89 + <img 90 + src="https://image.tmdb.org/t/p/w500{data.result.poster_path}" 91 + alt="" 92 + class="poster h-36 w-24 shrink-0 rounded-lg border border-white/10 sm:h-64 sm:w-44" 93 + style:--name={`poster-${data.result.id}`} 94 + /> 95 + {/if} 96 + </div> 97 + <div class="flex-column"> 98 + {#if data.kind2 === 'cast'} 99 + <div class="text-center text-sm font-medium">Cast2</div> 100 + <Avatar 101 + src={data.person2Details.profile_path 102 + ? 'https://image.tmdb.org/t/p/w500' + data.person2Details.profile_path 103 + : undefined} 104 + size="size-44" 105 + /> 106 + {:else} 107 + <div class="text-center text-sm font-medium">Movie2</div> 108 + <img 109 + src="https://image.tmdb.org/t/p/w500{data.result2.poster_path}" 110 + alt="" 111 + class="poster h-36 w-24 shrink-0 rounded-lg border border-white/10 sm:h-64 sm:w-44" 112 + style:--name={`poster-${data.result2.id}`} 113 + /> 114 + {/if} 115 + </div> 116 + </div> 117 + 118 + <div class="px-4 pt-4 text-sm text-white"> 119 + <div class="mb-4 flex gap-2 sm:hidden"> 120 + {@render buttons()} 121 + </div> 122 + </div> 123 + 124 + {#if data.cast.length > 0} 125 + <div class="flex flex-col gap-x-6 px-4 pb-8 pt-4 text-sm text-white"> 126 + {#if data.kind === 'cast'} 127 + <div class="text-center text-sm font-medium">Movies of {data.personDetails.name}</div> 128 + <div class={cn('flex justify-start gap-x-6 overflow-x-auto')}> 129 + {#each data.combinedCredits as castMember} 130 + <a 131 + href={`/game/movie/${castMember.id}-${castMember.title ?? castMember.name}/${data.kind2}/${data.ids2}-/${parseInt(data.couter) + 1}`} 132 + class="flex flex-col items-center gap-1" 133 + > 134 + <img 135 + src="https://image.tmdb.org/t/p/w342{castMember.poster_path}" 136 + alt="movie poster for {castMember.title ?? castMember.name}" 137 + class="poster size-32 w-32 object-cover object-center lg:size-full" 138 + /> 139 + <div class="size-32 text-center text-sm font-medium"> 140 + {castMember.title ?? castMember.name} 141 + </div> 142 + </a> 143 + {/each} 144 + </div> 145 + {:else} 146 + <div class="text-center text-sm font-medium">cast of {data.result.title}</div> 147 + <div class={'justify-left flex gap-x-6 overflow-x-auto'}> 148 + {#each data.cast as castMember} 149 + <a 150 + href={`/game/cast/${castMember.id}-${nameToId(castMember.name)}/${data.kind2}/${data.ids2}-/${parseInt(data.couter) + 1}`} 151 + class="flex flex-col items-center gap-1" 152 + > 153 + <Avatar 154 + src={castMember.profile_path 155 + ? 'https://image.tmdb.org/t/p/w500' + castMember.profile_path 156 + : undefined} 157 + size="size-32" 158 + /> 159 + <div class="text-center text-xs font-medium">{castMember.name}</div> 160 + <div class="text-center text-xs text-base-400">{castMember.character}</div> 161 + </a> 162 + {/each} 163 + </div> 164 + {/if} 165 + {#if data.kind2 === 'cast'} 166 + <div class="text-center text-sm font-medium">Movies of {data.person2Details.name}</div> 167 + <div class={cn('flex gap-x-6 overflow-x-auto')}> 168 + {#each data.combinedCredits2 as castMember} 169 + <a 170 + href={`/game/${data.kind}/${data.ids}-/movie/${castMember.id}-${castMember.title ?? castMember.name}/${parseInt(data.couter) + 1}`} 171 + class="flex flex-col items-center gap-1" 172 + > 173 + <img 174 + src="https://image.tmdb.org/t/p/w342{castMember.poster_path}" 175 + alt="movie poster for {castMember.title ?? castMember.name}" 176 + class="poster w-500 object-cover object-center lg:size-full" 177 + /> 178 + <div class="size-32 text-center text-sm font-medium"> 179 + {castMember.title ?? castMember.name} 180 + </div> 181 + </a> 182 + {/each} 183 + </div> 184 + {:else} 185 + <div class="text-center text-sm font-medium">Cast of {data.result2.title}</div> 186 + <div class={'justify-right flex gap-x-6 overflow-x-auto'}> 187 + {#each data.cast2 as castMember} 188 + <a 189 + href={`/game/${data.kind}/${data.ids}-/cast/${castMember.id}-${castMember.title ?? castMember.name}/${parseInt(data.couter) + 1}`} 190 + class="flex flex-col items-center gap-1" 191 + > 192 + <Avatar 193 + src={castMember.profile_path 194 + ? 'https://image.tmdb.org/t/p/w500' + castMember.profile_path 195 + : undefined} 196 + size="size-32" 197 + /> 198 + <div class="text-center text-xs font-medium">{castMember.name}</div> 199 + <div class="text-center text-xs text-base-400">{castMember.character}</div> 200 + </a> 201 + {/each} 202 + </div> 203 + {/if} 204 + </div> 205 + {/if} 206 + </Container>
+47
src/routes/gamesearch/+page.server.ts
··· 1 + import { searchMulti, getPopularMovies } from '$lib/server/movies'; 2 + import { redirect } from '@sveltejs/kit'; 3 + import type { Actions } from './$types'; 4 + 5 + /** @type {import('./$types').PageServerLoad} */ 6 + export async function load(event) { 7 + const query = event.url.searchParams.get('query'); 8 + const query2 = event.url.searchParams.get('query2'); 9 + 10 + if (query && query2) { 11 + const results = (await searchMulti(query)) 12 + .map((result) => { 13 + if (result.media_type === 'movie') { 14 + return { ...result, movieId: result.id }; 15 + } else if (result.media_type === 'tv') { 16 + return { ...result, showId: result.id }; 17 + } 18 + return null; 19 + }) 20 + .filter((result) => result?.poster_path !== null); 21 + const results2 = (await searchMulti(query2)) 22 + .map((result2) => { 23 + if (result2.media_type === 'movie') { 24 + return { ...result2, movieId: result2.id }; 25 + } else if (result2.media_type === 'tv') { 26 + return { ...result2, showId: result2.id }; 27 + } 28 + return null; 29 + }) 30 + .filter((result2) => result2?.poster_path !== null); 31 + return { results, results2, query, query2 }; 32 + } 33 + 34 + return { results: [], results2: [], query, query2 }; 35 + } 36 + export const actions: Actions = { 37 + randomize: async () => { 38 + const randmovies = await getPopularMovies(); 39 + const i = Math.floor(Math.random() * randmovies.results.length); 40 + const j = Math.floor(Math.random() * randmovies.results.length); 41 + 42 + return redirect( 43 + 302, 44 + '/game/movie/' + randmovies.results[i]?.id + '-/movie/' + randmovies.results[j]?.id + '-/0' 45 + ); 46 + } 47 + };
+93
src/routes/gamesearch/+page.svelte
··· 1 + <script lang="ts"> 2 + import ItemsGrid from '$lib/Components/Items/ItemsGrid.svelte'; 3 + import Container from '$lib/Components/Layout/Container.svelte'; 4 + import BaseHeadTags from '$lib/Components/Layout/BaseHeadTags.svelte'; 5 + 6 + import { type PageData } from './$types'; 7 + let { data }: { data: PageData } = $props(); 8 + </script> 9 + 10 + <svelte:head> 11 + <title>search | skywatched</title> 12 + </svelte:head> 13 + 14 + <BaseHeadTags /> 15 + 16 + <Container> 17 + <div class="flex-column px-4 py-16"> 18 + <h1 class="pb-8 text-4xl font-bold tracking-tight text-base-50">search for a movie or show</h1> 19 + <div class="mt-10 w-full sm:mx-auto sm:max-w-sm"> 20 + <form class="space-y-6" action="?/randomize" method="POST"> 21 + <div> 22 + <button 23 + type="submit" 24 + class="flex w-full justify-center rounded-xl bg-accent-500 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-accent-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-500" 25 + >randomize</button 26 + > 27 + </div> 28 + </form> 29 + </div> 30 + 31 + <form class="relative mt-6 flex items-center" method="GET"> 32 + <input 33 + value={data.query} 34 + type="text" 35 + name="query" 36 + id="query" 37 + class="block w-full rounded-md border-0 bg-base-900 py-1.5 pr-14 text-base-50 shadow-sm ring-1 ring-inset ring-base-700 placeholder:text-base-400 focus:ring-2 focus:ring-inset focus:ring-accent-600 sm:text-sm/6" 38 + /> 39 + <input 40 + value={data.query2} 41 + type="text" 42 + name="query2" 43 + id="query2" 44 + class="block w-full rounded-md border-0 bg-base-900 py-1.5 pr-14 text-base-50 shadow-sm ring-1 ring-inset ring-base-700 placeholder:text-base-400 focus:ring-2 focus:ring-inset focus:ring-accent-600 sm:text-sm/6" 45 + /> 46 + <button 47 + onsubmit={(event) => { 48 + if (data.query.length === 0) { 49 + event.preventDefault(); 50 + return; 51 + } 52 + }} 53 + type="submit" 54 + class="absolute inset-y-0 right-0 flex py-2 pr-2 text-accent-500 hover:text-accent-400" 55 + > 56 + <svg 57 + xmlns="http://www.w3.org/2000/svg" 58 + fill="none" 59 + viewBox="0 0 24 24" 60 + stroke-width="1.5" 61 + stroke="currentColor" 62 + class="size-5" 63 + aria-hidden="true" 64 + > 65 + <path 66 + stroke-linecap="round" 67 + stroke-linejoin="round" 68 + 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" 69 + /> 70 + </svg> 71 + 72 + <span class="sr-only">search</span> 73 + </button> 74 + </form> 75 + 76 + {#if data.results.length > 0} 77 + <div class="flex-row"> 78 + <div class="flex-col"> 79 + <h2 class="mt-8 text-2xl font-bold tracking-tight text-base-50">results</h2> 80 + 81 + <ItemsGrid items={data.results} showMark={!!data.user} /> 82 + </div> 83 + <div class="flex-col"> 84 + <h2 class="mt-8 text-2xl font-bold tracking-tight text-base-50">results</h2> 85 + 86 + <ItemsGrid items={data.results2} showMark={!!data.user} /> 87 + </div> 88 + </div> 89 + {:else if data.query} 90 + <p class="text-md mt-8 text-base-50">no results found</p> 91 + {/if} 92 + </div> 93 + </Container>
+3 -3
src/routes/review/[uri]/+page.svelte
··· 58 58 <ReviewCard data={data.record} bigText={true} /> 59 59 60 60 {#if data.record.record.crosspost?.uri} 61 - <div class="relative w-full max-w-2xl backdrop-blur-sm mb-8"> 61 + <div class="relative mb-8 w-full max-w-2xl backdrop-blur-sm"> 62 62 <div class="flex max-w-full items-center gap-4 overflow-hidden px-4"> 63 63 <Comments uri={data.record.record.crosspost.uri} user={''} comments={[]} url={''} /> 64 64 </div> ··· 66 66 {:else} 67 67 <div class="flex flex-wrap items-center justify-center gap-4"> 68 68 <div 69 - class="border-base-700 bg-base-50/5 text-base-200 hover:bg-base-50/10 z-10 rounded-lg border px-3 py-2 text-xs font-medium transition-all duration-100" 69 + class="z-10 rounded-lg border border-base-700 bg-base-50/5 px-3 py-2 text-xs font-medium text-base-200 transition-all duration-100 hover:bg-base-50/10" 70 70 > 71 71 <a 72 72 href={`/${data.record.record.item.ref === 'tmdb:m' ? 'movie' : 'tv'}/${data.record.record.item.value}`} ··· 75 75 </a> 76 76 </div> 77 77 <div 78 - class="border-base-700 bg-base-50/5 text-base-200 hover:bg-base-50/10 z-10 rounded-lg border px-3 py-2 text-xs font-medium transition-all duration-100" 78 + class="z-10 rounded-lg border border-base-700 bg-base-50/5 px-3 py-2 text-xs font-medium text-base-200 transition-all duration-100 hover:bg-base-50/10" 79 79 > 80 80 <a href={`/user/${data.record.author.handle}`}> 81 81 all reviews by {data.record.author.displayName || data.record.author.handle}
+30 -5
src/routes/user/[handle]/+page.server.ts
··· 1 - import { getProfile, resolveHandle } from '$lib/bluesky.js'; 2 - import { getRecentRecordOfUser } from '$lib/db'; 1 + import { getProfile, resolveHandle, getFollows } from '$lib/bluesky.js'; 2 + import { getRecentRecordOfUser, getAuthorDids } from '$lib/db'; 3 3 4 4 /** @type {import('./$types').PageServerLoad} */ 5 5 export async function load(event) { ··· 8 8 const did = await resolveHandle({ handle: handle }); 9 9 10 10 const profilePromise = getProfile({ did }); 11 - const itemsPromise = getRecentRecordOfUser({ did }); 11 + const reviewsPromise = getRecentRecordOfUser({ did }); 12 + 13 + const authorsPromise = getAuthorDids(); 12 14 13 - const [profile, items] = await Promise.all([profilePromise, itemsPromise]); 15 + const [profile, reviews, authors] = await Promise.all([ 16 + profilePromise, 17 + reviewsPromise, 18 + authorsPromise 19 + ]); 20 + 21 + if (event.locals.user) { 22 + const follows = await getFollows({ did: event.locals.user.did }); 23 + 24 + return { 25 + isUser: event.locals.user?.did === did, 26 + username: event.params.handle, 27 + profile, 28 + items: reviews, 29 + follows: follows.follows.filter((profile) => authors.includes(profile.did)), 30 + following: follows.follows.some((profile) => profile.did == did), 31 + did 32 + }; 33 + } 14 34 15 - return { isUser: event.locals.user?.did === did, username: event.params.handle, profile, items }; 35 + return { 36 + isUser: false, 37 + profile, 38 + items: reviews, 39 + did 40 + }; 16 41 }
+81 -2
src/routes/user/[handle]/+page.svelte
··· 1 1 <script lang="ts"> 2 + import Avatar from '$lib/Components/User/Avatar.svelte'; 2 3 import BaseHeadTags from '$lib/Components/Layout/BaseHeadTags.svelte'; 3 4 import Container from '$lib/Components/Layout/Container.svelte'; 4 5 import Profile from '$lib/Components/User/Profile.svelte'; 5 6 import ReviewList from '$lib/Components/Items/ReviewList.svelte'; 7 + import { toast } from 'svelte-sonner'; 8 + import { cn } from '$lib/utils.js'; 6 9 7 10 let { data } = $props(); 8 11 </script> ··· 14 17 <BaseHeadTags /> 15 18 16 19 <Container class="max-w-full lg:max-w-full"> 17 - <Profile profile={data.profile} /> 20 + <div class={'group flex-row'}> 21 + <Profile profile={data.profile} /> 22 + </div> 23 + {#if data.isUser} 24 + <div class="mx-auto mt-2 flex max-w-2xl px-4 sm:-mt-24 sm:justify-end sm:px-0"> 25 + <form class="space-y-6" action="/?/logout" method="POST"> 26 + <button 27 + type="submit" 28 + class={cn( 29 + 'inline-flex justify-center rounded-xl bg-white/10 px-3 py-2 text-sm font-semibold text-base-50 shadow-sm ring-1 ring-inset ring-base-300/20 hover:bg-base-50/20', 30 + data.profile.banner ? 'mt-6' : 'mt-3' 31 + )} 32 + > 33 + <span>Logout</span> 34 + </button> 35 + </form> 36 + </div> 37 + {:else if data.user} 38 + <div class="mx-auto mt-2 flex max-w-2xl px-4 sm:-mt-24 sm:justify-end sm:px-0"> 39 + <button 40 + type="button" 41 + onclick={async () => { 42 + if (data.following) return; 43 + 44 + data.following = true; 45 + 46 + try { 47 + const response = await fetch('/api/follow', { 48 + method: 'POST', 49 + body: JSON.stringify({ did: data.did }) 50 + }); 51 + if (response.ok) { 52 + toast.success('Followed'); 53 + } else { 54 + toast.error('You must be logged in to follow'); 55 + data.following = false; 56 + } 57 + } catch (e) { 58 + console.error(e); 59 + toast.error('Must be logged in to follow'); 60 + 61 + data.following = false; 62 + } 63 + }} 64 + class={cn( 65 + 'inline-flex justify-center rounded-xl bg-accent-500/10 px-3 py-2 text-sm font-semibold text-accent-400 shadow-sm ring-1 ring-inset ring-accent-300/20 hover:bg-accent-400/20', 66 + data.profile.banner ? 'mt-6' : 'mt-3', 67 + data.following ? 'bg-accent-500/10' : '' 68 + )} 69 + > 70 + <span>{data.following ? 'Following' : 'Follow'}</span> 71 + </button> 72 + </div> 73 + {/if} 18 74 75 + {#if data.isUser && data.follows} 76 + <p 77 + class="mx-auto mt-16 max-w-2xl truncate px-4 text-lg font-bold text-base-900 dark:text-base-100 sm:text-xl" 78 + > 79 + People you follow 80 + </p> 81 + <div class="mx-auto mt-4 flex max-w-2xl gap-x-6 overflow-x-auto px-4"> 82 + {#each data.follows as profile} 83 + {#if profile.did} 84 + <a href={`/user/` + profile.handle} class="flex flex-col items-center gap-1"> 85 + <Avatar src={profile.avatar ? profile.avatar : undefined} size="size-16" /> 86 + <div class="text-center text-xs font-medium">{profile.displayName}</div> 87 + <div class="text-center text-xs text-base-400">{profile.handle}</div> 88 + </a> 89 + {/if} 90 + {/each} 91 + </div> 92 + {/if} 19 93 {#if data.items.length > 0} 20 - <ReviewList reviews={data.items} class="mx-auto mt-8 max-w-2xl" /> 94 + <p 95 + class="mx-auto mt-16 max-w-2xl truncate px-4 text-lg font-bold text-base-900 dark:text-base-100 sm:text-xl" 96 + > 97 + Reviews 98 + </p> 99 + <div><ReviewList reviews={data.items} class="mx-auto mt-2 max-w-2xl" /></div> 21 100 {:else} 22 101 <p class="py-8 text-center text-base-500">No movies or tv shows rated yet.</p> 23 102 {/if}
+12
src/routes/win/[counter]/+page.svelte
··· 1 + <script lang="ts"> 2 + import Logo from '$lib/Components/UI/Logo.svelte'; 3 + </script> 4 + 5 + <div class="flex min-h-screen flex-col items-center justify-center px-6 py-12 lg:px-8"> 6 + <div class="sm:mx-auto sm:w-full sm:max-w-sm"> 7 + <Logo class="mx-auto size-10" /> 8 + <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-white"> 9 + You have Won. Congratulations 10 + </h2> 11 + </div> 12 + </div>
+43
static/game-controller-svgrepo-com.svg
··· 1 + <?xml version="1.0" encoding="iso-8859-1"?> 2 + <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 3 + <svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 4 + viewBox="0 0 470.001 470.001" xml:space="preserve"> 5 + <g> 6 + <g> 7 + <g> 8 + <path d="M469.413,349.114l-31.938-223.211c-2.786-19.467-12.513-37.35-27.387-50.355c-15.022-13.134-34.296-20.367-54.271-20.367 9 + c-28.272,0-54.562,14.564-69.647,38.296H183.831c-15.086-23.732-41.375-38.296-69.647-38.296 10 + c-19.976,0-39.249,7.233-54.271,20.367c-14.874,13.005-24.601,30.888-27.387,50.355L0.587,349.114 11 + C0.197,351.837,0,354.598,0,357.32c0,31.706,25.794,57.5,57.5,57.5c23.729,0,45.335-14.904,53.764-37.086l28.159-74.133 12 + c6.787,5.304,15.316,8.476,24.578,8.476c15.783,0,29.457-9.191,35.96-22.5h70.079c6.503,13.309,20.177,22.5,35.96,22.5 13 + c9.261,0,17.79-3.173,24.578-8.476l28.16,74.134c8.428,22.182,30.034,37.085,53.763,37.085c31.706,0,57.5-25.794,57.5-57.5 14 + C470,354.598,469.803,351.837,469.413,349.114z M164,297.077c-13.785,0-25-11.215-25-25c0-13.785,11.215-25,25-25 15 + c13.785,0,25,11.215,25,25C189,285.862,177.785,297.077,164,297.077z M306,297.077c-13.785,0-25-11.215-25-25 16 + c0-13.785,11.215-25,25-25c13.785,0,25,11.215,25,25C331,285.862,319.785,297.077,306,297.077z M412.5,399.82 17 + c-17.54,0-33.511-11.017-39.74-27.413l-31.181-82.086c2.819-5.475,4.421-11.674,4.421-18.244c0-22.056-17.944-40-40-40 18 + c-22.056,0-40,17.944-40,40c0,0.84,0.035,1.672,0.086,2.5h-62.172c0.051-0.828,0.086-1.66,0.086-2.5c0-22.056-17.944-40-40-40 19 + c-22.056,0-40,17.944-40,40c0,6.57,1.602,12.769,4.421,18.244l-31.18,82.086C91.011,388.803,75.04,399.82,57.5,399.82 20 + c-23.435,0-42.5-19.065-42.5-42.5c0-2.013,0.146-4.059,0.437-6.081l1.383-9.662l64.232,13.398c0.517,0.108,1.031,0.16,1.539,0.16 21 + c3.477,0,6.596-2.432,7.334-5.97c0.846-4.055-1.756-8.028-5.811-8.874l-65.165-13.593l21.704-151.683 22 + c13.65,26.774,41.475,45.165,73.532,45.165c45.49,0,82.5-37.009,82.5-82.5c0-2.46-0.109-4.939-0.325-7.369 23 + c-0.367-4.126-4.008-7.169-8.135-6.807c-4.126,0.367-7.173,4.009-6.807,8.135c0.177,1.989,0.267,4.021,0.267,6.041 24 + c0,37.22-30.28,67.5-67.5,67.5c-37.22,0-67.5-30.28-67.5-67.5c0-0.801-0.128-1.572-0.361-2.296l1.053-7.357 25 + c4.719-32.978,33.44-57.847,66.809-57.847c24.348,0,46.902,13.209,58.861,34.473c1.328,2.362,3.827,3.823,6.537,3.823H290.42 26 + c2.71,0,5.209-1.461,6.537-3.823c11.959-21.264,34.514-34.473,58.861-34.473c33.368,0,62.09,24.869,66.809,57.847l1.053,7.357 27 + c-0.233,0.724-0.361,1.495-0.361,2.296c0,37.22-30.28,67.5-67.5,67.5c-37.22,0-67.5-30.28-67.5-67.5 28 + c0-2.019,0.09-4.051,0.267-6.041c0.366-4.126-2.681-7.768-6.807-8.135c-4.108-0.363-7.768,2.681-8.135,6.807 29 + c-0.216,2.429-0.325,4.908-0.325,7.369c0,45.491,37.01,82.5,82.5,82.5c32.057,0,59.882-18.391,73.532-45.165l21.704,151.683 30 + l-65.164,13.594c-4.055,0.846-6.656,4.818-5.811,8.874c0.738,3.539,3.856,5.97,7.334,5.97c0.508,0,1.023-0.052,1.539-0.16 31 + l64.231-13.399l1.383,9.662c0.29,2.022,0.437,4.068,0.437,6.081C455,380.754,435.935,399.82,412.5,399.82z"/> 32 + <path d="M149.184,130.18h-27.5v-27.5c0-4.142-3.357-7.5-7.5-7.5s-7.5,3.358-7.5,7.5v27.5h-27.5c-4.143,0-7.5,3.358-7.5,7.5 33 + c0,4.142,3.357,7.5,7.5,7.5h27.5v27.5c0,4.142,3.357,7.5,7.5,7.5s7.5-3.358,7.5-7.5v-27.5h27.5c4.143,0,7.5-3.358,7.5-7.5 34 + C156.684,133.538,153.326,130.18,149.184,130.18z"/> 35 + <path d="M355.816,180.18c23.435,0,42.5-19.065,42.5-42.5s-19.065-42.5-42.5-42.5s-42.5,19.065-42.5,42.5 36 + S332.382,180.18,355.816,180.18z M355.816,110.18c15.163,0,27.5,12.336,27.5,27.5s-12.337,27.5-27.5,27.5 37 + c-15.163,0-27.5-12.336-27.5-27.5S340.653,110.18,355.816,110.18z"/> 38 + <path d="M246.781,244.577H223.22c-4.143,0-7.5,3.358-7.5,7.5c0,4.142,3.357,7.5,7.5,7.5h23.562c4.143,0,7.5-3.358,7.5-7.5 39 + C254.282,247.935,250.924,244.577,246.781,244.577z"/> 40 + </g> 41 + </g> 42 + </g> 43 + </svg>
+3 -3
static/plyr.css
··· 306 306 font-size: 13px; 307 307 font-size: var(--plyr-font-size-menu, var(--plyr-font-size-small, 13px)); 308 308 padding: 4.66667px 10.5px; 309 - padding: calc(var(--plyr-control-spacing, 10px) * 0.7/1.5) 309 + padding: calc(var(--plyr-control-spacing, 10px) * 0.7 / 1.5) 310 310 calc(var(--plyr-control-spacing, 10px) * 0.7 * 1.5); 311 311 -webkit-user-select: none; 312 312 user-select: none; ··· 347 347 margin: 7px; 348 348 margin: calc(var(--plyr-control-spacing, 10px) * 0.7); 349 349 margin-bottom: 3.5px; 350 - margin-bottom: calc(var(--plyr-control-spacing, 10px) * 0.7/2); 350 + margin-bottom: calc(var(--plyr-control-spacing, 10px) * 0.7 / 2); 351 351 padding-left: 28px; 352 352 padding-left: calc(var(--plyr-control-spacing, 10px) * 0.7 * 4); 353 353 position: relative; ··· 369 369 height: 1px; 370 370 left: 0; 371 371 margin-top: 3.5px; 372 - margin-top: calc(var(--plyr-control-spacing, 10px) * 0.7/2); 372 + margin-top: calc(var(--plyr-control-spacing, 10px) * 0.7 / 2); 373 373 overflow: hidden; 374 374 position: absolute; 375 375 right: 0;