From a97830eab7f63c003217e1ed28977fc40e7bda71 Mon Sep 17 00:00:00 2001 From: Valik Date: Sat, 10 Feb 2024 11:32:42 +0100 Subject: [PATCH] feat: add error and no data alerts --- src/@/components/ui/alert.jsx | 50 +++++++++++++++++++++++++ src/components/Alert.jsx | 53 +++++++++++++++++++++++++++ src/components/Cast.jsx | 44 ++++++++++++---------- src/components/Reviews.jsx | 56 +++++++++++++++------------- src/output.css | 69 +++++++++++++++++++++++++++++++++++ src/pages/Home.jsx | 3 +- src/pages/MovieDetails.jsx | 9 ++--- src/pages/Movies.jsx | 21 +++++++---- 8 files changed, 245 insertions(+), 60 deletions(-) create mode 100644 src/@/components/ui/alert.jsx create mode 100644 src/components/Alert.jsx diff --git a/src/@/components/ui/alert.jsx b/src/@/components/ui/alert.jsx new file mode 100644 index 0000000..4062905 --- /dev/null +++ b/src/@/components/ui/alert.jsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { cva } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +const Alert = React.forwardRef(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = 'Alert'; + +const AlertTitle = React.forwardRef(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = React.forwardRef(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = 'AlertDescription'; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/Alert.jsx b/src/components/Alert.jsx new file mode 100644 index 0000000..d4b59a3 --- /dev/null +++ b/src/components/Alert.jsx @@ -0,0 +1,53 @@ +import { + ExclamationTriangleIcon, + InfoCircledIcon, +} from '@radix-ui/react-icons'; + +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; + +export const AlertDestructive = ({ message }) => { + return ( + + + Error + {message} + + ); +}; + +export const AlertInfo = () => { + return ( + + + No Search Results + + Your search did not match any movies. Please try again with different + keywords or filters. + + + ); +}; + +export const AlertNoCast = () => { + return ( + + + No Cast Information + + There is no information available about the cast for this movie. + + + ); +}; + +export const AlertNoReviews = () => { + return ( + + + No Reviews Yet + + There are currently no reviews available for this movie. + + + ); +}; diff --git a/src/components/Cast.jsx b/src/components/Cast.jsx index d13591e..f7b9341 100644 --- a/src/components/Cast.jsx +++ b/src/components/Cast.jsx @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { fetchCast } from 'services/fetchCast'; import { Loader } from './Loader'; +import { AlertDestructive, AlertNoCast } from './Alert'; const Cast = () => { const { movieId } = useParams(); @@ -18,29 +19,32 @@ const Cast = () => { } if (error) { - return
Error fetching data: {error.message}
; + return ; } return ( -
- {data.map(actor => ( -
    -
  • - {actor.name} -

    {actor.name}

    -

    {actor.character}

    -
  • -
- ))} -
+ <> + {!isLoading && data.length === 0 && } +
+ {data.map(actor => ( +
    +
  • + {actor.name} +

    {actor.name}

    +

    {actor.character}

    +
  • +
+ ))} +
+ ); }; diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 4df6d21..fbea856 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { fetchReviews } from 'services/fetchReviews'; import { Loader } from './Loader'; +import { AlertDestructive, AlertNoReviews } from './Alert'; import { format } from 'date-fns'; import ReactMarkdown from 'react-markdown'; @@ -30,35 +31,38 @@ const Reviews = () => { } if (error) { - return
Error fetching data: {error.message}
; + return ; } return ( -
- {data.map(review => ( -
    -
  • - - - {review.author} - - {format(new Date(review.created_at), 'HH:mm, MMMM d, yyyy')} - - - - {/<([A-Za-z][A-Za-z0-9]*)\b[^>]*>(.*?)<\/\1>/.test( - review.content - ) ? ( -

    {parse(review.content)}

    - ) : ( - {review.content} - )} -
    -
    -
  • -
- ))} -
+ <> + {!isLoading && data.length === 0 && } +
+ {data.map(review => ( +
    +
  • + + + {review.author} + + {format(new Date(review.created_at), 'HH:mm, MMMM d, yyyy')} + + + + {/<([A-Za-z][A-Za-z0-9]*)\b[^>]*>(.*?)<\/\1>/.test( + review.content + ) ? ( +

    {parse(review.content)}

    + ) : ( + {review.content} + )} +
    +
    +
  • +
+ ))} +
+ ); }; diff --git a/src/output.css b/src/output.css index 35c4a07..94c4632 100644 --- a/src/output.css +++ b/src/output.css @@ -657,6 +657,14 @@ body { margin-top: 0.375rem; } +.mb-1 { + margin-bottom: 0.25rem; +} + +.mt-8 { + margin-top: 2rem; +} + .flex { display: flex; } @@ -742,6 +750,12 @@ body { width: max-content; } +.w-fit { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + .max-w-2xl { max-width: 42rem; } @@ -908,6 +922,10 @@ body { border-color: transparent; } +.border-destructive\/50 { + border-color: hsl(var(--destructive) / 0.5); +} + .bg-background { background-color: hsl(var(--background)); } @@ -1003,6 +1021,11 @@ body { padding-bottom: 2rem; } +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + .pb-2 { padding-bottom: 0.5rem; } @@ -1121,6 +1144,15 @@ body { color: hsl(var(--secondary-foreground)); } +.text-destructive { + color: hsl(var(--destructive)); +} + +.text-blue-500 { + --tw-text-opacity: 1; + color: rgb(59 130 246 / var(--tw-text-opacity)); +} + .underline-offset-4 { text-underline-offset: 4px; } @@ -1447,6 +1479,10 @@ body { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +:is(.dark .dark\:border-destructive) { + border-color: hsl(var(--destructive)); +} + @media (min-width: 640px) { .sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -1502,3 +1538,36 @@ body { line-height: 1; } } + +.\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div { + --tw-translate-y: -3px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.\[\&\>svg\]\:absolute>svg { + position: absolute; +} + +.\[\&\>svg\]\:left-4>svg { + left: 1rem; +} + +.\[\&\>svg\]\:top-4>svg { + top: 1rem; +} + +.\[\&\>svg\]\:text-destructive>svg { + color: hsl(var(--destructive)); +} + +.\[\&\>svg\]\:text-foreground>svg { + color: hsl(var(--foreground)); +} + +.\[\&\>svg\~\*\]\:pl-7>svg~* { + padding-left: 1.75rem; +} + +.\[\&_p\]\:leading-relaxed p { + line-height: 1.625; +} diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 3c3100c..c70b447 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -3,6 +3,7 @@ import { fetchTrendingData } from 'services/fetchTrendingData'; import MovieItem from 'components/MovieItem'; import { Loader } from 'components/Loader'; +import { AlertDestructive } from 'components/Alert'; const Home = () => { const { isPending, isError, data, error } = useQuery({ @@ -15,7 +16,7 @@ const Home = () => { } if (isError) { - return
Error fetching data: {error.message}
; + return ; } return ( diff --git a/src/pages/MovieDetails.jsx b/src/pages/MovieDetails.jsx index 1141a45..073cf2f 100644 --- a/src/pages/MovieDetails.jsx +++ b/src/pages/MovieDetails.jsx @@ -1,14 +1,15 @@ -import { useParams } from 'react-router-dom'; +import { NavLink, Outlet, useParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { Suspense } from 'react'; -import { NavLink, Outlet } from 'react-router-dom'; import { format } from 'date-fns'; import { fetchMovieDetails } from 'services/fetchMovieDetails'; import { Badge } from '@/components/ui/badge'; + import { Loader } from 'components/Loader'; +import { AlertDestructive } from 'components/Alert'; const MovieDetails = () => { const { movieId } = useParams(); @@ -23,11 +24,9 @@ const MovieDetails = () => { } if (isError) { - return
Error fetching data: {error.message}
; + return ; } - console.debug(data); - return (
diff --git a/src/pages/Movies.jsx b/src/pages/Movies.jsx index 1780183..ea9ef41 100644 --- a/src/pages/Movies.jsx +++ b/src/pages/Movies.jsx @@ -7,7 +7,9 @@ import MovieItem from 'components/MovieItem'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; + import { Loader } from 'components/Loader'; +import { AlertDestructive, AlertInfo } from 'components/Alert'; import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; @@ -69,12 +71,12 @@ const Movies = () => {
{isLoading && } - {isError &&
Error fetching data: {error.message}
} -
- {data?.map(movie => ( - - ))} -
+ {isError && } + {searchParams.has('query') && data?.length === 0 && } + {data && !isLoading && ( +
+ {data?.map(movie => ( + + ))} +
+ )}
); };