[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 #95 from flo-bit/alpha-v2

allow editing, deleting of reviews

authored by

Florian and committed by
GitHub
(Jan 22, 2025, 7:52 PM +0100) e15bc42e c94d60f7

+423 -67
+197
src/lib/Components/OptionButton.svelte
··· 1 + <script lang="ts"> 2 + import { type MainRecord } from '$lib/db'; 3 + import { rateMovieModal, user } from '$lib/state.svelte'; 4 + import { createDropdownMenu, melt } from '@melt-ui/svelte'; 5 + import { toast } from 'svelte-sonner'; 6 + import { fly } from 'svelte/transition'; 7 + 8 + const { 9 + elements: { trigger, menu }, 10 + states: { open } 11 + } = createDropdownMenu({ 12 + forceVisible: true, 13 + loop: true 14 + }); 15 + 16 + export let data: MainRecord; 17 + </script> 18 + 19 + <button type="button" class="" use:melt={$trigger}> 20 + <svg 21 + xmlns="http://www.w3.org/2000/svg" 22 + fill="none" 23 + viewBox="0 0 24 24" 24 + stroke-width="1.5" 25 + stroke="currentColor" 26 + class="size-4" 27 + > 28 + <path 29 + stroke-linecap="round" 30 + stroke-linejoin="round" 31 + d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" 32 + /> 33 + </svg> 34 + 35 + <span class="sr-only">Open Popover</span> 36 + </button> 37 + 38 + {#if $open} 39 + <div 40 + 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 + use:melt={$menu} 42 + transition:fly={{ duration: 150, y: -10 }} 43 + > 44 + {#if data.author.handle === user?.handle} 45 + <button 46 + class="item" 47 + on:click={async () => { 48 + const response = await fetch(`/api/review?uri=${encodeURIComponent(data.uri)}`, { 49 + method: 'DELETE' 50 + }); 51 + console.log(await response.json()); 52 + 53 + $open = false; 54 + 55 + await new Promise((resolve) => setTimeout(resolve, 1000)); 56 + 57 + if (window.location.pathname.includes('/review/')) { 58 + window.location.href = '/'; 59 + } else { 60 + window.location.reload(); 61 + } 62 + }} 63 + ><svg 64 + xmlns="http://www.w3.org/2000/svg" 65 + fill="none" 66 + viewBox="0 0 24 24" 67 + stroke-width="1" 68 + stroke="currentColor" 69 + class="size-4" 70 + > 71 + <path 72 + stroke-linecap="round" 73 + stroke-linejoin="round" 74 + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" 75 + /> 76 + </svg> 77 + 78 + delete review</button 79 + > 80 + <button 81 + class="item" 82 + on:click={() => { 83 + $open = false; 84 + 85 + console.log(data.record.rating?.value) 86 + rateMovieModal.show({ 87 + movieId: data.record.item.ref === 'tmdb:m' ? parseInt(data.record.item.value) : undefined, 88 + showId: data.record.item.ref === 'tmdb:s' ? parseInt(data.record.item.value) : undefined, 89 + kind: data.record.item.ref === 'tmdb:s' ? 'show' : 'movie', 90 + name: data.record.metadata?.title, 91 + posterPath: data.record.metadata?.poster_path, 92 + currentRating: (data.record.rating?.value ?? 0) / 2, 93 + currentReview: data.record.note?.value, 94 + editUri: data.uri 95 + }); 96 + }} 97 + ><svg 98 + xmlns="http://www.w3.org/2000/svg" 99 + fill="none" 100 + viewBox="0 0 24 24" 101 + stroke-width="1" 102 + stroke="currentColor" 103 + class="size-4" 104 + > 105 + <path 106 + stroke-linecap="round" 107 + stroke-linejoin="round" 108 + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" 109 + /> 110 + </svg> 111 + 112 + edit review</button 113 + > 114 + {:else} 115 + <!-- <button 116 + class="item" 117 + on:click={() => { 118 + alert('Check for Updates...'); 119 + }} 120 + > 121 + <svg 122 + xmlns="http://www.w3.org/2000/svg" 123 + fill="none" 124 + viewBox="0 0 24 24" 125 + stroke-width="1" 126 + stroke="currentColor" 127 + class="size-4" 128 + > 129 + <path 130 + stroke-linecap="round" 131 + stroke-linejoin="round" 132 + d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" 133 + /> 134 + </svg> 135 + 136 + report review</button 137 + > --> 138 + {/if} 139 + 140 + {#if data.record.crosspost?.uri} 141 + <button class="item" on:click={() => {}}> 142 + <svg 143 + xmlns="http://www.w3.org/2000/svg" 144 + fill="none" 145 + viewBox="0 0 24 24" 146 + stroke-width="1" 147 + stroke="currentColor" 148 + class="size-4" 149 + > 150 + <path 151 + stroke-linecap="round" 152 + stroke-linejoin="round" 153 + d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3" 154 + /> 155 + </svg> 156 + go to crosspost 157 + </button> 158 + {/if} 159 + 160 + <button 161 + class="item" 162 + on:click={() => { 163 + $open = false; 164 + 165 + // copy link to clipboard 166 + navigator.clipboard.writeText( 167 + 'https://skywatched.app/review/' + encodeURIComponent(data.uri) 168 + ); 169 + 170 + toast.success('Link copied to clipboard'); 171 + }} 172 + > 173 + <svg 174 + xmlns="http://www.w3.org/2000/svg" 175 + fill="none" 176 + viewBox="0 0 24 24" 177 + stroke-width="1" 178 + stroke="currentColor" 179 + class="size-4" 180 + > 181 + <path 182 + stroke-linecap="round" 183 + stroke-linejoin="round" 184 + d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z" 185 + /> 186 + </svg> 187 + 188 + copy link to post 189 + </button> 190 + </div> 191 + {/if} 192 + 193 + <style> 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; 196 + } 197 + </style>
+82 -45
src/lib/Components/RateMovieModal.svelte
··· 13 13 }); 14 14 15 15 let sending = $state(false); 16 + 17 + async function saveNew() { 18 + const response = await fetch(`/api/rate`, { 19 + method: 'POST', 20 + body: JSON.stringify({ 21 + rating, 22 + review, 23 + kind: rateMovieModal.selectedItem.kind, 24 + id: rateMovieModal.selectedItem.movieId ?? rateMovieModal.selectedItem.showId 25 + }) 26 + }); 27 + 28 + if (!response.ok) { 29 + toast.error('Failed to save rating'); 30 + 31 + return; 32 + } 33 + rateMovieModal.showModal = false; 34 + 35 + const data = await response.json(); 36 + 37 + watchedItems.addRated({ 38 + movieId: rateMovieModal.selectedItem.movieId, 39 + showId: rateMovieModal.selectedItem.showId, 40 + rating 41 + }); 42 + 43 + toast.success('Rating saved'); 44 + 45 + if (settings.crosspostEnabled && review.length > 0) { 46 + await new Promise((resolve) => setTimeout(resolve, 1000)); 47 + 48 + crosspostModal.show(data.uri, review, rating, rateMovieModal.selectedItem.name ?? ''); 49 + } 50 + } 51 + 52 + async function saveEdit() { 53 + 54 + const response = await fetch(`/api/review`, { 55 + method: 'PUT', 56 + body: JSON.stringify({ 57 + rating, 58 + review, 59 + kind: rateMovieModal.selectedItem.kind, 60 + id: rateMovieModal.selectedItem.movieId ?? rateMovieModal.selectedItem.showId, 61 + uri: rateMovieModal.selectedItem.editUri 62 + }) 63 + }); 64 + 65 + if (!response.ok) { 66 + toast.error('Failed to save rating'); 67 + 68 + return; 69 + } 70 + 71 + toast.success('Rating saved'); 72 + 73 + rateMovieModal.showModal = false; 74 + 75 + await new Promise((resolve) => setTimeout(resolve, 1000)); 76 + 77 + window.location.reload(); 78 + } 16 79 </script> 17 80 18 81 {#if rateMovieModal.showModal} 19 82 <div class="relative z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true"> 20 83 <div 21 - class="fixed inset-0 bg-base-950/90 backdrop-blur-sm transition-opacity" 84 + class="bg-base-950/90 fixed inset-0 backdrop-blur-sm transition-opacity" 22 85 onclick={() => (rateMovieModal.showModal = false)} 23 86 aria-hidden="true" 24 87 ></div> 25 88 <div class="pointer-events-none fixed inset-0 z-50 h-[100dvh] w-screen overflow-y-auto"> 26 89 <div class="flex h-[100dvh] items-end justify-center p-4 text-center sm:items-center sm:p-0"> 27 90 <div 28 - 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" 91 + 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" 29 92 > 30 93 <button 31 - class="absolute right-2 top-2 rounded-full bg-base-800/50 p-1 hover:bg-base-800/80" 94 + class="bg-base-800/50 hover:bg-base-800/80 absolute right-2 top-2 rounded-full p-1" 32 95 onclick={() => (rateMovieModal.showModal = false)} 33 96 > 34 97 <svg ··· 46 109 </button> 47 110 48 111 <div> 49 - <h3 class="text-md mb-4 font-semibold text-base-50" id="modal-title"> 50 - Rate and review 112 + <h3 class="text-md text-base-50 mb-4 font-semibold" id="modal-title"> 113 + {#if rateMovieModal.selectedItem.editUri} 114 + Edit review 115 + {:else} 116 + Rate and review 117 + {/if} 51 118 </h3> 52 119 53 120 {#if rateMovieModal.selectedItem.name} 54 121 <div class="relative flex items-center gap-4"> 55 122 <div 56 - 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" 123 + 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" 57 124 > 58 125 {#if rateMovieModal.selectedItem.posterPath} 59 126 <img ··· 64 131 {/if} 65 132 </div> 66 133 <h3 67 - class="mb-4 flex flex-col gap-2 text-xl font-semibold text-base-50" 134 + class="text-base-50 mb-4 flex flex-col gap-2 text-xl font-semibold" 68 135 id="modal-title" 69 136 > 70 137 {rateMovieModal.selectedItem.name} ··· 85 152 {/if} 86 153 87 154 <div class="mt-4"> 88 - <label for="comment" class="block text-xs font-medium text-base-50">review</label> 155 + <label for="comment" class="text-base-50 block text-xs font-medium">review</label> 89 156 <div class="mt-2"> 90 157 <textarea 91 158 rows="4" ··· 93 160 id="comment" 94 161 bind:value={review} 95 162 placeholder="write a review" 96 - 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" 163 + 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" 97 164 ></textarea> 98 165 </div> 99 166 </div> ··· 101 168 <div class="mt-5 sm:mt-6"> 102 169 <button 103 170 onclick={async () => { 104 - const response = await fetch(`/api/rate`, { 105 - method: 'POST', 106 - body: JSON.stringify({ 107 - rating, 108 - review, 109 - kind: rateMovieModal.selectedItem.kind, 110 - id: rateMovieModal.selectedItem.movieId ?? rateMovieModal.selectedItem.showId 111 - }) 112 - }); 113 - 114 - if (!response.ok) { 115 - toast.error('Failed to save rating'); 116 - 117 - return; 171 + if (rateMovieModal.selectedItem.editUri) { 172 + saveEdit(); 173 + } else { 174 + saveNew(); 118 175 } 119 - rateMovieModal.showModal = false; 120 - 121 - const data = await response.json(); 122 - 123 - if (settings.crosspostEnabled && review.length > 0) { 124 - crosspostModal.show( 125 - data.uri, 126 - review, 127 - rating, 128 - rateMovieModal.selectedItem.name ?? '' 129 - ); 130 - } 131 - 132 - watchedItems.addRated({ 133 - movieId: rateMovieModal.selectedItem.movieId, 134 - showId: rateMovieModal.selectedItem.showId, 135 - rating 136 - }); 137 - 138 - toast.success('Rating saved'); 139 176 }} 140 177 type="button" 141 178 disabled={sending || !rateMovieModal.selectedItem.name} 142 - 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" 143 - >{sending ? 'Sending...' : 'Review'}</button 179 + 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" 180 + >{sending ? 'Sending...' : (rateMovieModal.selectedItem.editUri ? 'Update' : 'Review')}</button 144 181 > 145 182 </div> 146 183 </div>
+3 -2
src/lib/Components/ReviewCard.svelte
··· 4 4 import Rating from './Rating.svelte'; 5 5 import RelativeTime from './relative-time/RelativeTime.svelte'; 6 6 import { nameToId } from '$lib/utils'; 7 + import OptionButton from './OptionButton.svelte'; 7 8 // import { crosspostModal } from '$lib/state.svelte'; 8 9 9 10 let { data, showMovieDetails = true }: { data: MainRecord; showMovieDetails?: boolean } = ··· 119 120 {/if} 120 121 </button> --> 121 122 122 - <!-- this is kind of how the bookmark button should look like too 123 - (but it should work without login too and just use the local db) --> 124 123 <button 125 124 class="group inline-flex items-center gap-2 text-sm" 126 125 onclick={async () => { ··· 175 174 {/if} 176 175 {(data.record.likes ?? 0) + (isLiked ? 1 : 0)} 177 176 </button> 177 + 178 + <OptionButton data={data} /> 178 179 </div> 179 180 180 181 <!-- <a href={`/review/${encodeURIComponent(data.uri)}`}>
+8 -2
src/lib/state.svelte.ts
··· 40 40 posterPath: string | undefined; 41 41 currentRating: number | undefined; 42 42 currentReview: string | undefined; 43 + 44 + editUri?: string; 43 45 }; 44 46 45 47 export const rateMovieModal: { ··· 60 62 name: undefined, 61 63 posterPath: undefined, 62 64 currentRating: undefined, 63 - currentReview: undefined 65 + currentReview: undefined, 66 + 67 + editUri: undefined 64 68 }, 65 69 66 70 show: (item: RateMovieModalItem) => { ··· 80 84 name: undefined, 81 85 posterPath: undefined, 82 86 currentRating: undefined, 83 - currentReview: undefined 87 + currentReview: undefined, 88 + 89 + editUri: undefined 84 90 }; 85 91 rateMovieModal.showModal = true; 86 92 }
+2 -11
src/routes/+page.server.ts
··· 2 2 import { isValidHandle } from '@atproto/syntax'; 3 3 import { error, redirect, type Actions } from '@sveltejs/kit'; 4 4 5 - import { getRecentRecords, type MainRecord } from '$lib/db'; 6 - 7 - let feed: MainRecord[] = []; 8 - let lastUpdated: Date | undefined = undefined; 5 + import { getRecentRecords } from '$lib/db'; 9 6 10 7 /** @type {import('./$types').PageServerLoad} */ 11 8 export async function load() { 12 - const now = new Date(); 13 - // if lastUpdated is more than 2 minutes ago, update the feed 14 - if (!lastUpdated || now.getTime() - lastUpdated.getTime() > 2 * 60 * 1000) { 15 - feed = await getRecentRecords(); 16 - lastUpdated = now; 17 - } 18 - return { feed: feed.slice(0, 10) }; 9 + return { feed: (await getRecentRecords()).slice(0, 10) }; 19 10 } 20 11 21 12 export const actions: Actions = {
+99
src/routes/api/review/+server.ts
··· 1 + import { error, json, type RequestHandler } from '@sveltejs/kit'; 2 + import { AtpBaseClient } from '@atproto/api'; 3 + import { REL_COLLECTION } from '$lib'; 4 + 5 + export const DELETE: RequestHandler = async ({ locals, url }) => { 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 + // const uri = body.uri; 13 + // or get uri from params 14 + const uri = url.searchParams.get('uri') ?? ''; 15 + 16 + const [did, collection, rkey] = uri.replace('at://', '').split('/'); 17 + console.log(uri); 18 + 19 + console.log(did, user.did); 20 + if (did !== user.did) { 21 + return error(401, 'Unauthorized API call'); 22 + } 23 + 24 + if (collection !== REL_COLLECTION) { 25 + return error(400, 'Invalid collection'); 26 + } 27 + 28 + await agent.com.atproto.repo.deleteRecord({ repo: did, collection: collection, rkey }); 29 + 30 + return json({ status: 'deleted', uri: `at://${did}/${REL_COLLECTION}/${rkey}` }); 31 + }; 32 + 33 + export const PUT: RequestHandler = async ({ locals, request }) => { 34 + const user = locals.user; 35 + const agent = locals.agent; 36 + if (!user || !agent || agent instanceof AtpBaseClient) { 37 + return error(401, 'Unauthorized API call'); 38 + } 39 + 40 + const body = await request.json(); 41 + const uri = body.uri; 42 + 43 + const [did, collection, rkey] = uri.replace('at://', '').split('/'); 44 + 45 + if (did !== user.did) { 46 + return error(401, 'Unauthorized API call'); 47 + } 48 + 49 + if (collection !== REL_COLLECTION) { 50 + return error(400, 'Invalid collection'); 51 + } 52 + 53 + const rating = body.rating; 54 + const review = body.review; 55 + const kind = body.kind; 56 + const id = body.id; 57 + 58 + const record: { 59 + repo: string; 60 + collection: string; 61 + rkey: string; 62 + record: { 63 + item: { 64 + ref: string; 65 + value: string; 66 + }; 67 + rating: { value: number; createdAt: string }; 68 + note?: { 69 + value: string; 70 + createdAt: string; 71 + updatedAt: string; 72 + }; 73 + from?: string; 74 + }; 75 + } = { 76 + repo: did, 77 + collection: REL_COLLECTION, 78 + rkey, 79 + record: { 80 + item: { 81 + ref: `tmdb:${kind === 'movie' ? 'm' : 's'}`, 82 + value: id.toString() 83 + }, 84 + rating: { value: rating * 2, createdAt: new Date().toISOString() }, 85 + from: 'skywatched' 86 + } 87 + }; 88 + if (review) { 89 + record.record.note = { 90 + value: review, 91 + createdAt: new Date().toISOString(), 92 + updatedAt: new Date().toISOString() 93 + }; 94 + } 95 + 96 + await agent.com.atproto.repo.putRecord(record); 97 + 98 + return json({ status: 'ok' }); 99 + };
+16 -7
src/routes/review/[uri]/og.png/+server.ts
··· 4 4 import { error, type RequestHandler } from '@sveltejs/kit'; 5 5 6 6 const template = (data: MainRecord) => { 7 + 8 + const backdrop = data.record.metadata?.backdrop_path; 9 + const poster = data.record.metadata?.poster_path; 10 + 11 + const avatar = data.author.avatar; 12 + const displayName = data.author.displayName ?? data.author.handle; 13 + const title = data.record.metadata?.title; 14 + const rating = data.record.rating?.value; 15 + 7 16 return ` 8 17 <div tw="bg-zinc-900 flex flex-col w-full h-full items-center justify-center"> 9 18 <div tw="flex absolute bottom-0 left-0 right-0 top-0 bg-sky-400"> 10 - <img src="https://image.tmdb.org/t/p/w780${data.record.metadata?.backdrop_path}" alt="" class="flex h-full w-full rounded-xl opacity-50" /> 19 + <img src="https://image.tmdb.org/t/p/w780${backdrop}" alt="" class="flex h-full w-full rounded-xl opacity-50" /> 11 20 12 21 <div tw="flex absolute h-full w-full bg-black/80"> 13 22 </div> 14 23 </div> 15 24 <div tw="flex flex-row w-full py-12 px-4 items-center justify-start p-8"> 16 25 <div tw="flex h-auto aspect-[3/2] w-82"> 17 - <img src="https://image.tmdb.org/t/p/w500${data.record.metadata?.poster_path}" alt="" 26 + <img src="https://image.tmdb.org/t/p/w500${poster}" alt="" 18 27 class="flex h-full w-full rounded-xl border-2 border-zinc-800" style="border-radius: 16px; border-width: 1px; border-color: #3f3f46;" /> 19 28 </div> 20 29 21 30 <div tw="flex flex-col text-7xl font-bold text-zinc-100 text-left px-8 max-w-3xl"> 22 31 <div tw="flex items-center"> 23 - ${data.author.avatar ? `<img src=${data.author.avatar} tw="w-16 h-16 rounded-full" />` : ``} 32 + ${avatar ? `<img src=${avatar} tw="w-16 h-16 rounded-full" />` : ``} 24 33 <div tw="flex truncate text-4xl text-zinc-100 pl-4"> 25 - ${data.author.displayName || data.author.handle} 34 + ${displayName} 26 35 <span tw="flex truncate text-sky-500 pl-2"> 27 36 reviewed 28 37 </span> 29 38 </div> 30 39 </div> 31 40 32 - <span tw="flex tracking-tight mt-4">${data.record.metadata?.title}</span> 41 + <span tw="flex tracking-tight mt-4">${title}</span> 33 42 34 43 ${ 35 - data.record.rating 44 + rating 36 45 ? `<div tw="flex items-center mt-8"> 37 46 <div tw="flex items-center"> 38 47 ··· 40 49 .fill(0) 41 50 .map( 42 51 (_, i) => `<svg 43 - tw="w-20 h-20 flex ${i * 2 < (data.record.rating?.value ?? 0) - 1 ? 'text-sky-400' : 'text-zinc-600'}" 52 + tw="w-20 h-20 flex ${i * 2 < (rating ?? 0) - 1 ? 'text-sky-400' : 'text-zinc-600'}" 44 53 viewBox="0 0 24 24" 45 54 fill="currentColor"> 46 55 <path
+16
src/routes/user/[handle]/refresh/+page.server.ts
··· 1 + import { env } from '$env/dynamic/private'; 2 + import { resolveHandle } from '$lib/bluesky.js'; 3 + import { redirect } from '@sveltejs/kit'; 4 + 5 + /** @type {import('./$types').PageServerLoad} */ 6 + export async function load(event) { 7 + const handle = event.params.handle; 8 + 9 + const did = await resolveHandle({ handle: handle }); 10 + 11 + const response = await fetch(`${env.BACKEND_URL}/api/refresh-user?did=${did}`); 12 + const data = await response.json(); 13 + console.log(data); 14 + 15 + redirect(302, `/user/${handle}`); 16 + }