-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathshow-card.tsx
184 lines (171 loc) · 5.14 KB
/
show-card.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"use client"
import type { Show, ShowWithVideoAndGenre } from "~/lib/types"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog"
import { Skeleton } from "./ui/skeleton"
import { PlusCircle, CheckCircle } from "lucide-react"
import { useEffect } from "react"
import useSWR from "swr"
import { env } from "~/env.mjs"
import { SignedIn } from "@clerk/nextjs"
import { toggleMyShow, getMyShowStatus } from "~/actions"
import { useOptimisticAction, useAction } from "next-safe-action/hook"
export function ShowCard({
children,
show,
}: {
children: React.ReactNode
show: Show
}) {
return (
<Dialog>
<DialogTrigger>{children}</DialogTrigger>
<DialogContent className="max-w-3xl p-0">
<DialogHeader className="p-4 pb-0">
<DialogTitle className="flex items-center gap-1.5">
{show.title ?? show.name}
<SignedIn>
<SaveOrUnsave show={show} />
</SignedIn>
</DialogTitle>
<div className="flex items-center gap-1.5">
<p className="text-green-400">
{Math.round((show.vote_average * 100) / 10)}% Match
</p>
<p>
{show.release_date?.substring(0, 4) ??
show.first_air_date?.substring(0, 4)}
</p>
<p className="border border-neutral-500 px-1 text-xs text-white/50">
EN
</p>
</div>
<DialogDescription className="text-left">
{show.overview}
</DialogDescription>
<ShowGenres show={show} />
</DialogHeader>
<ShowTrailer show={show} />
</DialogContent>
</Dialog>
)
}
function SaveOrUnsave({ show }: { show: Show }) {
const {
execute: executeQuery,
res: initialRes,
isExecuting,
hasExecuted,
} = useAction(getMyShowStatus)
const {
execute: executeToggle,
optimisticData,
res,
isExecuting: isRunning,
} = useOptimisticAction(toggleMyShow, { isSaved: initialRes.data?.isSaved })
useEffect(() => {
executeQuery({ id: show.id })
}, [])
if (!show.backdrop_path && !show.poster_path) return
if (isExecuting && !hasExecuted)
return <Skeleton className="h-6 w-6 rounded-full" />
function doUpdate() {
void executeToggle(
{
id: show.id,
isSaved: res.data?.isSaved ?? initialRes.data!.isSaved,
movieOrTv: show.title ? "movie" : "tv",
},
{ isSaved: !res.data?.isSaved ?? !initialRes.data!.isSaved },
)
}
return (
<button onClick={doUpdate} disabled={isRunning}>
{optimisticData.isSaved ? (
<CheckCircle
className="h-6 w-6 cursor-pointer"
strokeWidth="1.5"
opacity={isRunning ? 0.5 : 1}
/>
) : (
<PlusCircle
className="h-6 w-6 cursor-pointer"
strokeWidth="1.5"
opacity={isRunning ? 0.5 : 1}
/>
)}
</button>
)
}
function ShowGenres({ show }: { show: Show }) {
const { data } = useShowWithVideoAndGenre(show)
if (data === undefined) return <Skeleton className="h-5 w-full" />
if (data === null) return
return (
<p className="text-left text-sm">
{data.genres.map((genre) => genre.name).join(", ")}
</p>
)
}
function ShowTrailer({ show }: { show: Show }) {
const { data } = useShowWithVideoAndGenre(show)
if (data === undefined) return <Skeleton className="aspect-video w-full" />
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (data === null || !data.videos.results)
return (
<div className="grid aspect-video animate-pulse place-content-center text-xl font-semibold">
No Trailer
</div>
)
return (
<iframe
src={`https://www.youtube.com/embed/${findTrailer(data)}`}
className="aspect-video w-full rounded-md"
/>
)
}
function findTrailer(show: ShowWithVideoAndGenre) {
const trailerIndex = show.videos.results.findIndex(
(item) => item.type === "Trailer",
)
if (trailerIndex === -1) return ""
const trailerKey = show.videos.results[trailerIndex]?.key
return trailerKey ?? ""
}
export function useShowWithVideoAndGenre(show: Show) {
const { data } = useSWR<ShowWithVideoAndGenre | { success: boolean }>(
`https://api.themoviedb.org/3/${show.title ? "movie" : "tv"}/${
show.id
}?api_key=${env.NEXT_PUBLIC_TMDB_API}&append_to_response=videos,genres`,
(url: string) => fetch(url).then((r) => r.json()),
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
)
if (data && "success" in data) return { data: null }
return { data }
}
// const Spinner = ({ ...props }: LucideProps) => (
// <svg viewBox="0 0 24 24" fill="none" {...props}>
// <circle
// cx="12"
// cy="12"
// r="10"
// className="stroke-slate-200"
// strokeWidth="4"
// />
// <path
// d="M12 22C14.6522 22 17.1957 20.9464 19.0711 19.0711C20.9464 17.1957 22 14.6522 22 12C22 9.34784 20.9464 6.8043 19.0711 4.92893C17.1957 3.05357 14.6522 2 12 2"
// className="stroke-emerald-500"
// strokeWidth="4"
// />
// </svg>
// )