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

update design

Florian (Dec 16, 2024, 5:21 AM +0100) 7aa4ed07 ac8c992c

+121 -94
+3 -7
src/lib/Components/Profile.svelte
··· 1 1 <script lang="ts"> 2 2 import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 3 + import Avatar from './Avatar.svelte'; 3 4 4 5 let { profile }: { profile: ProfileViewDetailed } = $props(); 5 6 </script> ··· 18 19 {/if} 19 20 </div> 20 21 <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> 22 + 23 + <Avatar src={profile.avatar} size="size-24 sm:size-32" /> 28 24 <div 29 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" 30 26 >
+3 -3
src/lib/Components/RateMovieModal.svelte
··· 22 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 24 <div 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" 25 + 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" 26 26 > 27 27 <div> 28 28 <h3 class="text-lg font-semibold text-base-50" id="modal-title"> ··· 32 32 <div class="mt-4 flex items-center gap-2"> 33 33 <p class="text-xs font-medium text-base-50">stars</p> 34 34 35 - <Rating bind:rating /> 35 + <Rating bind:rating canChange/> 36 36 </div> 37 37 38 38 <div class="mt-4"> ··· 44 44 id="comment" 45 45 bind:value={review} 46 46 placeholder="write a review" 47 - class="block w-full rounded-md bg-base-900 px-3 py-1.5 text-base text-base-50 outline outline-1 -outline-offset-1 outline-base-700 placeholder:text-base-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-accent-600 sm:text-sm/6" 47 + class="block w-full rounded-md bg-base-950 px-3 py-1.5 text-base text-base-50 outline outline-1 -outline-offset-1 outline-base-700 placeholder:text-base-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-accent-600 sm:text-sm/6" 48 48 ></textarea> 49 49 </div> 50 50 </div>
+36 -10
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 { 4 + rating = $bindable(), 5 + size = 'size-5', 6 + canChange = false 7 + }: { rating: number; size?: string; canChange?: boolean } = $props(); 4 8 5 9 let hoverRating = $state(rating); 6 10 ··· 11 15 12 16 <div class="flex items-center xl:col-span-1"> 13 17 <div class="flex items-center"> 14 - {#each Array.from({ length: 5 }).map((_, i) => i + 1) as i} 15 - <button onclick={() => (rating = i)}> 18 + {#if canChange} 19 + {#each Array.from({ length: 5 }).map((_, i) => i + 1) as i} 20 + <button onclick={() => (rating = i)}> 21 + <svg 22 + class={cn( 23 + size, 24 + 'shrink-0 stroke-base-500 text-base-600', 25 + i <= hoverRating && 'stroke-accent-400 text-accent-500' 26 + )} 27 + viewBox="0 0 24 24" 28 + fill="currentColor" 29 + aria-hidden="true" 30 + data-slot="icon" 31 + onmouseenter={() => (hoverRating = i)} 32 + onmouseleave={() => (hoverRating = rating)} 33 + > 34 + <path 35 + fill-rule="evenodd" 36 + 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" 37 + clip-rule="evenodd" 38 + /> 39 + </svg> 40 + 41 + <span class="sr-only">rate {i} stars</span> 42 + </button> 43 + {/each} 44 + {:else} 45 + {#each Array.from({ length: 5 }).map((_, i) => i + 1) as i} 16 46 <svg 17 47 class={cn( 18 48 size, 19 49 'shrink-0 stroke-base-500 text-base-600', 20 - i <= hoverRating && 'stroke-accent-400 text-accent-500' 50 + i <= rating && 'stroke-accent-400 text-accent-500' 21 51 )} 22 52 viewBox="0 0 24 24" 23 53 fill="currentColor" 24 54 aria-hidden="true" 25 55 data-slot="icon" 26 - onmouseenter={() => (hoverRating = i)} 27 - onmouseleave={() => (hoverRating = rating)} 28 56 > 29 57 <path 30 58 fill-rule="evenodd" ··· 32 60 clip-rule="evenodd" 33 61 /> 34 62 </svg> 35 - 36 - <span class="sr-only">rate {i} stars</span> 37 - </button> 38 - {/each} 63 + {/each} 64 + {/if} 39 65 </div> 40 66 </div>
+49 -44
src/lib/Components/ReviewCard.svelte
··· 6 6 let { data, showMovieDetails = true }: { data: MainRecord; showMovieDetails?: boolean } = 7 7 $props(); 8 8 </script> 9 + <div class="w-full max-w-2xl p-6 backdrop-blur-sm"> 10 + {#if showMovieDetails} 11 + <a 12 + href={data.record.item.ref === 'tmdb:m' 13 + ? `/movie/${data.record.item.value}` 14 + : `/tv/${data.record.item.value}`} 15 + class="relative z-20 float-left mr-4 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 hover:opacity-80 group-hover:opacity-75" 16 + > 17 + {#if data.record.metadata?.poster_path} 18 + <img 19 + src="https://image.tmdb.org/t/p/w500{data.record.metadata.poster_path}" 20 + alt="movie poster for {data.record.metadata.title}" 21 + class="size-full object-cover object-center lg:size-full" 22 + /> 23 + {/if} 24 + <span class="sr-only">View {data.record.metadata?.title}</span> 25 + </a> 26 + {/if} 9 27 10 - <a 11 - href={data.record.item.ref === 'tmdb:m' 12 - ? `/movie/${data.record.item.value}` 13 - : `/tv/${data.record.item.value}`} 14 - class="flex w-full max-w-2xl flex-col gap-2 rounded-xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm" 15 - > 16 - <div class="flex gap-4"> 17 - {#if showMovieDetails} 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 - /> 28 + <div class="flex flex-col gap-4"> 29 + <a href={`/user/${data.author.handle}`} class="flex flex-row items-center gap-4 font-medium"> 30 + <div class="flex items-center gap-2"> 31 + {#if data.author.avatar} 32 + <img src={data.author.avatar} alt="user avatar" class="size-5 rounded-full" /> 27 33 {/if} 34 + <div class="text-md font-semibold">{data.author.displayName || data.author.handle}</div> 28 35 </div> 29 - {/if} 30 - <div class="flex w-full flex-col justify-center gap-4"> 31 - <div class="flex w-full flex-row gap-4 sm:items-center"> 32 - <div class="flex items-center gap-2"> 33 - {#if data.author.avatar} 34 - <img src={data.author.avatar} alt="user avatar" class="size-5 rounded-full" /> 35 - {/if} 36 - <div class="text-md font-semibold">{data.author.displayName || data.author.handle}</div> 37 - </div> 38 - <div class="text-sm text-base-500"> 39 - <RelativeTime date={new Date(data.updatedAt)} locale="en-US" /> 40 - </div> 36 + <div class="text-sm text-base-500"> 37 + <RelativeTime date={new Date(data.updatedAt)} locale="en-US" /> 38 + </div> 39 + </a> 40 + 41 + <div class="flex flex-col gap-4"> 42 + <div class="flex flex-col {data.record.note?.value ? 'sm:flex-row sm:items-center' : ''} gap-4"> 43 + {#if showMovieDetails} 44 + <a 45 + href={data.record.item.ref === 'tmdb:m' 46 + ? `/movie/${data.record.item.value}` 47 + : `/tv/${data.record.item.value}`} 48 + class="text-2xl font-bold hover:text-accent-200" 49 + > 50 + {data.record.metadata?.title} 51 + </a> 52 + {/if} 53 + {#if data.record.rating?.value} 54 + <Rating rating={data.record.rating?.value / 2} size={data.record.note?.value ? 'size-6' : 'size-6'} /> 55 + {/if} 41 56 </div> 42 57 43 - <div class="flex w-full flex-col gap-4"> 44 - <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"> 45 - {#if showMovieDetails} 46 - <div class="text-2xl font-bold">{data.record.metadata?.title}</div> 47 - {/if} 48 - {#if data.record.rating?.value} 49 - <Rating rating={data.record.rating?.value / 2} size="size-6" /> 50 - {/if} 58 + {#if data.record.note?.value} 59 + <div class="text-sm text-base-300"> 60 + {@html data.record.note?.value?.replace('\n', '<br /><br />')} 51 61 </div> 52 62 53 - {#if data.record.note?.value} 54 - <div class="text-sm text-base-300"> 55 - {@html data.record.note?.value?.replace('\n', '<br /><br />')} 56 - </div> 57 - {/if} 58 - </div> 63 + {/if} 59 64 </div> 60 65 </div> 61 - </a> 66 + </div>
+13
src/lib/Components/ReviewList.svelte
··· 1 + <script lang="ts"> 2 + import type { MainRecord } from "$lib/db"; 3 + import { cn } from "$lib/utils"; 4 + import ReviewCard from "./ReviewCard.svelte"; 5 + 6 + let { reviews, class: className, showMovieDetails }: { reviews: MainRecord[]; class?: string; showMovieDetails?: boolean } = $props(); 7 + </script> 8 + 9 + <div class={cn("flex w-full flex-col divide-y divide-base-900", className)}> 10 + {#each reviews as review} 11 + <ReviewCard data={review} showMovieDetails={showMovieDetails} /> 12 + {/each} 13 + </div>
+1 -1
src/lib/Components/Sidebar.svelte
··· 46 46 )} 47 47 > 48 48 <ul role="list" class="flex h-full flex-col items-center justify-between space-y-1 pb-2"> 49 - <div class="flex flex-col items-center space-y-1"> 49 + <div class="flex flex-col items-center space-y-2"> 50 50 {#each menu as item} 51 51 <li> 52 52 <a
+1 -1
src/routes/+page.svelte
··· 19 19 skywatched 20 20 </h1> 21 21 <p class="text-md mt-8 text-pretty font-medium text-base-400 sm:text-lg"> 22 - social media for movies based on atproto. work in progress. 22 + social media for movies based on at proto. work in progress. 23 23 </p> 24 24 </div> 25 25
+10 -11
src/routes/[kind]/[id]/+page.svelte
··· 7 7 import ItemsList from '$lib/Components/ItemsList.svelte'; 8 8 import Rating from '$lib/Components/Rating.svelte'; 9 9 import ReviewCard from '$lib/Components/ReviewCard.svelte'; 10 + import ReviewList from '$lib/Components/ReviewList.svelte'; 10 11 11 12 let { data }: { data: PageData } = $props(); 12 13 </script> ··· 30 31 {data.result.original_title ?? data.result.original_name} 31 32 </div> 32 33 33 - {#if data.watchProviders.DE?.flatrate} 34 + {#if data.watchProviders.US?.flatrate} 34 35 <div class="mt-2 text-sm text-white sm:mt-4"> 35 36 <div class="mb-2 flex flex-wrap gap-4 text-xs font-medium"> 36 37 stream on ··· 44 45 </a></span 45 46 > 46 47 </div> 47 - <a href={data.watchProviders.DE.link} target="_blank" class="flex flex-wrap gap-2"> 48 - {#each data.watchProviders.DE.flatrate as provider} 48 + <a href={data.watchProviders.US.link} target="_blank" class="flex flex-wrap gap-2"> 49 + {#each data.watchProviders.US.flatrate as provider} 49 50 <img 50 51 src="https://image.tmdb.org/t/p/w500{provider.logo_path}" 51 52 alt={provider.provider_name} ··· 140 141 <ItemsList items={data.recommendations} showMark={!!data.user} /> 141 142 {/if} 142 143 143 - {#if data.ratings.length > 0} 144 - <div class="mb-2 mt-8 text-lg font-semibold">recent ratings</div> 145 - <div class="flex flex-col gap-2"> 146 - {#each data.ratings as rating} 147 - <ReviewCard data={rating} showMovieDetails={false} /> 148 - {/each} 149 - </div> 150 - {/if} 151 144 </div> 145 + 146 + {#if data.ratings.length > 0} 147 + <div class="mb-2 mt-8 text-lg font-semibold px-4">recent reviews</div> 148 + 149 + <ReviewList reviews={data.ratings} showMovieDetails={false} class="pb-8"/> 150 + {/if} 152 151 </Container>
+3 -11
src/routes/feed/+page.svelte
··· 2 2 import Container from '$lib/Components/Container.svelte'; 3 3 import { onMount } from 'svelte'; 4 4 import { type PageData } from './$types'; 5 - import ReviewCard from '$lib/Components/ReviewCard.svelte'; 5 + import ReviewList from '$lib/Components/ReviewList.svelte'; 6 6 7 7 let { data }: { data: PageData } = $props(); 8 - 9 - onMount(async () => { 10 - console.log(data); 11 - }); 12 8 </script> 13 9 14 10 <Container class="relative z-10 py-8 sm:py-16"> 15 11 <div class="flex flex-col gap-4 px-4"> 16 - <h1 class="pb-4 text-2xl font-bold">Feed</h1> 12 + <h1 class="pb-4 text-2xl font-bold">Recent reviews</h1> 17 13 </div> 18 14 19 15 {#if data.feed.length > 0} 20 - <div class="mx-auto flex max-w-2xl flex-col gap-4 px-4"> 21 - {#each data.feed as record} 22 - <ReviewCard data={record} /> 23 - {/each} 24 - </div> 16 + <ReviewList reviews={data.feed} /> 25 17 {/if} 26 18 </Container>
+2 -6
src/routes/user/[handle]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import Container from '$lib/Components/Container.svelte'; 3 3 import Profile from '$lib/Components/Profile.svelte'; 4 - import ReviewCard from '$lib/Components/ReviewCard.svelte'; 4 + import ReviewList from '$lib/Components/ReviewList.svelte'; 5 5 6 6 let { data } = $props(); 7 7 </script> ··· 10 10 <Profile profile={data.profile} /> 11 11 12 12 {#if data.items.length > 0} 13 - <div class="flex w-full flex-col items-center gap-8 px-4 py-8"> 14 - {#each data.items as item} 15 - <ReviewCard data={item} /> 16 - {/each} 17 - </div> 13 + <ReviewList reviews={data.items} class="mt-8" /> 18 14 {:else} 19 15 <p class="py-8 text-center text-base-500">No movies or tv shows rated yet.</p> 20 16 {/if}