diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index 76d88ed..5bb7964 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -117,6 +117,29 @@ "Avatar": true, "AvatarFallback": true, "AvatarImage": true, - "AccountProvider": true + "AccountProvider": true, + "AccountProviderContext": true, + "ThemeProviderContext": true, + "AddToWatchlistButton": true, + "DropdownMenu": true, + "DropdownMenuCheckboxItem": true, + "DropdownMenuContent": true, + "DropdownMenuGroup": true, + "DropdownMenuItem": true, + "DropdownMenuLabel": true, + "DropdownMenuPortal": true, + "DropdownMenuRadioGroup": true, + "DropdownMenuRadioItem": true, + "DropdownMenuSeparator": true, + "DropdownMenuShortcut": true, + "DropdownMenuSub": true, + "DropdownMenuSubContent": true, + "DropdownMenuSubTrigger": true, + "DropdownMenuTrigger": true, + "Watchlist": true, + "Movies": true, + "TvShows": true, + "WatchlistLayout": true, + "BackdropCard": true } } diff --git a/auto-imports.d.ts b/auto-imports.d.ts index db0cbac..a59eae2 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -7,10 +7,13 @@ export {} declare global { const $fetch: typeof import('./src/utils/$fetch')['default'] const $localStorage: typeof import('./src/utils/$local-storage')['default'] - const AccountProvider: typeof import('./src/providers/account')['AccountProvider'] + const AccountProvider: typeof import('./src/contexts/AccountContext/AccountProvider')['AccountProvider'] + const AccountProviderContext: typeof import('./src/contexts/AccountContext/AccountProvider')['AccountProviderContext'] + const AddToWatchlistButton: typeof import('./src/components/AddToWatchlistButton')['default'] const Avatar: typeof import('./src/components/ui/avatar')['Avatar'] const AvatarFallback: typeof import('./src/components/ui/avatar')['AvatarFallback'] const AvatarImage: typeof import('./src/components/ui/avatar')['AvatarImage'] + const BackdropCard: typeof import('./src/components/BackdropCard')['default'] const Badge: typeof import('./src/components/ui/badge')['Badge'] const Button: typeof import('./src/components/ui/button')['Button'] const CardItem: typeof import('./src/components/CardItem')['default'] @@ -29,11 +32,27 @@ declare global { const DialogPortal: typeof import('./src/components/ui/dialog')['DialogPortal'] const DialogTitle: typeof import('./src/components/ui/dialog')['DialogTitle'] const DialogTrigger: typeof import('./src/components/ui/dialog')['DialogTrigger'] + const DropdownMenu: typeof import('./src/components/ui/dropdown-menu')['DropdownMenu'] + const DropdownMenuCheckboxItem: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuCheckboxItem'] + const DropdownMenuContent: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuContent'] + const DropdownMenuGroup: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuGroup'] + const DropdownMenuItem: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuItem'] + const DropdownMenuLabel: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuLabel'] + const DropdownMenuPortal: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuPortal'] + const DropdownMenuRadioGroup: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuRadioGroup'] + const DropdownMenuRadioItem: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuRadioItem'] + const DropdownMenuSeparator: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuSeparator'] + const DropdownMenuShortcut: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuShortcut'] + const DropdownMenuSub: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuSub'] + const DropdownMenuSubContent: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuSubContent'] + const DropdownMenuSubTrigger: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuSubTrigger'] + const DropdownMenuTrigger: typeof import('./src/components/ui/dropdown-menu')['DropdownMenuTrigger'] const Home: typeof import('./src/pages/Home')['default'] const Input: typeof import('./src/components/ui/input')['Input'] const Label: typeof import('./src/components/ui/label')['Label'] const Link: typeof import('react-router-dom')['Link'] const Movie: typeof import('./src/pages/Show/Movie')['default'] + const Movies: typeof import('./src/pages/Watchlist/Movies')['default'] const NavLink: typeof import('react-router-dom')['NavLink'] const Navbar: typeof import('./src/components/Navbar')['default'] const Navigate: typeof import('react-router-dom')['Navigate'] @@ -58,7 +77,8 @@ declare global { const SelectValue: typeof import('./src/components/ui/select')['SelectValue'] const Setting: typeof import('./src/pages/Setting')['default'] const SimilarCardItem: typeof import('./src/components/SimilarCardItem')['default'] - const ThemeProvider: typeof import('./src/providers/theme')['ThemeProvider'] + const ThemeProvider: typeof import('./src/contexts/ThemeContext/ThemeProvider')['ThemeProvider'] + const ThemeProviderContext: typeof import('./src/contexts/ThemeContext/ThemeProvider')['ThemeProviderContext'] const Toast: typeof import('./src/components/ui/toast')['Toast'] const ToastAction: typeof import('./src/components/ui/toast')['ToastAction'] const ToastClose: typeof import('./src/components/ui/toast')['ToastClose'] @@ -68,8 +88,10 @@ declare global { const ToastViewport: typeof import('./src/components/ui/toast')['ToastViewport'] const Toaster: typeof import('./src/components/ui/toaster')['Toaster'] const Tv: typeof import('./src/pages/Show/Tv')['default'] + const TvShows: typeof import('./src/pages/Watchlist/TvShows')['default'] const WatchProvider: typeof import('./src/components/WatchProvider')['default'] const WatchProviderContainer: typeof import('./src/components/WatchProviderContainer')['default'] + const WatchlistLayout: typeof import('./src/layouts/WatchlistLayout')['default'] const badgeVariants: typeof import('./src/components/ui/badge')['badgeVariants'] const buttonVariants: typeof import('./src/components/ui/button')['buttonVariants'] const cn: typeof import('./src/lib/utils')['cn'] @@ -89,7 +111,7 @@ declare global { const runtimeDuration: typeof import('./src/utils/runtime-duration')['default'] const startTransition: typeof import('react')['startTransition'] const toast: typeof import('./src/components/ui/use-toast')['toast'] - const useAccount: typeof import('./src/providers/account')['useAccount'] + const useAccount: typeof import('./src/contexts/AccountContext/useAccount')['useAccount'] const useCallback: typeof import('react')['useCallback'] const useContext: typeof import('react')['useContext'] const useDebugValue: typeof import('react')['useDebugValue'] @@ -104,7 +126,7 @@ declare global { const useInsertionEffect: typeof import('react')['useInsertionEffect'] const useLayoutEffect: typeof import('react')['useLayoutEffect'] const useLinkClickHandler: typeof import('react-router-dom')['useLinkClickHandler'] - const useLocalStorage: typeof import('./src/hooks/useLocalStorage')['default'] + const useLocalStorage: typeof import("./src/hooks/useLocalStorage")["default"] const useLocation: typeof import('react-router-dom')['useLocation'] const useMemo: typeof import('react')['useMemo'] const useNavigate: typeof import('react-router-dom')['useNavigate'] @@ -119,7 +141,7 @@ declare global { const useSearchParams: typeof import('react-router-dom')['useSearchParams'] const useState: typeof import('react')['useState'] const useSyncExternalStore: typeof import('react')['useSyncExternalStore'] - const useTheme: typeof import('./src/providers/theme')['useTheme'] + const useTheme: typeof import('./src/contexts/ThemeContext/useTheme')['useTheme'] const useToast: typeof import('./src/components/ui/use-toast')['useToast'] const useTransition: typeof import('react')['useTransition'] } diff --git a/package.json b/package.json index 50a0ed4..b18bc34 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-select": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af5a8c4..8dfd37c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.6 + version: 2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) @@ -881,6 +884,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.66)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -962,6 +992,44 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.66)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: @@ -1056,6 +1124,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} peerDependencies: diff --git a/src/App.tsx b/src/App.tsx index 239e7e8..6682068 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,26 @@ import "./App.css"; function App() { - return ( - - - - - - } /> - } /> - } /> - } /> - } /> - } /> - - - - - - ); + return ( + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + ); } export default App; diff --git a/src/components/AddToWatchlistButton.tsx b/src/components/AddToWatchlistButton.tsx new file mode 100644 index 0000000..b018993 --- /dev/null +++ b/src/components/AddToWatchlistButton.tsx @@ -0,0 +1,40 @@ +import type { AccountStates } from '@/types/response'; +import { ResponseMessage } from '@/utils/$fetch'; + +interface Props { + states: AccountStates; + type: 'movie' | 'tv'; + mediaId: number | string; +} + +export default function AddToWatchlistButton({ states, type, mediaId }: Props) { + const { account } = useAccount(); + const { toast } = useToast(); + const [isWatchlisted, setIsWatchlisted] = useState(states.watchlist) + const addToWatchlist = async () => { + try { + const { data } = await $fetch(`/account/${account?.id}/watchlist`, { + method: 'POST', + body: { + "media_type": type, + "media_id": mediaId, + "watchlist": isWatchlisted === true ? false : true, + } + }); + setIsWatchlisted(!isWatchlisted); + + toast({ + description: data.status_message + }) + } catch (e) { + console.error(e) + } + }; + + + return ( + + ) +} diff --git a/src/components/BackdropCard.tsx b/src/components/BackdropCard.tsx new file mode 100644 index 0000000..2e59360 --- /dev/null +++ b/src/components/BackdropCard.tsx @@ -0,0 +1,40 @@ +import type { ComponentPropsWithRef } from "react"; + +export interface SimpleBaseMedia { + adult: boolean; + backdrop_path: string; + genre_ids: number[]; + id: number; + original_language: string; + overview: string; + popularity: number; + poster_path: string; + vote_average: number; + vote_count: number; +} + + +interface BackdropCardProps extends ComponentPropsWithRef<'div'> { + media: T; + title: string; +} + +export default function BackdropCard({ media, title, ...props }: BackdropCardProps) { + const isMovieType = Object.prototype.hasOwnProperty.call(media, 'video'); + + return ( +
+ +
+ {title} +
+
+ ) +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 752693e..b425d6e 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,54 +1,75 @@ +import { LibraryBig, Settings } from "lucide-react"; import type { FormEvent } from "react"; -import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; export default function Navbar() { - const [searchParams] = useSearchParams(); - const querySearch = searchParams.get("title"); - const [search, setSearch] = useState(querySearch ?? ""); - const navigate = useNavigate(); - const { account } = useAccount() + const [searchParams] = useSearchParams(); + const querySearch = searchParams.get("title"); + const [search, setSearch] = useState(querySearch ?? ""); + const navigate = useNavigate(); + const { account } = useAccount() - const handleSearch = (e: FormEvent) => { - e.preventDefault(); - if (!search.length) { - return; - } - navigate({ - pathname: "/search", - search: `?title=${search}&type=movie`, - }); - }; + const handleSearch = (e: FormEvent) => { + e.preventDefault(); + if (!search.length) { + return; + } + navigate({ + pathname: "/search", + search: `?title=${search}&type=movie`, + }); + }; - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/src/components/PopupYoutubeTrailer.tsx b/src/components/PopupYoutubeTrailer.tsx index 38cb3ca..1625cfe 100644 --- a/src/components/PopupYoutubeTrailer.tsx +++ b/src/components/PopupYoutubeTrailer.tsx @@ -1,21 +1,21 @@ export default function PopupYoutubeTrailer({ video }: { video: string }) { - return ( - - - Watch Trailer - - -