Skip to content

Commit

Permalink
GPTUBE-64 Create the "new analysis" modal in dashboard (#65)
Browse files Browse the repository at this point in the history
* 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
webtaken and CamiloEMP authored Oct 30, 2023
1 parent bef30f7 commit fbddca8
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 32 deletions.
14 changes: 7 additions & 7 deletions gptube/assets/icons/NextIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const NextPage = (props: { className: string }) => {
function NextPage(props: { className: string }) {
return (
<svg
aria-hidden="true"
Expand All @@ -8,12 +8,12 @@ const NextPage = (props: { className: string }) => {
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
fillRule="evenodd"
/>
</svg>
);
};
)
}

export default NextPage;
export default NextPage
14 changes: 7 additions & 7 deletions gptube/assets/icons/PrevPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const PrevPage = (props: { className: string }) => {
function PrevPage(props: { className: string }) {
return (
<svg
aria-hidden="true"
Expand All @@ -8,12 +8,12 @@ const PrevPage = (props: { className: string }) => {
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z"
clipRule="evenodd"
></path>
d="M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z"
fillRule="evenodd"
/>
</svg>
);
};
)
}

export default PrevPage;
export default PrevPage
146 changes: 138 additions & 8 deletions gptube/components/dashboard/button-new-analysis.tsx
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>
</>
)
}
117 changes: 117 additions & 0 deletions gptube/components/dashboard/video-preview.tsx
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>
)
}
10 changes: 10 additions & 0 deletions gptube/components/gptube-logo.tsx
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} />
}
6 changes: 2 additions & 4 deletions gptube/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { Navbar, NavbarBrand, NavbarContent, NavbarItem } from '@nextui-org/navb
import Link from 'next/link'
import { Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react'
import { LogIn } from 'lucide-react'
import Image from 'next/image'

import gptube_logo from '@/assets/icons/gptube_logo.svg'
import { useAuth, useAuthActions } from '@/hooks/use-auth'

import { Button } from './Common/button'
import { LogoGPTube } from './gptube-logo'

export function Header() {
const { user } = useAuth()
Expand All @@ -16,8 +15,7 @@ export function Header() {
return (
<Navbar isBordered maxWidth="lg">
<NavbarBrand className="gap-2">
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */},
<Image alt="GPTube logo" className="w-10 h-10" src={gptube_logo} />
<LogoGPTube className="w-10 h-10" />
<p className="text-2xl font-bold">GPTube</p>
</NavbarBrand>
<NavbarContent justify="end">
Expand Down
1 change: 1 addition & 0 deletions gptube/constants/youtube.constants.ts
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
17 changes: 17 additions & 0 deletions gptube/hooks/use-form.ts
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,
}
}
Loading

1 comment on commit fbddca8

@vercel
Copy link

@vercel vercel bot commented on fbddca8 Oct 30, 2023

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

Please sign in to comment.