Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Post completion overlay #1505

Merged
merged 9 commits into from
Jan 31, 2025
Merged
144 changes: 144 additions & 0 deletions src/components/posts/video-player-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Link from 'next/link'
import Image from 'next/image'

import {
useVideoPlayerOverlay,
type CompletedAction,
} from '@/hooks/mux/use-video-player-overlay'
import {Post} from '@/pages/[post]'
import Spinner from '@/spinner'
import {Button} from '@/ui/button'
import {ArrowRight} from 'lucide-react'
import {RefreshCw} from 'lucide-react'
import {Card, CardContent, CardTitle, CardHeader, CardFooter} from '@/ui/card'
import {motion} from 'framer-motion'

type VideoPlayerOverlayProps = {
resource: Post
}

const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({resource}) => {
const {state: overlayState, dispatch} = useVideoPlayerOverlay()

switch (overlayState.action?.type) {
case 'COMPLETED':
const {playerRef, cta} = overlayState.action as CompletedAction

if (cta === 'cursor_workshop') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if cta is cursor_workshop display the custom overlay, otherwise display default

return (
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
aria-live="polite"
className="z-40 bg-background/85 absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center pb-6 backdrop-blur-md sm:pb-16 text-white"
>
<CursorCTAOverlay
signUpLink="/workshop/cursor"
onReplay={() => {
if (playerRef.current) {
playerRef.current.play()
}
dispatch({type: 'HIDDEN'})
}}
/>
</motion.div>
)
}

return (
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
aria-live="polite"
className="z-40 bg-background/85 absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center pb-6 backdrop-blur-md sm:pb-16 text-white"
>
<div className="flex flex-col items-center justify-center gap-2 p-4">
<h2 className="text-lg sm:text-2xl font-bold mb-4 text-center">
{resource.fields.title}
</h2>
<Button
variant="outline"
onClick={() => {
if (playerRef.current) {
playerRef.current.play()
}
dispatch({type: 'HIDDEN'})
}}
className="border border-blue-600 hover:bg-blue-600"
>
<RefreshCw className="mr-2 h-4 w-4" />
Watch again
</Button>
Comment on lines +57 to +73
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default overlay is simple right now with a watch again button. @vojtaholik we could put vector search results here if we implemented that

</div>
</motion.div>
)
case 'LOADING':
return (
<div
aria-live="polite"
className="text-foreground absolute left-0 top-0 z-40 flex aspect-video h-full w-full flex-col items-center justify-center gap-10 bg-black/80 p-5 text-lg backdrop-blur-md"
>
<Spinner className="text-white" />
</div>
)
case 'HIDDEN':
return null
default:
return null
}
}

export default VideoPlayerOverlay

function CursorCTAOverlay({
signUpLink,
onReplay,
}: {
signUpLink: string
onReplay: () => void
}) {
return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 p-4">
<Card className="w-full max-w-2xl border-none">
<CardContent className="text-center p-0 sm:p-6">
<div className="sm:px-8 flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2">
<Image
src="https://d2eip9sf3oo6c2.cloudfront.net/tags/images/000/001/411/full/cursor.png"
alt="Cursor"
width={80}
height={80}
className="hidden sm:block"
/>
<h2 className="text-md sm:text-2xl font-bold sm:mb-4">
Reliably Build Software with AI through Cursor
</h2>
</div>
<p className="text-center mb-2 sm:mb-6 text-muted-foreground">
John Lindquist is teaching a workshop on how to use Cursor to it's
fullest abilities. Get notified when the workshop is released.
</p>
</div>
</CardContent>
<CardFooter className="flex gap-2 items-center justify-center p-0">
<Button
variant="outline"
onClick={onReplay}
className="border border-blue-600 hover:bg-blue-600"
>
<RefreshCw className="mr-2 h-4 w-4" />
Watch again
</Button>
<Link href={signUpLink} className="">
<Button className="w-full max-w-xs bg-blue-500 hover:bg-blue-600">
Join Waitlist
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</Link>
</CardFooter>
</Card>
</div>
)
}
95 changes: 95 additions & 0 deletions src/hooks/mux/use-video-player-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client'

import React, {createContext, Reducer, useContext, useReducer} from 'react'
import type {MuxPlayerRefAttributes} from '@mux/mux-player-react'

type VideoPlayerOverlayState = {
action: VideoPlayerOverlayAction | null
}

export type CompletedAction = {
type: 'COMPLETED'
playerRef: React.RefObject<MuxPlayerRefAttributes | null>
cta?: string
}

export type VideoPlayerOverlayAction =
| CompletedAction
| {type: 'BLOCKED'}
| {type: 'HIDDEN'}
| {type: 'LOADING'}

const initialState: VideoPlayerOverlayState = {
action: {
type: 'HIDDEN',
},
}

const reducer: Reducer<VideoPlayerOverlayState, VideoPlayerOverlayAction> = (
state,
action,
) => {
switch (action.type) {
case 'COMPLETED':
// TODO: Track video completion
return {
...state,
action,
}
case 'LOADING':
console.log('loading')
return {
...state,
action,
}
case 'BLOCKED':
return {
...state,
action,
}
case 'HIDDEN': {
return {
...state,
action,
}
}
default:
return state
}
}

type VideoPlayerOverlayContextType = {
state: VideoPlayerOverlayState
dispatch: React.Dispatch<VideoPlayerOverlayAction>
}

export const VideoPlayerOverlayProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [state, dispatch] = useReducer(reducer, initialState)

const value = React.useMemo(() => ({state, dispatch}), [state, dispatch])

return (
<VideoPlayerOverlayContext.Provider value={value}>
{children}
</VideoPlayerOverlayContext.Provider>
)
}

const VideoPlayerOverlayContext = createContext<
VideoPlayerOverlayContextType | undefined
>(undefined)

export const useVideoPlayerOverlay = () => {
const context = useContext(VideoPlayerOverlayContext)
if (!context) {
throw new Error(
'useVideoPlayerContext must be used within a VideoPlayerProvider',
)
}
return {
state: context.state,
dispatch: context.dispatch,
}
}
36 changes: 36 additions & 0 deletions src/hooks/use-mux-player.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'

import React, {createContext, useContext} from 'react'
import type {MuxPlayerRefAttributes} from '@mux/mux-player-react'

type MuxPlayerContextType = {
setMuxPlayerRef: React.Dispatch<
React.SetStateAction<React.RefObject<MuxPlayerRefAttributes | null> | null>
>
muxPlayerRef: React.RefObject<MuxPlayerRefAttributes | null> | null
}

export const MuxPlayerProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [muxPlayerRef, setMuxPlayerRef] =
React.useState<React.RefObject<MuxPlayerRefAttributes | null> | null>(null)

return (
<MuxPlayerContext.Provider value={{muxPlayerRef, setMuxPlayerRef}}>
{children}
</MuxPlayerContext.Provider>
)
}

const MuxPlayerContext = createContext<MuxPlayerContextType | undefined>(
undefined,
)

export const useMuxPlayer = () => {
const context = useContext(MuxPlayerContext)
if (!context) {
throw new Error('useMuxPlayer must be used within a MuxPlayerProvider')
}
return context
}
Loading