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

format

Florian (Dec 16, 2024, 3:51 AM +0100) ac8c992c adefd57f

+417 -352
+1 -2
src/app.css
··· 2 2 @import 'tailwindcss/components'; 3 3 @import 'tailwindcss/utilities'; 4 4 5 - 6 5 ::selection { 7 6 @apply bg-accent-800/50 text-accent-50; 8 - } 7 + }
+48 -3
src/app.html
··· 1 1 <!doctype html> 2 2 <html lang="en" class="h-full"> 3 3 <script> 4 - !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]); 5 - posthog.init('phc_1Q4q6SdTwvStnxFWbmdOIusLc5ve0u6Fk7WpsHPoAlD',{api_host:'https://eu.i.posthog.com', person_profiles: 'identified_only', persistence: 'memory'}) 4 + !(function (t, e) { 5 + var o, n, p, r; 6 + e.__SV || 7 + ((window.posthog = e), 8 + (e._i = []), 9 + (e.init = function (i, s, a) { 10 + function g(t, e) { 11 + var o = e.split('.'); 12 + 2 == o.length && ((t = t[o[0]]), (e = o[1])), 13 + (t[e] = function () { 14 + t.push([e].concat(Array.prototype.slice.call(arguments, 0))); 15 + }); 16 + } 17 + ((p = t.createElement('script')).type = 'text/javascript'), 18 + (p.async = !0), 19 + (p.src = 20 + s.api_host.replace('.i.posthog.com', '-assets.i.posthog.com') + '/static/array.js'), 21 + (r = t.getElementsByTagName('script')[0]).parentNode.insertBefore(p, r); 22 + var u = e; 23 + for ( 24 + void 0 !== a ? (u = e[a] = []) : (a = 'posthog'), 25 + u.people = u.people || [], 26 + u.toString = function (t) { 27 + var e = 'posthog'; 28 + return 'posthog' !== a && (e += '.' + a), t || (e += ' (stub)'), e; 29 + }, 30 + u.people.toString = function () { 31 + return u.toString(1) + '.people (stub)'; 32 + }, 33 + o = 34 + 'capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId'.split( 35 + ' ' 36 + ), 37 + n = 0; 38 + n < o.length; 39 + n++ 40 + ) 41 + g(u, o[n]); 42 + e._i.push([i, s, a]); 43 + }), 44 + (e.__SV = 1)); 45 + })(document, window.posthog || []); 46 + posthog.init('phc_1Q4q6SdTwvStnxFWbmdOIusLc5ve0u6Fk7WpsHPoAlD', { 47 + api_host: 'https://eu.i.posthog.com', 48 + person_profiles: 'identified_only', 49 + persistence: 'memory' 50 + }); 6 51 </script> 7 - 52 + 8 53 <head> 9 54 <meta charset="utf-8" /> 10 55 <link rel="icon" href="%sveltekit.assets%/favicon.svg" />
+32 -32
src/lib/Components/Avatar.svelte
··· 1 1 <script lang="ts"> 2 - import { cn } from '$lib/utils'; 2 + import { cn } from '$lib/utils'; 3 3 4 - type Props = { 5 - link?: string; 6 - size?: string; 7 - src?: string; 8 - alt?: string; 9 - }; 4 + type Props = { 5 + link?: string; 6 + size?: string; 7 + src?: string; 8 + alt?: string; 9 + }; 10 10 11 - const { link, size = 'size-11', src, alt }: Props = $props(); 11 + const { link, size = 'size-11', src, alt }: Props = $props(); 12 12 </script> 13 13 14 14 {#snippet avatar()} 15 - <div 16 - class={cn( 17 - 'border-base-400/50 bg-base-100 dark:border-base-700 dark:bg-base-800 overflow-hidden rounded-full border', 18 - size 19 - )} 20 - > 21 - {#if src} 22 - <img loading="lazy" class="h-full w-full object-cover" {src} {alt} /> 23 - {:else} 24 - <svg 25 - class="text-base-300 dark:text-base-600 size-full" 26 - fill="currentColor" 27 - viewBox="0 0 24 24" 28 - > 29 - <path 30 - d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" 31 - /> 32 - </svg> 33 - {/if} 34 - </div> 15 + <div 16 + class={cn( 17 + 'overflow-hidden rounded-full border border-base-400/50 bg-base-100 dark:border-base-700 dark:bg-base-800', 18 + size 19 + )} 20 + > 21 + {#if src} 22 + <img loading="lazy" class="h-full w-full object-cover" {src} {alt} /> 23 + {:else} 24 + <svg 25 + class="size-full text-base-300 dark:text-base-600" 26 + fill="currentColor" 27 + viewBox="0 0 24 24" 28 + > 29 + <path 30 + d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" 31 + /> 32 + </svg> 33 + {/if} 34 + </div> 35 35 {/snippet} 36 36 37 37 {#if link} 38 - <a href={link} class="inline-block" target="_blank" rel="noopener noreferrer nofollow"> 39 - {@render avatar()} 40 - </a> 38 + <a href={link} class="inline-block" target="_blank" rel="noopener noreferrer nofollow"> 39 + {@render avatar()} 40 + </a> 41 41 {:else} 42 - {@render avatar()} 42 + {@render avatar()} 43 43 {/if}
+27 -20
src/lib/Components/Footer.svelte
··· 1 - <footer class="bg-black border-t border-base-800 z-10 relative"> 2 - <div class="mx-auto max-w-2xl sm:px-6 lg:max-w-4xl xl:max-w-6xl py-12 md:flex md:items-center md:justify-between lg:px-8"> 3 - <div class="flex justify-center gap-x-6 md:order-2"> 4 - 5 - <a href="https://github.com/flo-bit/skywatched" class="text-base-400 hover:text-base-300" target="_blank"> 6 - <span class="sr-only">see source code</span> 7 - <svg class="size-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 8 - <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" /> 9 - </svg> 10 - </a> 11 - </div> 12 - <div class="md:order-1 mt-8 flex flex-col items-center md:items-start"> 13 - 14 - 15 - <img src="/tmdb_logo.svg" alt="TMDB logo" class="h-3" /> 16 - <p class="mt-2 text-center text-xs text-base-400">This website uses the TMDB API but is not endorsed or certified by TMDB.</p> 17 - 18 - </div> 1 + <footer class="relative z-10 border-t border-base-800 bg-black"> 2 + <div 3 + class="mx-auto max-w-2xl py-12 sm:px-6 md:flex md:items-center md:justify-between lg:max-w-4xl lg:px-8 xl:max-w-6xl" 4 + > 5 + <div class="flex justify-center gap-x-6 md:order-2"> 6 + <a 7 + href="https://github.com/flo-bit/skywatched" 8 + class="text-base-400 hover:text-base-300" 9 + target="_blank" 10 + > 11 + <span class="sr-only">see source code</span> 12 + <svg class="size-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 13 + <path 14 + fill-rule="evenodd" 15 + d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" 16 + clip-rule="evenodd" 17 + /> 18 + </svg> 19 + </a> 20 + </div> 21 + <div class="mt-8 flex flex-col items-center md:order-1 md:items-start"> 22 + <img src="/tmdb_logo.svg" alt="TMDB logo" class="h-3" /> 23 + <p class="mt-2 text-center text-xs text-base-400"> 24 + This website uses the TMDB API but is not endorsed or certified by TMDB. 25 + </p> 26 + </div> 19 27 </div> 20 - </footer> 21 - 28 + </footer>
+4 -4
src/lib/Components/Input.svelte
··· 1 1 <script lang="ts"> 2 - let { value = $bindable(), ...rest } = $props(); 2 + let { value = $bindable(), ...rest } = $props(); 3 3 </script> 4 4 5 5 <input 6 - bind:value 7 - class="dark:bg-base-950 text-base-900 dark:text-base-100 ring-base-300 dark:ring-base-700 placeholder:text-base-400 dark:placeholder:text-base-600 focus:ring-accent-600 block w-full rounded-full border-0 py-1.5 ring-1 shadow-sm ring-inset focus:ring-2 focus:ring-inset sm:text-sm/6" 8 - {...rest} 6 + bind:value 7 + class="block w-full rounded-full border-0 py-1.5 text-base-900 shadow-sm ring-1 ring-inset ring-base-300 placeholder:text-base-400 focus:ring-2 focus:ring-inset focus:ring-accent-600 sm:text-sm/6 dark:bg-base-950 dark:text-base-100 dark:ring-base-700 dark:placeholder:text-base-600" 8 + {...rest} 9 9 />
+1 -1
src/lib/Components/ItemCard.svelte
··· 2 2 import { rateMovieModal, watchedItems } from '$lib/state.svelte'; 3 3 import { cn } from '$lib/utils'; 4 4 import Rating from './Rating.svelte'; 5 - 5 + 6 6 const { 7 7 item, 8 8 showMark
+1 -1
src/lib/Components/ItemsGrid.svelte
··· 5 5 const { 6 6 items, 7 7 class: className, 8 - showMark, 8 + showMark 9 9 }: { 10 10 items: { poster_path: string; original_title: string; movieId?: number; showId?: number }[]; 11 11 class?: string;
+2 -7
src/lib/Components/ItemsList.svelte
··· 5 5 const { 6 6 items, 7 7 class: className, 8 - showMark, 8 + showMark 9 9 }: { 10 10 items: { poster_path: string; original_title: string; movieId?: number; showId?: number }[]; 11 11 class?: string; ··· 13 13 } = $props(); 14 14 </script> 15 15 16 - <div 17 - class={cn( 18 - 'flex gap-x-6 overflow-x-auto', 19 - className 20 - )} 21 - > 16 + <div class={cn('flex gap-x-6 overflow-x-auto', className)}> 22 17 {#each items as item} 23 18 <ItemCard {item} {showMark} /> 24 19 {/each}
+1 -1
src/lib/Components/LoginModal.svelte
··· 19 19 <div class="pointer-events-none fixed inset-0 z-10 w-screen overflow-y-auto"> 20 20 <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> 21 21 <div 22 - class="pointer-events-auto w-full relative transform overflow-hidden rounded-lg border border-base-700 bg-base-800 px-4 pb-4 pt-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6" 22 + class="pointer-events-auto relative w-full transform overflow-hidden rounded-lg border border-base-700 bg-base-800 px-4 pb-4 pt-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6" 23 23 > 24 24 <div> 25 25 <div class="text-center">
+44 -40
src/lib/Components/Profile.svelte
··· 1 1 <script lang="ts"> 2 - import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 2 + import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 3 3 4 - let { profile }: { profile: ProfileViewDetailed } = $props(); 4 + let { profile }: { profile: ProfileViewDetailed } = $props(); 5 5 </script> 6 6 7 7 {#if profile} 8 - <div class="sm:py-6"> 9 - <div> 10 - {#if profile.banner} 11 - <img class="aspect-[3/1] w-full object-cover sm:rounded-xl sm:border border-b border-base-800" src={profile.banner} alt="" /> 12 - {:else} 13 - <div class="aspect-[8/1] w-full"></div> 14 - {/if} 15 - </div> 16 - <div class="-mt-12 flex items-end space-x-5 px-4 sm:-mt-16 sm:px-6 lg:px-8"> 17 - <div class="flex"> 18 - <img 19 - class="ring-base-950 size-24 rounded-full ring-4 sm:size-32" 20 - src={profile.avatar} 21 - alt="" 22 - /> 23 - </div> 24 - <div 25 - class="flex min-w-0 flex-1 flex-row sm:flex-row sm:items-center sm:justify-end sm:space-x-6 sm:pb-1" 26 - > 27 - <div class="mt-4 flex min-w-0 flex-1 items-baseline gap-2 sm:mt-0"> 28 - <h1 class="text-base-900 dark:text-base-100 truncate text-xl font-bold"> 29 - {profile.displayName} 30 - </h1> 31 - <div class="text-base-900 dark:text-base-400 text-md truncate"> 32 - @{profile.handle} 33 - </div> 34 - </div> 35 - <div class="hidden mt-6 flex flex-row justify-stretch space-y-0 space-x-4"> 36 - <button 37 - type="button" 38 - class="text-base-900 ring-base-300 hover:bg-base-50 inline-flex justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold ring-1 shadow-sm ring-inset" 39 - > 40 - <span>Call</span> 41 - </button> 42 - </div> 43 - </div> 44 - </div> 45 - </div> 8 + <div class="sm:py-6"> 9 + <div> 10 + {#if profile.banner} 11 + <img 12 + class="aspect-[3/1] w-full border-b border-base-800 object-cover sm:rounded-xl sm:border" 13 + src={profile.banner} 14 + alt="" 15 + /> 16 + {:else} 17 + <div class="aspect-[8/1] w-full"></div> 18 + {/if} 19 + </div> 20 + <div class="-mt-12 flex items-end space-x-5 px-4 sm:-mt-16 sm:px-6 lg:px-8"> 21 + <div class="flex"> 22 + <img 23 + class="size-24 rounded-full ring-4 ring-base-950 sm:size-32" 24 + src={profile.avatar} 25 + alt="" 26 + /> 27 + </div> 28 + <div 29 + class="flex min-w-0 flex-1 flex-row sm:flex-row sm:items-center sm:justify-end sm:space-x-6 sm:pb-1" 30 + > 31 + <div class="mt-4 flex min-w-0 flex-1 items-baseline gap-2 sm:mt-0"> 32 + <h1 class="truncate text-xl font-bold text-base-900 dark:text-base-100"> 33 + {profile.displayName} 34 + </h1> 35 + <div class="text-md truncate text-base-900 dark:text-base-400"> 36 + @{profile.handle} 37 + </div> 38 + </div> 39 + <div class="mt-6 flex hidden flex-row justify-stretch space-x-4 space-y-0"> 40 + <button 41 + type="button" 42 + 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" 43 + > 44 + <span>Call</span> 45 + </button> 46 + </div> 47 + </div> 48 + </div> 49 + </div> 46 50 {/if}
+5 -4
src/lib/Components/RateMovieModal.svelte
··· 19 19 onclick={() => (rateMovieModal.showModal = false)} 20 20 aria-hidden="true" 21 21 ></div> 22 - <div class="pointer-events-none fixed inset-0 h-screen z-50 w-screen overflow-y-auto"> 22 + <div class="pointer-events-none fixed inset-0 z-50 h-screen w-screen overflow-y-auto"> 23 23 <div class="flex h-screen items-end justify-center p-4 text-center sm:items-center sm:p-0"> 24 - 25 24 <div 26 25 class="pointer-events-auto relative w-full transform overflow-hidden rounded-lg border border-base-700 bg-base-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:max-w-sm sm:p-6" 27 26 > 28 27 <div> 29 - <h3 class="text-lg font-semibold text-base-50" id="modal-title">Rate {rateMovieModal.selectedItem.name}</h3> 28 + <h3 class="text-lg font-semibold text-base-50" id="modal-title"> 29 + Rate {rateMovieModal.selectedItem.name} 30 + </h3> 30 31 31 32 <div class="mt-4 flex items-center gap-2"> 32 33 <p class="text-xs font-medium text-base-50">stars</p> 33 34 34 - <Rating bind:rating={rating} /> 35 + <Rating bind:rating /> 35 36 </div> 36 37 37 38 <div class="mt-4">
+13 -5
src/lib/Components/Rating.svelte
··· 1 1 <script lang="ts"> 2 2 import { cn } from '$lib/utils'; 3 - let { rating = $bindable(), size = "size-5" }: { rating: number, size?: string } = $props(); 3 + let { rating = $bindable(), size = 'size-5' }: { rating: number; size?: string } = $props(); 4 4 5 5 let hoverRating = $state(rating); 6 6 ··· 14 14 {#each Array.from({ length: 5 }).map((_, i) => i + 1) as i} 15 15 <button onclick={() => (rating = i)}> 16 16 <svg 17 - class={cn(size, 'shrink-0 text-base-600 stroke-base-500', i <= hoverRating && 'text-accent-500 stroke-accent-400')} 17 + class={cn( 18 + size, 19 + 'shrink-0 stroke-base-500 text-base-600', 20 + i <= hoverRating && 'stroke-accent-400 text-accent-500' 21 + )} 18 22 viewBox="0 0 24 24" 19 23 fill="currentColor" 20 24 aria-hidden="true" ··· 22 26 onmouseenter={() => (hoverRating = i)} 23 27 onmouseleave={() => (hoverRating = rating)} 24 28 > 25 - <path fill-rule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" clip-rule="evenodd" /> 26 - </svg> 27 - 29 + <path 30 + fill-rule="evenodd" 31 + d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 32 + clip-rule="evenodd" 33 + /> 34 + </svg> 35 + 28 36 <span class="sr-only">rate {i} stars</span> 29 37 </button> 30 38 {/each}
+12 -11
src/lib/Components/ReviewCard.svelte
··· 3 3 import Rating from './Rating.svelte'; 4 4 import RelativeTime from './relative-time/RelativeTime.svelte'; 5 5 6 - let { data, showMovieDetails = true }: { data: MainRecord; showMovieDetails?: boolean } = $props(); 6 + let { data, showMovieDetails = true }: { data: MainRecord; showMovieDetails?: boolean } = 7 + $props(); 7 8 </script> 8 9 9 10 <a ··· 14 15 > 15 16 <div class="flex gap-4"> 16 17 {#if showMovieDetails} 17 - <div 18 - class="relative z-20 aspect-[2/3] h-44 w-auto shrink-0 overflow-hidden rounded-md border border-base-800 bg-base-900/50 transition-opacity duration-75 group-hover:opacity-75" 19 - > 20 - {#if data.record.metadata?.poster_path} 21 - <img 22 - src="https://image.tmdb.org/t/p/w500{data.record.metadata.poster_path}" 23 - alt="movie poster for {data.record.metadata.title}" 24 - class="size-full object-cover object-center lg:size-full" 25 - /> 26 - {/if} 18 + <div 19 + class="relative z-20 aspect-[2/3] h-44 w-auto shrink-0 overflow-hidden rounded-md border border-base-800 bg-base-900/50 transition-opacity duration-75 group-hover:opacity-75" 20 + > 21 + {#if data.record.metadata?.poster_path} 22 + <img 23 + src="https://image.tmdb.org/t/p/w500{data.record.metadata.poster_path}" 24 + alt="movie poster for {data.record.metadata.title}" 25 + class="size-full object-cover object-center lg:size-full" 26 + /> 27 + {/if} 27 28 </div> 28 29 {/if} 29 30 <div class="flex w-full flex-col justify-center gap-4">
+23 -23
src/lib/Components/Select.svelte
··· 1 1 <script lang="ts"> 2 - import SelectItem from './SelectItem.svelte'; 2 + import SelectItem from './SelectItem.svelte'; 3 3 4 - type Props = { 5 - items: string[]; 6 - active: string; 7 - onchange?: (active: string) => void; 8 - }; 4 + type Props = { 5 + items: string[]; 6 + active: string; 7 + onchange?: (active: string) => void; 8 + }; 9 9 10 - let { items, active = $bindable(), onchange }: Props = $props(); 10 + let { items, active = $bindable(), onchange }: Props = $props(); 11 11 </script> 12 12 13 13 <div class="inline-block"> 14 - <ul 15 - class="text-base-800 shadow-base-800/5 ring-base-900/5 dark:text-base-200 dark:bg-base-950 flex w-fit rounded-full px-3 text-sm font-medium ring-1 shadow-lg backdrop-blur dark:ring-white/10" 16 - > 17 - {#each items as item} 18 - <SelectItem 19 - onclick={() => { 20 - if (active == item) return; 21 - active = item; 22 - if (onchange) onchange(item); 23 - }} 24 - isActive={active == item} 25 - > 26 - {item} 27 - </SelectItem> 28 - {/each} 29 - </ul> 14 + <ul 15 + class="flex w-fit rounded-full px-3 text-sm font-medium text-base-800 shadow-lg shadow-base-800/5 ring-1 ring-base-900/5 backdrop-blur dark:bg-base-950 dark:text-base-200 dark:ring-white/10" 16 + > 17 + {#each items as item} 18 + <SelectItem 19 + onclick={() => { 20 + if (active == item) return; 21 + active = item; 22 + if (onchange) onchange(item); 23 + }} 24 + isActive={active == item} 25 + > 26 + {item} 27 + </SelectItem> 28 + {/each} 29 + </ul> 30 30 </div>
+17 -17
src/lib/Components/SelectItem.svelte
··· 1 1 <script lang="ts"> 2 - import { fade } from 'svelte/transition'; 2 + import { fade } from 'svelte/transition'; 3 3 4 - const { onclick, isActive, children } = $props(); 4 + const { onclick, isActive, children } = $props(); 5 5 </script> 6 6 7 7 <li> 8 - <button 9 - {onclick} 10 - class={'relative block px-3 py-2 text-xs transition duration-200 ' + 11 - (isActive 12 - ? 'text-accent-500 dark:text-accent-400' 13 - : 'hover:text-accent-500 dark:hover:text-accent-400 cursor-pointer')} 14 - > 15 - {@render children()} 16 - {#if isActive} 17 - <span 18 - transition:fade 19 - class="from-accent-500/0 via-accent-500/40 to-accent-500/0 dark:from-accent-400/0 dark:via-accent-400/40 dark:to-accent-400/0 absolute inset-x-1 -bottom-px h-px bg-gradient-to-r" 20 - ></span> 21 - {/if} 22 - </button> 8 + <button 9 + {onclick} 10 + class={'relative block px-3 py-2 text-xs transition duration-200 ' + 11 + (isActive 12 + ? 'text-accent-500 dark:text-accent-400' 13 + : 'cursor-pointer hover:text-accent-500 dark:hover:text-accent-400')} 14 + > 15 + {@render children()} 16 + {#if isActive} 17 + <span 18 + transition:fade 19 + class="absolute inset-x-1 -bottom-px h-px bg-gradient-to-r from-accent-500/0 via-accent-500/40 to-accent-500/0 dark:from-accent-400/0 dark:via-accent-400/40 dark:to-accent-400/0" 20 + ></span> 21 + {/if} 22 + </button> 23 23 </li>
+16 -9
src/lib/Components/Sidebar.svelte
··· 54 54 class={cn( 55 55 'group flex gap-x-3 rounded-md p-3 text-sm/6 font-semibold', 56 56 $page.url.pathname === item.href 57 - ? 'text-accent-400 bg-accent-950/10' 58 - : 'text-base-200 hover:bg-accent-950/20 hover:text-accent-400 transition-colors duration-100' 57 + ? 'bg-accent-950/10 text-accent-400' 58 + : 'text-base-200 transition-colors duration-100 hover:bg-accent-950/20 hover:text-accent-400' 59 59 )} 60 60 > 61 61 {@html item.icon} ··· 71 71 {/key} 72 72 73 73 {#if showSidebar.value} 74 - <button transition:fade class="fixed md:hidden inset-0 z-40 bg-base-900/50 backdrop-blur-sm" onclick={() => showSidebar.toggle()}> 74 + <button 75 + transition:fade 76 + class="fixed inset-0 z-40 bg-base-900/50 backdrop-blur-sm md:hidden" 77 + onclick={() => showSidebar.toggle()} 78 + > 75 79 <span class="sr-only">Close Menu</span> 76 80 </button> 77 81 {:else} 78 82 <button 79 - class="md:hidden fixed bottom-2 left-2 z-50 rounded-xl border border-base-800 bg-base-900/75 backdrop-blur-sm p-2" 83 + class="fixed bottom-2 left-2 z-50 rounded-xl border border-base-800 bg-base-900/75 p-2 backdrop-blur-sm md:hidden" 80 84 onclick={() => showSidebar.toggle()} 81 85 > 82 86 <span class="sr-only">Open Menu</span> 83 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 84 - <path fill-rule="evenodd" d="M3 9a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 9Zm0 6.75a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" /> 85 - </svg> 86 - 87 - </button> 87 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 88 + <path 89 + fill-rule="evenodd" 90 + d="M3 9a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 9Zm0 6.75a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" 91 + clip-rule="evenodd" 92 + /> 93 + </svg> 94 + </button> 88 95 {/if}
+51 -51
src/lib/Components/User.svelte
··· 1 1 <script lang="ts"> 2 - import { showLoginModal } from '$lib/state.svelte'; 3 - import { cn } from '$lib/utils'; 2 + import { showLoginModal } from '$lib/state.svelte'; 3 + import { cn } from '$lib/utils'; 4 4 5 - type Props = { 6 - user?: { 7 - avatar?: string; 8 - handle?: string; 9 - }; 10 - }; 5 + type Props = { 6 + user?: { 7 + avatar?: string; 8 + handle?: string; 9 + }; 10 + }; 11 11 12 - const { user }: Props = $props(); 12 + const { user }: Props = $props(); 13 13 </script> 14 14 15 15 {#snippet renderUser()} 16 - <span class="sr-only">Open user menu</span> 17 - <div 18 - class={cn( 19 - 'border-base-400/50 bg-base-100 dark:border-base-700 hover:border-accent-400 dark:bg-base-800 size-8 overflow-hidden rounded-full border transition-colors duration-100', 20 - false 21 - ? 'bg-accent-100/50 dark:bg-accent-800/50 border-accent-300/50 dark:border-accent-500/50' 22 - : '' 23 - )} 24 - > 25 - {#if user?.avatar} 26 - <img loading="lazy" class="h-full w-full object-cover" src={user.avatar} alt="" /> 27 - {:else} 28 - <svg 29 - class={cn( 30 - 'text-base-300 dark:text-base-600 size-full', 31 - user ? 'text-accent-300 dark:text-accent-500' : '' 32 - )} 33 - fill="currentColor" 34 - viewBox="0 0 24 24" 35 - > 36 - <path 37 - d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" 38 - /> 39 - </svg> 40 - {/if} 41 - </div> 16 + <span class="sr-only">Open user menu</span> 17 + <div 18 + class={cn( 19 + 'size-8 overflow-hidden rounded-full border border-base-400/50 bg-base-100 transition-colors duration-100 hover:border-accent-400 dark:border-base-700 dark:bg-base-800', 20 + false 21 + ? 'border-accent-300/50 bg-accent-100/50 dark:border-accent-500/50 dark:bg-accent-800/50' 22 + : '' 23 + )} 24 + > 25 + {#if user?.avatar} 26 + <img loading="lazy" class="h-full w-full object-cover" src={user.avatar} alt="" /> 27 + {:else} 28 + <svg 29 + class={cn( 30 + 'size-full text-base-300 dark:text-base-600', 31 + user ? 'text-accent-300 dark:text-accent-500' : '' 32 + )} 33 + fill="currentColor" 34 + viewBox="0 0 24 24" 35 + > 36 + <path 37 + d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" 38 + /> 39 + </svg> 40 + {/if} 41 + </div> 42 42 {/snippet} 43 43 44 44 <div class="relative"> 45 - {#if user} 46 - <a href="/user/{user.handle}"> 47 - {@render renderUser()} 48 - </a> 49 - {:else} 50 - <button 51 - type="button" 52 - class="flex cursor-pointer items-center hover:opacity-80" 53 - id="user-menu-button" 54 - aria-expanded="false" 55 - aria-haspopup="true" 56 - onclick={() => showLoginModal.toggle()} 57 - > 58 - {@render renderUser()} 59 - </button> 60 - {/if} 45 + {#if user} 46 + <a href="/user/{user.handle}"> 47 + {@render renderUser()} 48 + </a> 49 + {:else} 50 + <button 51 + type="button" 52 + class="flex cursor-pointer items-center hover:opacity-80" 53 + id="user-menu-button" 54 + aria-expanded="false" 55 + aria-haspopup="true" 56 + onclick={() => showLoginModal.toggle()} 57 + > 58 + {@render renderUser()} 59 + </button> 60 + {/if} 61 61 </div>
+1 -1
src/lib/Components/VideoPlayer.svelte
··· 53 53 ></button> 54 54 <div 55 55 class={cn( 56 - 'aspect-video max-h-screen relative w-full overflow-hidden rounded-xl border border-black bg-white object-cover dark:border-white/10 dark:bg-white/5', 56 + 'aspect-video relative max-h-screen w-full overflow-hidden rounded-xl border border-black bg-white object-cover dark:border-white/10 dark:bg-white/5', 57 57 className 58 58 )} 59 59 >
+9 -9
src/lib/Components/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>
+19 -19
src/lib/Components/relative-time/action.ts
··· 2 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 - function init(options: Options) { 14 - const date = options.date; 15 - const locale = options.locale || navigator.language; 16 - const live = (options.live = true); 13 + function init(options: Options) { 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); 19 - } 18 + register(node, date, locale, live, callback); 19 + } 20 20 21 - init(options); 21 + init(options); 22 22 23 - return { 24 - update(options: Options) { 25 - init(options); 26 - }, 27 - destroy() { 28 - unregister(node); 29 - } 30 - }; 23 + return { 24 + update(options: Options) { 25 + init(options); 26 + }, 27 + destroy() { 28 + unregister(node); 29 + } 30 + }; 31 31 }
+6 -6
src/lib/Components/relative-time/formatter.ts
··· 3 3 4 4 // get the Intl.RelativeTimeFormat formatter for the given locale 5 5 export function getFormatter(locale: string) { 6 - if (formatters.has(locale)) { 7 - return formatters.get(locale)!; 8 - } 9 - const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' }); 10 - formatters.set(locale, formatter); 11 - return formatter; 6 + if (formatters.has(locale)) { 7 + return formatters.get(locale)!; 8 + } 9 + const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' }); 10 + formatters.set(locale, formatter); 11 + return formatter; 12 12 }
+36 -36
src/lib/Components/relative-time/state.ts
··· 3 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 ··· 14 14 15 15 // register or update instance 16 16 export function register( 17 - instance: Object, 18 - date: Date | number, 19 - locale: string, 20 - live: boolean, 21 - callback: Callback 17 + instance: Object, 18 + date: Date | number, 19 + locale: string, 20 + live: boolean, 21 + callback: Callback 22 22 ) { 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 24 - const formatter = getFormatter(locale); 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 24 + const formatter = getFormatter(locale); 25 25 26 - // create state to render 27 - const state = { date, callback, formatter }; 26 + // create state to render 27 + const state = { date, callback, formatter }; 28 28 29 - // initial render is immediate, so works for SSR 30 - const update = render(state, Date.now()); 29 + // initial render is immediate, so works for SSR 30 + const update = render(state, Date.now()); 31 31 32 - // if it's to update live, we keep a track and schedule the next update 33 - if (live) { 34 - instances.set(instance, { ...state, update }); 35 - } else { 36 - instances.delete(instance); 37 - } 32 + // if it's to update live, we keep a track and schedule the next update 33 + if (live) { 34 + instances.set(instance, { ...state, update }); 35 + } else { 36 + instances.delete(instance); 37 + } 38 38 39 - // start the clock ticking if there are any live instances 40 - if (instances.size) { 41 - updateInterval = 42 - updateInterval || 43 - setInterval(() => { 44 - const now = Date.now(); 45 - for (const state of instances.values()) { 46 - if (state.update <= now) { 47 - state.update = render(state, now); 48 - } 49 - } 50 - }, 1000); 51 - } 39 + // start the clock ticking if there are any live instances 40 + if (instances.size) { 41 + updateInterval = 42 + updateInterval || 43 + setInterval(() => { 44 + const now = Date.now(); 45 + for (const state of instances.values()) { 46 + if (state.update <= now) { 47 + state.update = render(state, now); 48 + } 49 + } 50 + }, 1000); 51 + } 52 52 } 53 53 54 54 export function unregister(instance: Object) { 55 - instances.delete(instance); 56 - if (instances.size === 0) { 57 - clearInterval(updateInterval); 58 - updateInterval = 0; 59 - } 55 + instances.delete(instance); 56 + if (instances.size === 0) { 57 + clearInterval(updateInterval); 58 + updateInterval = 0; 59 + } 60 60 }
+1 -1
src/lib/index.ts
··· 2 2 3 3 export const BASE_PATH = 'https://skywatched.app'; 4 4 5 - export const REL_COLLECTION = 'my.skylights.rel'; 5 + export const REL_COLLECTION = 'my.skylights.rel';
+38 -38
src/lib/server/crypts.ts
··· 1 1 // Code by @pilcrowonpaper on GitHub: https://gist.github.com/pilcrowonpaper/353318556029221c8e25f451b91e5f76 2 2 // AES128 with the Web Crypto API. 3 3 async function encrypt(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> { 4 - const iv = new Uint8Array(16); 5 - crypto.getRandomValues(iv); 6 - const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]); 7 - const cipher = await crypto.subtle.encrypt( 8 - { 9 - name: "AES-GCM", 10 - iv, 11 - tagLength: 128 12 - }, 13 - cryptoKey, 14 - data 15 - ); 16 - const encrypted = new Uint8Array(iv.byteLength + cipher.byteLength); 17 - encrypted.set(iv); 18 - encrypted.set(new Uint8Array(cipher), iv.byteLength); 19 - return encrypted; 4 + const iv = new Uint8Array(16); 5 + crypto.getRandomValues(iv); 6 + const cryptoKey = await crypto.subtle.importKey('raw', key, 'AES-GCM', false, ['encrypt']); 7 + const cipher = await crypto.subtle.encrypt( 8 + { 9 + name: 'AES-GCM', 10 + iv, 11 + tagLength: 128 12 + }, 13 + cryptoKey, 14 + data 15 + ); 16 + const encrypted = new Uint8Array(iv.byteLength + cipher.byteLength); 17 + encrypted.set(iv); 18 + encrypted.set(new Uint8Array(cipher), iv.byteLength); 19 + return encrypted; 20 20 } 21 - 21 + 22 22 export async function encryptString(key: Uint8Array, data: string): Promise<Uint8Array> { 23 - const encoded = new TextEncoder().encode(data); 24 - const encrypted = await encrypt(key, encoded); 25 - return encrypted; 23 + const encoded = new TextEncoder().encode(data); 24 + const encrypted = await encrypt(key, encoded); 25 + return encrypted; 26 26 } 27 27 28 28 async function decrypt(key: Uint8Array, encrypted: Uint8Array): Promise<Uint8Array> { 29 - if (encrypted.length < 16) { 30 - throw new Error("Invalid data"); 31 - } 32 - const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]); 33 - const decrypted = await crypto.subtle.decrypt( 34 - { 35 - name: "AES-GCM", 36 - iv: encrypted.slice(0, 16), 37 - tagLength: 128 38 - }, 39 - cryptoKey, 40 - encrypted.slice(16) 41 - ); 42 - return new Uint8Array(decrypted); 29 + if (encrypted.length < 16) { 30 + throw new Error('Invalid data'); 31 + } 32 + const cryptoKey = await crypto.subtle.importKey('raw', key, 'AES-GCM', false, ['decrypt']); 33 + const decrypted = await crypto.subtle.decrypt( 34 + { 35 + name: 'AES-GCM', 36 + iv: encrypted.slice(0, 16), 37 + tagLength: 128 38 + }, 39 + cryptoKey, 40 + encrypted.slice(16) 41 + ); 42 + return new Uint8Array(decrypted); 43 43 } 44 - 44 + 45 45 export async function decryptToString(key: Uint8Array, data: Uint8Array): Promise<string> { 46 - const decrypted = await decrypt(key, data); 47 - const decoded = new TextDecoder().decode(decrypted); 48 - return decoded; 46 + const decrypted = await decrypt(key, data); 47 + const decoded = new TextDecoder().decode(decrypted); 48 + return decoded; 49 49 }
-1
src/routes/+page.server.ts
··· 59 59 redirect(301, '/'); 60 60 } 61 61 }; 62 -
+1 -2
src/routes/[kind]/[id]/+page.svelte
··· 84 84 <button 85 85 onclick={() => videoPlayer.show(data.trailer ?? '')} 86 86 type="button" 87 - class="inline-flex mb-4 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 sm:hidden" 87 + class="mb-4 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 sm:hidden" 88 88 > 89 89 <svg 90 90 xmlns="http://www.w3.org/2000/svg" ··· 133 133 <div class="mb-2 text-lg font-semibold">overview</div> 134 134 {data.result.overview} 135 135 </div> 136 - 137 136 138 137 {#if data.recommendations.length > 0} 139 138 <div class="mb-2 mt-8 text-lg font-semibold">recommendations</div>
+3 -3
src/routes/client-metadata.json/+server.ts
··· 1 - import { atclient } from "$lib/server/client"; 2 - import { json } from "@sveltejs/kit"; 1 + import { atclient } from '$lib/server/client'; 2 + import { json } from '@sveltejs/kit'; 3 3 4 4 export async function GET() { 5 - return json(atclient.clientMetadata); 5 + return json(atclient.clientMetadata); 6 6 }
+3 -3
src/routes/review/[uri]/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import ReviewCard from '$lib/Components/ReviewCard.svelte'; 2 + import { onMount } from 'svelte'; 3 + import ReviewCard from '$lib/Components/ReviewCard.svelte'; 4 4 import Container from '$lib/Components/Container.svelte'; 5 5 6 - let { data } = $props(); 6 + let { data } = $props(); 7 7 </script> 8 8 9 9 <Container>
+2 -2
src/routes/user/[handle]/+page.svelte
··· 10 10 <Profile profile={data.profile} /> 11 11 12 12 {#if data.items.length > 0} 13 - <div class="flex flex-col gap-8 w-full items-center py-8 px-4"> 13 + <div class="flex w-full flex-col items-center gap-8 px-4 py-8"> 14 14 {#each data.items as item} 15 15 <ReviewCard data={item} /> 16 16 {/each} 17 17 </div> 18 18 {:else} 19 - <p class="text-center text-base-500 py-8">No movies or tv shows rated yet.</p> 19 + <p class="py-8 text-center text-base-500">No movies or tv shows rated yet.</p> 20 20 {/if} 21 21 </Container>