-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GPTUBE-64 Create the "new analysis" modal in dashboard (#65)
* feat: adding initial code for the modal * feat: added basic query after query mutation * Refactor code button new analysis * Add validation on query video preview * Fix styles on new analysis preview * Adding scroll to card video preview * Add logo gptube component * Fix styles to card video preview --------- Co-authored-by: Camilo Mora <[email protected]>
- Loading branch information
Showing
13 changed files
with
354 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,144 @@ | ||
import { | ||
Input, | ||
Checkbox, | ||
Modal, | ||
ModalBody, | ||
ModalContent, | ||
ModalHeader, | ||
useDisclosure, | ||
Divider, | ||
} from '@nextui-org/react' | ||
import { useDebounce } from 'use-debounce' | ||
import { Video } from 'lucide-react' | ||
|
||
import { useVideoPreview } from '@/hooks/use-video-preview' | ||
import { isValidEmail, isValidYoutubeUrl } from '@/utils/validations.utils' | ||
import { useForm } from '@/hooks/use-form' | ||
import { useAuth } from '@/hooks/use-auth' | ||
|
||
import { Button } from '../Common/button' | ||
|
||
import { VideoPreview } from './video-preview' | ||
|
||
export function ButtonNewAnalysis() { | ||
const { user } = useAuth() | ||
const { handleChange, email, showEmail, url } = useForm({ | ||
url: '', | ||
email: user?.email || '', | ||
showEmail: true, | ||
}) | ||
|
||
const [debouncedUrl] = useDebounce(url, 500) | ||
const modalAnalysis = useDisclosure() | ||
const videoPreviewQuery = useVideoPreview(debouncedUrl) | ||
|
||
const isInvalidUrl = !isValidYoutubeUrl(url) | ||
const isInvalidEmail = email === '' || !isValidEmail(email) | ||
|
||
return ( | ||
<Button | ||
className="font-medium text-success hover:!text-white" | ||
color="success" | ||
radius="sm" | ||
variant="ghost" | ||
> | ||
New Analysis | ||
</Button> | ||
<> | ||
<Button | ||
className="font-medium text-success hover:!text-white" | ||
color="success" | ||
radius="sm" | ||
variant="ghost" | ||
onPress={modalAnalysis.onOpen} | ||
> | ||
New Analysis | ||
</Button> | ||
<Modal isOpen={modalAnalysis.isOpen} size="4xl" onOpenChange={modalAnalysis.onOpenChange}> | ||
<ModalContent> | ||
{onClose => ( | ||
<> | ||
<ModalHeader className="w-full justify-center flex-col gap-1 items-center p-5 border-b-2"> | ||
<Video className="w-8 h-8 text-gray-500" /> | ||
<span className="text-gray-500 text-sm font-medium">New video analysis</span> | ||
</ModalHeader> | ||
<ModalBody className="text-sm p-0"> | ||
<section className="grid grid-cols-[300px_10px_1fr] gap-6"> | ||
<aside className="flex flex-col gap-5 p-6"> | ||
<Input | ||
color={isInvalidUrl || videoPreviewQuery.isError ? 'danger' : 'success'} | ||
errorMessage={ | ||
isInvalidUrl | ||
? 'Please enter a valid Url' | ||
: videoPreviewQuery.isError | ||
? 'Something is wrong with the Url (check the video id)' | ||
: null | ||
} | ||
isInvalid={isInvalidUrl} | ||
label="Url" | ||
name="videoURL" | ||
placeholder="https://youtu.be/mv5SZ7i6QLI" | ||
type="url" | ||
value={url} | ||
variant="underlined" | ||
onValueChange={value => { | ||
handleChange({ | ||
name: 'url', | ||
value: value, | ||
}) | ||
}} | ||
/> | ||
<Checkbox | ||
classNames={{ | ||
label: 'text-sm', | ||
icon: 'text-white', | ||
}} | ||
color="success" | ||
isSelected={showEmail} | ||
radius="sm" | ||
size="md" | ||
onValueChange={value => { | ||
handleChange({ | ||
name: 'showEmail', | ||
value: value, | ||
}) | ||
}} | ||
> | ||
Send to email | ||
</Checkbox> | ||
{showEmail ? ( | ||
<Input | ||
color={isInvalidEmail ? 'danger' : 'success'} | ||
errorMessage={isInvalidEmail ? 'Please enter a valid E-mail' : null} | ||
isInvalid={isInvalidEmail} | ||
label="E-mail" | ||
name="email" | ||
placeholder="[email protected]" | ||
type="email" | ||
value={email} | ||
variant="underlined" | ||
onValueChange={value => { | ||
handleChange({ | ||
name: 'email', | ||
value: value, | ||
}) | ||
}} | ||
/> | ||
) : null} | ||
<Button | ||
className={`${ | ||
videoPreviewQuery.isSuccess ? 'hover:!bg-opacity-60' : '' | ||
} font-medium text-white disabled:cursor-not-allowed transition-opacity`} | ||
color="success" | ||
disabled={isInvalidUrl || isInvalidEmail || url.length === 0} | ||
radius="sm" | ||
onPress={onClose} | ||
> | ||
Start analysis | ||
</Button> | ||
</aside> | ||
<Divider orientation="vertical" /> | ||
<aside className="p-6 w-full h-full"> | ||
<VideoPreview {...videoPreviewQuery} /> | ||
</aside> | ||
</section> | ||
</ModalBody> | ||
</> | ||
)} | ||
</ModalContent> | ||
</Modal> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import type { ModelsYoutubePreAnalyzerRespBody } from '@/gptube-api' | ||
|
||
import { | ||
Card, | ||
CardBody, | ||
CardFooter, | ||
CardHeader, | ||
Chip, | ||
Image, | ||
ScrollShadow, | ||
Skeleton, | ||
} from '@nextui-org/react' | ||
import { ThumbsDown, ThumbsUp, Video } from 'lucide-react' | ||
|
||
export function VideoPreview({ | ||
isLoading, | ||
isSuccess, | ||
video, | ||
isIdle, | ||
}: { | ||
video: ModelsYoutubePreAnalyzerRespBody | undefined | ||
isLoading: boolean | ||
isSuccess: boolean | ||
isIdle: boolean | ||
}) { | ||
return ( | ||
<Card | ||
fullWidth | ||
className={`p-4 w-full h-full max-h-[400px] ${isIdle ? 'bg-gray-100' : ''}`} | ||
radius="sm" | ||
shadow="sm" | ||
> | ||
<ScrollShadow className="w-full h-full" offset={0} size={0}> | ||
{isLoading || isIdle ? ( | ||
<VideoSkeleton showSkeleton={!isIdle} /> | ||
) : isSuccess && video ? ( | ||
<> | ||
<CardHeader> | ||
<h3 className="text-lg font-semibold">{video.snippet?.title ?? ''}</h3> | ||
</CardHeader> | ||
<CardBody className="flex flex-col gap-4"> | ||
<Image | ||
alt={video.snippet?.title ?? ''} | ||
radius="none" | ||
src={video.snippet?.thumbnails?.standard?.url ?? ''} | ||
/> | ||
<div className="flex items-center justify-between w-full gap-2 font-medium"> | ||
<span className="text-black">{video.statistics?.viewCount} views</span> | ||
<section className="flex gap-1"> | ||
<div className="flex items-center gap-1"> | ||
<ThumbsUp className="w-4" /> | ||
<span>{video.statistics?.likeCount}</span> | ||
</div> | ||
<div className="flex items-center gap-1 text-black"> | ||
<ThumbsDown className="w-4" /> | ||
<span>{video.statistics?.dislikeCount ?? 0}</span> | ||
</div> | ||
</section> | ||
</div> | ||
</CardBody> | ||
<CardFooter className="flex flex-col gap-4"> | ||
<div className="flex flex-wrap gap-2"> | ||
{video.snippet?.tags?.slice(0, 5).map(tag => { | ||
return ( | ||
<Chip key={tag} className="whitespace-nowrap"> | ||
{tag} | ||
</Chip> | ||
) | ||
})} | ||
{video.snippet?.tags && video.snippet.tags.length > 5 ? ( | ||
<Chip>+{video.snippet.tags.length - 5}</Chip> | ||
) : null} | ||
</div> | ||
</CardFooter> | ||
</> | ||
) : null} | ||
</ScrollShadow> | ||
</Card> | ||
) | ||
} | ||
|
||
function VideoSkeleton({ showSkeleton }: { showSkeleton: boolean }) { | ||
return ( | ||
<Card | ||
fullWidth | ||
className={`p-4 w-full h-full min-h-[400px] ${!showSkeleton ? 'bg-gray-100' : ''}`} | ||
radius="sm" | ||
shadow="sm" | ||
> | ||
<ScrollShadow className="w-full h-full" offset={0} size={0}> | ||
{showSkeleton ? ( | ||
<div className="space-y-5 w-full h-full"> | ||
<Skeleton className="rounded-lg"> | ||
<div className="h-24 rounded-lg bg-default-300" /> | ||
</Skeleton> | ||
<div className="space-y-3"> | ||
<Skeleton className="w-3/5 rounded-lg"> | ||
<div className="w-3/5 h-3 rounded-lg bg-default-200" /> | ||
</Skeleton> | ||
<Skeleton className="w-4/5 rounded-lg"> | ||
<div className="w-4/5 h-3 rounded-lg bg-default-200" /> | ||
</Skeleton> | ||
<Skeleton className="w-2/5 rounded-lg"> | ||
<div className="w-2/5 h-3 rounded-lg bg-default-300" /> | ||
</Skeleton> | ||
</div> | ||
</div> | ||
) : ( | ||
<div className="flex items-center flex-col gap-1 justify-center h-full w-full"> | ||
<Video className="w-6 h-6 text-gray-500" /> | ||
<span className="font-medium text-gray-500">No video selected</span> | ||
</div> | ||
)} | ||
</ScrollShadow> | ||
</Card> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ | ||
/* eslint-disable react/jsx-no-useless-fragment */ | ||
|
||
import Image from 'next/image' | ||
|
||
import gptube_logo from '@/assets/icons/gptube_logo.svg' | ||
|
||
export function LogoGPTube({ className }: { className?: string }) { | ||
return <Image alt="GPTube logo" className={className} src={gptube_logo} /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const YOUTUBE_URL_REGEX = /^(https?\:\/\/)?((www\.)?youtube\.com|youtu\.be)\/.+$/g |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { useState } from 'react' | ||
|
||
export function useForm<T>(initialValues: T) { | ||
const [values, setValues] = useState<T>(initialValues) | ||
|
||
const handleChange = ({ name, value }: { name: keyof T; value: T[keyof T] }) => { | ||
setValues({ | ||
...values, | ||
[name]: value, | ||
}) | ||
} | ||
|
||
return { | ||
...values, | ||
handleChange, | ||
} | ||
} |
Oops, something went wrong.
fbddca8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
gptube-subscriptions – ./subscriptions-server
gptube-subscriptions-luckly083-gmailcom.vercel.app
gptube-subscriptions.vercel.app
gptube-subscriptions-git-main-luckly083-gmailcom.vercel.app