updated README, added favorites reducer, updated types

This commit is contained in:
cscough 2024-05-31 01:20:17 -04:00
parent 8afe43cb96
commit 6ca7efc2f6
6 changed files with 131 additions and 56 deletions

View File

@ -1,23 +1,17 @@
A simple app that allows users to search for movies by title, review the search results and navigate to a movies detail page.
It contains: # Movie Search
### `A simple app that allows users to search for movies by title, review the search results and navigate to a movies detail page.`
- Navbar ### Containing:
- NavBar
- A page to search for movies by title: - A page to search for movies by title:
- searchParams to string
- Object.entries(searchParams)
.map(
([key, value], i, arr) =>
`${key}=${value}${i < arr.length - 1 ? "&" : ""}`
)
.toString();
- SearchBar component: - SearchBar component:
- Input for Title - Input for Title
- Submit button - Submit button
- Clear button - Clear button
- List of results - paginated so we might need a pagination component: - List of results:
- Title - Title
- Year - Year
- Type (Filter out "series"?) - hidden ? - Type (Filter out "series"?) - hidden ?
@ -26,7 +20,7 @@ It contains:
Error Handling component Error Handling component
- A page for individual movie details: - A page for individual movie details:
- Movie component: - Movie Details component:
- Title - Title
- Year - Year
- Rated - Rated
@ -41,7 +35,7 @@ It contains:
- Country - Country
- Awards - Awards
- Poster - Poster
- Ratings: [ { Source } ] - Ratings: `[ { Source, Value } ]`
- Metascore - Metascore
- imdbRating - imdbRating
- imdbVotes - imdbVotes
@ -53,27 +47,28 @@ It contains:
- Website - Website
- Response - Response
External API Docs: https://www.omdbapi.com/ ### External API Docs: [OMDB](https://www.omdbapi.com/)
Built on: ### Built on:
Next.js Next.js
Server side rendering for list & movie details to hide api token/network requests and eliminate the need for state-management on data returned from the external API. Server side rendering for search results list & movie details to hide api token/network requests and eliminate the need for state-management on data returned from the external API.
Switched to Tailwindcss for styling. Was using chakra ui components but it isn't server-side render friendly Switched to [tailwindcss](https://tailwindcss.com/) for styling. Was using [chakra ui](https://v2.chakra-ui.com/) components but it isn't server-side render friendly
Using React Context because Redux is cool for a good number of things, but when built correctly - React Context is powerful enough for most apps. It can be layered if need be, but for this example we will stick with a simple context: ### State Management:
Using React Context + simple useState management because Redux is cool for a good number of things, especially larger apps that need a central, client-side store to manage many peices of state in one place, as well as helper functions to manage data functions and state mutation. - React Context in combination with simple state managment hooks like useState or useReducer is powerful enough for most small apps that only need to manage a small amount of state in separate places. It can be layered if need be, but for this example we will stick with a simple context:
Since we're using server side rendering for pages that fetch data, we don't need a lot of state management. Since we're using server side rendering for pages that fetch data, we don't need a lot of state management.
We can implement Context in a client component for managing "Favorites" for a user if we'd like We can implement Context in a client component for managing "Favorites" for a user if we'd like
this might look like: this might look like:
- FavoriteContext - favorites, setFavorites - FavoriteContext - favorites, setFavorites
- FavoriteProvider - A wrapper for FavoriteContext.Provider with local state that we can render client-side and pass children through to make use of server-side rendering for child components (interleaving) - FavoriteProvider - A wrapper for FavoriteContext.Provider with local state that we can render client-side and pass children through to make use of server-side rendering for child components (interleaving)
- FavoriteBtn - a client-side component rendering a button and using useContext(FormContext) to consume favorites and setFavorites - filter favorites by id to prevent duplicats/allow removing from list. - FavoriteBtn - a client-side component rendering a button and using useContext(FormContext) to consume favorites and setFavorites - filter favorites by id to prevent duplicats/allow removing from list.
Known issues: ### Known Issues:
* https://github.com/vercel/next.js/issues/65161 * [Error when testing components using NextJS Image](https://github.com/vercel/next.js/issues/65161)
* https://github.com/vercel/next.js/issues/54757 * [Jest cannot test form actions - this is how we would test our SearchBar component fully in a unit test. E2E testing should be able to cover this though](https://github.com/vercel/next.js/issues/54757)

View File

@ -4,7 +4,10 @@ import { useContext } from "react";
import { FavoriteContext } from "./FavoriteContext"; import { FavoriteContext } from "./FavoriteContext";
export default function FavoriteBtn({ movie }: any) { export default function FavoriteBtn({ movie }: any) {
const { favorites, setFavorites } = useContext(FavoriteContext); const { favorites,
dispatch
// setFavorites
} = useContext(FavoriteContext);
const filteredFavorites = favorites.filter((f) => f.imdbID != movie.imdbID); const filteredFavorites = favorites.filter((f) => f.imdbID != movie.imdbID);
const isFavorite = const isFavorite =
favorites.filter((f) => f.imdbID == movie.imdbID).length == 1; favorites.filter((f) => f.imdbID == movie.imdbID).length == 1;
@ -13,9 +16,11 @@ export default function FavoriteBtn({ movie }: any) {
<button <button
onClick={() => { onClick={() => {
if (isFavorite) { if (isFavorite) {
setFavorites([...filteredFavorites]); dispatch({type:'Remove', payload: movie})
// setFavorites([...filteredFavorites]);
} else { } else {
setFavorites([...favorites, movie]); dispatch({type:'Add', payload: movie})
// setFavorites([...favorites, movie]);
} }
}} }}
className={`block text-yellow-500 rounded my-auto h-8 w-8 className={`block text-yellow-500 rounded my-auto h-8 w-8

View File

@ -1,21 +1,59 @@
'use client' "use client";
import { PropsWithChildren, createContext, useState } from "react"; import { PropsWithChildren, createContext, useReducer
import { IMovieCardItem } from "./MovieCard"; // , useState
} from "react";
// interface IFavoriteContext {
// favorites: Array<IMovieSearch>
// setFavorites: (value: Array<IMovieSearch>) => void
// }
// export const FavoriteContext = createContext<IFavoriteContext>({
// favorites: [],
// setFavorites: (_value: Array<IMovieSearch>) => { },
// });
// // Using interleaving to render this context provider client-side, but still utilize server-side renderung for children components
// export const FavoriteProvider = ({ children }: PropsWithChildren) => {
// const [favorites, setFavorites] = useState<Array<IMovieSearch>>([])
// return (
// <FavoriteContext.Provider value={{ favorites, setFavorites }}>{children}</FavoriteContext.Provider>
// )
// }
interface IFavoriteContext { interface IFavoriteContext {
favorites: Array<IMovieCardItem> favorites: FavoriteState;
setFavorites: (value: Array<IMovieCardItem>) => void dispatch: React.Dispatch<FavoriteAction>;
} }
type FavoriteState = Array<IMovieSearch>;
type FavoriteAction = { type: "Add" | "Remove"; payload: IMovieSearch };
function FavoriteReducer(state: FavoriteState, action: FavoriteAction) {
switch (action.type) {
case "Add":
state = [...state, action.payload];
break;
case "Remove":
state = state.filter((s) => s.imdbID != action.payload.imdbID);
default:
break;
}
return state;
}
const initialState: FavoriteState = [];
export const FavoriteContext = createContext<IFavoriteContext>({ export const FavoriteContext = createContext<IFavoriteContext>({
favorites: [], favorites: initialState,
setFavorites: (_value: Array<IMovieCardItem>) => { }, dispatch: () => null,
}); });
// Using interleaving to render this context provider client-side, but still utilize server-side renderung for children components export function FavoriteProvider({ children }: PropsWithChildren) {
export const FavoriteProvider = ({ children }: PropsWithChildren) => { const [favorites, dispatch] = useReducer(FavoriteReducer, initialState);
const [favorites, setFavorites] = useState<Array<IMovieCardItem>>([])
return ( return (
<FavoriteContext.Provider value={{ favorites, setFavorites }}>{children}</FavoriteContext.Provider> <FavoriteContext.Provider value={{ favorites, dispatch }}>
) {children}
} </FavoriteContext.Provider>
);
}

View File

@ -3,15 +3,8 @@ import ImgCard from "@/common/components/ImgCard";
import Link from "next/link"; import Link from "next/link";
import { primaryBtn } from "../classes"; import { primaryBtn } from "../classes";
export interface IMovieCardItem {
Title: string;
Year: string;
imdbID: string;
Type: string;
Poster: string;
}
interface IMovieCardProps { interface IMovieCardProps {
movie: IMovieCardItem; movie: IMovieSearch;
} }
export default function MovieCard({ movie }: IMovieCardProps) { export default function MovieCard({ movie }: IMovieCardProps) {

View File

@ -1,7 +1,7 @@
import MovieCard, { IMovieCardItem } from "./MovieCard"; import MovieCard from "./MovieCard";
export interface IMovieListProps { export interface IMovieListProps {
movies: Array<IMovieCardItem>; movies: Array<IMovieSearch>;
} }
export default function MovieList({ movies }: IMovieListProps) { export default function MovieList({ movies }: IMovieListProps) {

View File

@ -0,0 +1,44 @@
type OMDbResponse = "True" | "False";
interface IMovieSearch {
Title: string;
Year: string;
imdbID: string;
Type: string;
Poster: string | "N/A" ;
}
interface IMovieSearchResponse {
Search: Array<IMovieSearch>;
totalResults: string;
Response: OMDbResponse;
}
interface IMovieDetails extends IMovieSearch{
Rated: string;
Released: string;
Runtime: string;
Genre: string;
Director: string;
Writer: string;
Actors: string;
Plot: string;
Language: string;
Country: string;
Awards: string;
Ratings: [
{
Source: string;
Value: string;
}
];
Metascore: string;
imdbRating: string;
imdbVotes: string;
DVD: string;
BoxOffice: string;
Production: string;
Website: string;
Response: OMDbResponse;
}
interface INextPageProps {
searchParams: object;
}