1- import { createFileRoute , Link , redirect } from "@tanstack/react-router" ;
2- import { queryClient } from "./__root" ;
3- import { videoQueryOptions } from "../lib/query-utils" ;
4- import { useQuery } from "@tanstack/react-query" ;
5- import { ViewIncrementer } from "../components/view-incrementer" ;
6- import {
7- EyeIcon ,
8- Loader2Icon ,
9- SquareArrowOutUpRightIcon ,
10- VideoIcon ,
11- } from "lucide-react" ;
121import { Button } from "@/components/ui/button" ;
2+ import { Card , CardContent } from "@/components/ui/card" ;
133import { useUser } from "@clerk/tanstack-start" ;
4+ import { getAuth } from "@clerk/tanstack-start/server" ;
5+ import { Separator } from "@radix-ui/react-dropdown-menu" ;
6+ import {
7+ Link ,
8+ createFileRoute ,
9+ notFound ,
10+ redirect ,
11+ } from "@tanstack/react-router" ;
12+ import { createServerFn } from "@tanstack/start" ;
13+ import { getWebRequest } from "@tanstack/start/server" ;
1414import { MediaPlayer , MediaProvider , Poster } from "@vidstack/react" ;
1515import {
16- defaultLayoutIcons ,
1716 DefaultVideoLayout ,
17+ defaultLayoutIcons ,
1818} from "@vidstack/react/player/layouts/default" ;
19- import { Card , CardContent } from "@/components/ui/card" ;
20- import { WordyDate } from "../components/wordy-date" ;
21- import { AuthorInfo } from "../components/author-info" ;
22- import { Separator } from "@radix-ui/react-dropdown-menu" ;
23-
24- import themeCss from "@vidstack/react/player/styles/default/theme.css?url" ;
2519import audioCss from "@vidstack/react/player/styles/default/layouts/audio.css?url" ;
2620import videoCss from "@vidstack/react/player/styles/default/layouts/video.css?url" ;
21+ import themeCss from "@vidstack/react/player/styles/default/theme.css?url" ;
22+ import {
23+ EyeIcon ,
24+ Loader2Icon ,
25+ SquareArrowOutUpRightIcon ,
26+ VideoIcon ,
27+ } from "lucide-react" ;
28+ import { z } from "zod" ;
29+ import { AuthorInfo } from "../components/author-info" ;
30+ import { ViewIncrementer } from "../components/view-incrementer" ;
31+ import { WordyDate } from "../components/wordy-date" ;
32+ import { getVideoDataServerFn } from "../server-fns/video-player" ;
33+
34+ const fetchVideoData = createServerFn ( { method : "POST" } )
35+ . validator ( z . object ( { videoId : z . string ( ) } ) )
36+ . handler ( async ( { data } ) => {
37+ const video = await getVideoDataServerFn ( {
38+ data : { videoId : data . videoId } ,
39+ } ) ;
40+
41+ if ( video . videoData . isPrivate ) {
42+ const { userId } = await getAuth ( getWebRequest ( ) ! ) ;
43+
44+ if ( userId !== video . videoData . authorId ) {
45+ throw notFound ( ) ;
46+ }
47+ }
48+
49+ return video ;
50+ } ) ;
2751
2852export const Route = createFileRoute ( "/p/$videoId" ) ( {
2953 component : RouteComponent ,
3054 loader : ( { params } ) => {
31- if ( ! params . videoId ) {
32- throw redirect ( { to : "/" } ) ;
33- }
34-
35- queryClient . prefetchQuery ( videoQueryOptions ( params . videoId ) ) ;
55+ return fetchVideoData ( { data : { videoId : params . videoId } } ) ;
3656 } ,
57+ ssr : true ,
3758 head : ( ) => ( {
3859 links : [
3960 { rel : "stylesheet" , href : themeCss } ,
@@ -45,16 +66,11 @@ export const Route = createFileRoute("/p/$videoId")({
4566
4667function RouteComponent ( ) {
4768 const { videoId } = Route . useParams ( ) ;
48-
49- const { data } = useQuery ( videoQueryOptions ( videoId ) ) ;
69+ const video = Route . useLoaderData ( ) ;
5070
5171 const { user } = useUser ( ) ;
5272
53- if ( ! data ) {
54- return null ;
55- }
56-
57- const { videoData, videoSources } = data ;
73+ const { videoData, videoSources } = video ;
5874
5975 const isViewerAuthor = user ?. id === videoData . authorId ;
6076
@@ -100,18 +116,18 @@ function RouteComponent() {
100116 streamType = "on-demand"
101117 playsInline
102118 title = { videoData . title }
103- poster = { data . largeThumbnailUrl ?? undefined }
119+ poster = { video . largeThumbnailUrl ?? undefined }
104120 duration = { videoData . videoLengthSeconds ?? undefined }
105121 storage = "player"
106122 >
107123 < MediaProvider >
108- { data . largeThumbnailUrl !== null && (
109- < Poster className = "vds-poster" src = { data . largeThumbnailUrl } />
124+ { video . largeThumbnailUrl !== null && (
125+ < Poster className = "vds-poster" src = { video . largeThumbnailUrl } />
110126 ) }
111127 </ MediaProvider >
112128 < DefaultVideoLayout
113129 icons = { defaultLayoutIcons }
114- thumbnails = { data . storyboard }
130+ thumbnails = { video . storyboard }
115131 />
116132 </ MediaPlayer >
117133 < div className = "flex flex-col gap-4 min-w-96 w-96 grow" >
@@ -120,7 +136,7 @@ function RouteComponent() {
120136 < h1 className = "text-2xl font-bold" > { videoData . title } </ h1 >
121137 < div className = "flex flex-col md:flex-row items-start md:items-center justify-between text-sm text-muted-foreground" >
122138 < span >
123- Uploaded on < WordyDate timestamp = { data . videoCreatedAt } />
139+ Uploaded on < WordyDate timestamp = { video . videoCreatedAt } />
124140 </ span >
125141 < span className = "flex items-center gap-1" >
126142 < EyeIcon className = "w-4 h-4" />
0 commit comments